入门jvm字节码
java是很简单的,但是这背后支撑着java的是什么?
引言
jvm是一台虚拟机,能够执行java字节码,也就是java的汇编。
本篇文章将避免涉及具体的指令,从了解的程度来描述java字节码设计。
虚拟机结构
重点内容(为了方便理解,以下内容做了简化)
对于每一个class
,有一个常量池、静态变量表
对于每一个实例对象,有一个成员变量表
对于每一个function
,有一个局部变量表(类似list)、一个操作数堆栈
与8086汇编的区别
-
弃用了标志位、寄存器
-
java字节码属于精简指令集,8086汇编属于复杂指令集
-
引入了局部变量表、常量池
-
有些不是可执行代码片段(包括函数标记、类标记)
- 因此,java字节码保留了类、函数的概念,可以像高级语言一样调用函数
-
更多抽象类型的支持,的基本类型:
- 基本类型
- int
- byte
- short
- long
- float
- double
- char
- boolean
- 引用类型
- 类实例
- 数组
- 接口
- 基本类型
-
java字节码更高级,难以使用逻辑电路实现
- 异常处理
- 字节码级别的面向对象
- 字节码级别的多线程锁
- 自动管理的函数调用(传参、返回)
- 还隐含了runtime与jvm之间的各种操作,例如
- 自动内存管理
- 动态加载、反射
优势与代价
作为高级汇编,优势有
- 是一个抽象层,模糊了不同架构CPU之间的差异,易于跨平台
- 在汇编层面实现了面向对象支持,提高了简便性
但同时也有缺陷
- 增加了抽象、面向对象、runtime的代价
- 简便性的同时也意味着失去了灵活性,使得java难以在编译的时候进行优化(字节码层面的优化),各项优化高度依赖运行时
字节码设计
java的指令长度都是一个字节,因此叫做字节码。(不算额外参数)
同样,java字节码也有显式参数与隐含参数。
指令 | 显式参数 | 隐含参数(堆栈) | 影响 |
---|---|---|---|
加法运算 | 无 | 堆栈上栈顶两个参数 | 弹出栈`1顶两个元素并将运算结果压栈 |
无条件跳转 | 目标位置偏移量 | 无 | 程序逻辑跳转 |
函数调用 | 函数地址 | 堆栈上的参数 | 弹出对应参数(如有)并压入返回值(如有) |
另外,大部分指令都是对当前堆栈进行操作(除开几个局部变量表与堆栈的载入/读取指令会涉及局部变量表),所以常常需要你先把参数从局部变量表中加载到堆栈中,执行完指令,再把值存回去
无关紧要的东西(嵌入指令内的参数注)
java设计者发现有些指令常常跟着特定的参数,比如在java中我们经常这样写:
1 | int a=0; |
我使用一种伪汇编来描述这个过程
1 | int_push_stack 0 ;将int 0入栈 |
然后java设计者就觉得这个数字0太常用了,就专门搞了一个指令int_push_stack_0
,用来代表参数是0的情况。同理,这个存入本地变量表index为2也很常用,就搞了一个指令store_local_varible_2
1 | int_push_stack_0 |
这样做有什么好处呢?直接节省了71%的空间!(从7字节到2字节)
补充说明
有的地方会说这是嵌入了参数的指令,但实际上这些字节码几乎是完全不同的,不能把他看成复杂指令集的东西。从字节码的视角来看,这些都是不同的指令;从操作逻辑来看,可以将其看作一个指令,嵌入了不同参数。
助记符/伪码
详细的内容请参阅这些文章
函数调用
之所以要先讲函数传参方式,是因为java字节码的许多指令也都是以这种方式工作的(类似于调用函数)。
均采用堆栈的方式传参。
由于jvm知道目标函数需要什么,返回什么(有函数标记)。
- 自动从操作数堆栈上(从栈顶开始)弹出第一个、第二个…参数。(当然你得先把这些相应的参数放在操作数堆栈上)
- 传入目标函数(的局部变量表)
- 目标函数执行
- 目标函数返回栈顶元素(目标函数的操作数堆栈),传到调用者的堆栈上。(如果无返回值,那么就没有这一步)
示意图如下,表示调用者的操作数堆栈
调用者的操作数堆栈(调用前) |
---|
12(对应参数a) |
11(对应参数b) |
目标函数
1 | static int getMax(int a,int b){ |
调用者的操作数堆栈(调用后) |
---|
12 |
非静态函数调用
这里可以提一嘴非静态函数的调用,在Python中,一个类的非静态函数我们通常定义如下:
1 | def not_static_function(self,*args,**kwargs): |
其实在java中本质也是一样的,调用非静态方法时会传入this参数(并且也是第一个参数)
算数
就像函数调用一样,调用Java的算数指令时会从堆栈上弹出相应操作数(一个或两个),然后将计算结果压入堆栈。
java在字节码级别支持以下运算:
加法指令 减法指令 乘法指令 除法指令 求余指令 自增指令
分支结构/跳转
虽然jvm没有标志位这一设计,但仍能够进行条件跳转,在java字节码级别有两个方法。
0.无条件跳转
这个没啥好说的,这个助记符叫goto
,挺好记的。
1.先比较再跳转(形如8086汇编中的CMP & Jxx)
java字节码有一系列很神奇的指令,这玩意传参类似于函数调用,能够弹出堆栈上的栈顶两个元素进行比较,然后将比较结果(1、0、-1)压入堆栈。
然后再调用各种条件跳转指令,即可跳转到指定位置。
2.比较并跳转
直接比较栈顶的两个元素,如果符合条件就跳转
注
跳转接的参数直接就是偏移量(以当前偏移量为0)。
比如goto 3
,表示跳转到3个字节后。因此,在java字节码里面,程序逻辑最多只能在这个函数内部跳来跳去。
异常处理
其实java的异常处理核心并不是放在可执行字节码里面的,而是对函数的一个标注,放在函数标记里面的。
1 | void f(){ |
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字节码。
我是觉得好玩才学的