JVM基础 -- 字节码
操作数栈
- JVM是基于栈的计算模型
- 在解析过程中,每当为Java方法分配栈帧时
- 执行每条执行之前,JVM要求该指令的操作数已被压入操作数栈中
- 在执行指令时,JVM会将该指令所需要的操作数弹出,并将该指令的结果重新压入栈中
iadd
- 执行iadd之前,栈顶的元素为int值1和int值2
- 执行iadd指令会将弹出这两个int,并将求得的和int值3压入栈中
- iadd只消耗栈顶的两个元素,iadd并不关心更远的元素,也不会对它们进行修改
dup + pop
- dup和pop只能处理非long和非double类型的值
- long类型和double类型需要占据两个栈单元,对应使用dup2和pop2
dup
- dup:复制栈顶元素
- dup指令常用于复制new指令生成的未经初始化的引用
1 | public void dup() { |
- 执行new指令时,JVM将指向一块已分配的但未初始化的内存引用压入操作数栈
- invokespecial指令将要以这个引用为调用者,调用其构造器
- 该指令将消耗操作数栈上的元素,作为它的调用者和参数
- 因此,在这之前利用dup指令复制一份new指令的结果,并用来调用构造器
pop
- pop:舍弃栈顶元素
- pop指令常用于舍弃调用指令的返回结果
1 | public static boolean judge() { |
- invokestatic指令依然会将返回值压入pop方法的操作数栈
- 因此JVM需要执行额外的pop指令,将返回值舍弃
加载常量
- iconst指令加载-1和5之间的int值
- bipush(sipush)指令加载一个字节(两个字节)所能代表的int值
- ldc指令加载常量池中的常量值
类型 | 常数指令 | 范围 |
---|---|---|
int/short/char/byte/boolean | iconst | [-1,5] |
bipush | [-128,127] | |
sipush | [-32768,32767] | |
ldc | any int value | |
long | lconst | 0,1 |
ldc | any long value | |
float | fconst | 0,1,2 |
ldc | any float value | |
double | dconst | 0,1 |
ldc | any double value | |
reference | aconst | null |
ldc | String literal,Class literal |
异常
- 正常情况下,操作数栈的压入弹出都是一条条指令完成的
- 在抛出异常时,JVM会清空操作数栈上到所有内容,而后将异常实例的引用压入操作数栈
局部变量表
- Java方法栈帧的组成:操作数栈+局部变量表
- 字节码程序将计算的结果缓存在局部变量表
- 局部变量表类似于一个数组,依次存放
- this指针(针对实例方法)
- 所传入的参数
- 字节码中的局部变量
- 与操作数栈一样,long类型和double类型将占据两个存储单元
1 | public void locals(long l, float f) { |
- locals是一个实例方法,局部变量表的第0个单元存放this指针
- 第一个参数为long类型,占用局部变量表的第1、2个单元
- 第二个参数为int类型,占用局部变量表的第3个单元
- 方法体内的两个代码块中,分别定义了局部变量i和s,两者的生命周期没有重合
- Java编译器将它们编排至同一单元,即局部变量表的第4个单元
istore 4
和astore 4
- Java编译器在编译时就已经能确定操作数栈、局部变量表的大小以及参数个数
stack=1, locals=5, args_size=3
加载 + 存储
- 存储在局部变量表中的值,通常需要加载至操作数栈中,才能进行计算
- 得到的计算结果后再存储至局部变量表中
- 局部变量表的加载和存储指令都需要指明所加载单元的下标
- 唯一直接作用于局部变量表的指令:iinc M N
- 将局部变量表的第M个单元中的int值增加N
- 常用于for循环中的自增量的更新
类型 | 加载指令 | 存储指令 |
---|---|---|
int/short/char/byte/boolean | iload | istore |
long | lload | lstore |
float | fload | fstore |
double | dload | dstore |
reference | aload | astore |
1 | public void innc() { |
其他字节码
Java相关
- new:后跟目标类,生成该类未初始化的对象引用
- instanceof:后跟目标类,判断栈顶元素是否为目标类(接口)的实例,是则压入1,否则压入0
- checkcast:后跟目标类,判断栈顶元素是否为目标类(接口)的实例,如果不是则抛出异常
- athrow:将栈顶异常抛出
- monitorenter:为栈顶对象加锁
- monitorexit:为栈顶对象解锁
- getstatic/putstatic/getfield/putfield
- 附带用于定位目标字段的信息,但消耗的操作数栈元素个各不相同
- 如下图,将值v存储至对象obj的目标字段中
- invokestatic/invokespecial/invokevirtual/invokeinterface
- 这四个方法调用指令所消耗的操作数栈元素是根据调用类型以及目标方法描述符来确定的
- 在进行方法调用之前,需要依次压入调用者(invokestatic不需要)以及各个参数
putfiled
1 | public class PutField { |
invokevirtual
1 | public int neg(int i) { |
foo(2)的执行过程
数组相关
- newarray:新建基本类型的数组
- anewarray:新建引用类型的数组
- multianewarray:生成多维数组
- arraylegth:求数组长度
- 数组的加载和存储指令(区分类型)
1 | public void array() { |
数组类型 | 加载指令 | 存储指令 |
---|---|---|
byte/boolean | baload | bastore |
char | caload | castore |
short | saload | sastore |
int | iaload | iastore |
long | laload | lastore |
float | faload | fastore |
double | daload | dastore |
reference | aaload | aastore |
控制流
- goto:无条件跳转
- tableswitch/lookupswitch:密集case/稀疏case
- 返回指令
- 除了返回指令外,其他控制流指令均附带一个或多个字节码偏移量,代表需要跳转到的位置
返回类型 | 返回指令 |
---|---|
void | return |
int/short/char/byte/boolean | ireturn |
long | lreturn |
float | freturn |
double | dreturn |
reference | areturn |
参考资料
All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.