java是很简单的,但是这背后支撑着java的是什么?

引言

jvm是一台虚拟机,能够执行java字节码,也就是java的汇编。

本篇文章将避免涉及具体的指令,从了解的程度来描述java字节码设计。

虚拟机结构

重点内容(为了方便理解,以下内容做了简化)

对于每一个class,有一个常量池、静态变量表

对于每一个实例对象,有一个成员变量表

对于每一个function,有一个局部变量表(类似list)、一个操作数堆栈

与8086汇编的区别

  1. 弃用了标志位、寄存器

  2. java字节码属于精简指令集,8086汇编属于复杂指令集

  3. 引入了局部变量表、常量池

  4. 有些不是可执行代码片段(包括函数标记、类标记)

    • 因此,java字节码保留了类、函数的概念,可以像高级语言一样调用函数
  5. 更多抽象类型的支持,的基本类型:

    1. 基本类型
      • int
      • byte
      • short
      • long
      • float
      • double
      • char
      • boolean
    2. 引用类型
      • 类实例
      • 数组
      • 接口
  6. java字节码更高级,难以使用逻辑电路实现

    • 异常处理
    • 字节码级别的面向对象
    • 字节码级别的多线程锁
    • 自动管理的函数调用(传参、返回)
    • 还隐含了runtime与jvm之间的各种操作,例如
      • 自动内存管理
      • 动态加载、反射

优势与代价

作为高级汇编,优势有

  • 是一个抽象层,模糊了不同架构CPU之间的差异,易于跨平台
  • 在汇编层面实现了面向对象支持,提高了简便性

但同时也有缺陷

  • 增加了抽象、面向对象、runtime的代价
  • 简便性的同时也意味着失去了灵活性,使得java难以在编译的时候进行优化(字节码层面的优化),各项优化高度依赖运行时

字节码设计

java的指令长度都是一个字节,因此叫做字节码。(不算额外参数)

同样,java字节码也有显式参数与隐含参数。

指令 显式参数 隐含参数(堆栈) 影响
加法运算 堆栈上栈顶两个参数 弹出栈`1顶两个元素并将运算结果压栈
无条件跳转 目标位置偏移量 程序逻辑跳转
函数调用 函数地址 堆栈上的参数 弹出对应参数(如有)并压入返回值(如有)

另外,大部分指令都是对当前堆栈进行操作(除开几个局部变量表与堆栈的载入/读取指令会涉及局部变量表),所以常常需要你先把参数从局部变量表中加载到堆栈中,执行完指令,再把值存回去

无关紧要的东西(嵌入指令内的参数

java设计者发现有些指令常常跟着特定的参数,比如在java中我们经常这样写:

1
int a=0;

我使用一种汇编来描述这个过程

1
2
int_push_stack 0        ;将int 0入栈
store_local_varible 2 ;将堆栈pop,值传到本地变量表index为2的地方(index的值是我编的)

然后java设计者就觉得这个数字0太常用了,就专门搞了一个指令int_push_stack_0,用来代表参数是0的情况。同理,这个存入本地变量表index为2也很常用,就搞了一个指令store_local_varible_2

1
2
int_push_stack_0        
store_local_varible_2

这样做有什么好处呢?直接节省了71%的空间!(从7字节到2字节)

补充说明

有的地方会说这是嵌入了参数的指令,但实际上这些字节码几乎是完全不同的,不能把他看成复杂指令集的东西。从字节码的视角来看,这些都是不同的指令;从操作逻辑来看,可以将其看作一个指令,嵌入了不同参数。

助记符/伪码

详细的内容请参阅这些文章

函数调用

之所以要先讲函数传参方式,是因为java字节码的许多指令也都是以这种方式工作的(类似于调用函数)。

均采用堆栈的方式传参。

由于jvm知道目标函数需要什么,返回什么(有函数标记)。

  1. 自动从操作数堆栈上(从栈顶开始)弹出第一个、第二个…参数。(当然你得先把这些相应的参数放在操作数堆栈上)
  2. 传入目标函数(的局部变量表)
  3. 目标函数执行
  4. 目标函数返回栈顶元素(目标函数的操作数堆栈),传到调用者的堆栈上。(如果无返回值,那么就没有这一步)

示意图如下,表示调用者的操作数堆栈

调用者的操作数堆栈(调用前)
12(对应参数a)
11(对应参数b)

目标函数

1
2
3
static int getMax(int a,int b){
return a>b?a:b;
}
调用者的操作数堆栈(调用后)
12

非静态函数调用

这里可以提一嘴非静态函数的调用,在Python中,一个类的非静态函数我们通常定义如下:

1
2
def not_static_function(self,*args,**kwargs):
pass

其实在java中本质也是一样的,调用非静态方法时会传入this参数(并且也是第一个参数)

算数

就像函数调用一样,调用Java的算数指令时会从堆栈上弹出相应操作数(一个或两个),然后将计算结果压入堆栈。

java在字节码级别支持以下运算:
加法指令 减法指令 乘法指令 除法指令 求余指令 自增指令

分支结构/跳转

虽然jvm没有标志位这一设计,但仍能够进行条件跳转,在java字节码级别有两个方法。

0.无条件跳转

这个没啥好说的,这个助记符叫goto,挺好记的。

1.先比较再跳转(形如8086汇编中的CMP & Jxx)

java字节码有一系列很神奇的指令,这玩意传参类似于函数调用,能够弹出堆栈上的栈顶两个元素进行比较,然后将比较结果(1、0、-1)压入堆栈。

然后再调用各种条件跳转指令,即可跳转到指定位置。

2.比较并跳转

直接比较栈顶的两个元素,如果符合条件就跳转

跳转接的参数直接就是偏移量(以当前偏移量为0)。

比如goto 3,表示跳转到3个字节后。因此,在java字节码里面,程序逻辑最多只能在这个函数内部跳来跳去。

异常处理

其实java的异常处理核心并不是放在可执行字节码里面的,而是对函数的一个标注,放在函数标记里面的。

1
2
3
4
5
6
7
8
void f(){
try{
//do sth
}catch(ArithmeticException e){
//do sth
}
//do sth
}
From To Target Type
0 4 7 Class java/lang/ArithmeticException

大致如上,记函数起始偏移量为0,表示在0~4的字节码(对应try部分)执行过程中若发生了java/lang/ArithmeticException异常,那么跳转到第7个字节码的位置(对应catch部分)继续执行,并且将该异常压入栈,然后放在局部变量表里(对应e)。

由于在字节码中代码也是按照顺序执行的,因此,编译器会自动在try代码块的末尾加上goto指令,跳过catch代码块。

面向对象

与java语法类似,通过new指令,可以创建一个对象的实例,并将其引用压栈。

为何要了解java字节码

其实java runtime已经几乎完成了所有背后的工作,如果你不用接触底层的原理,emm,也没有多大必要学java字节码。

我是觉得好玩才学的

Reference