jvm字节码指令理解? 理解JVM的指令的一个基础是理解JVM的栈内存,因此在开始之前最好先参阅一下《Java 栈内
jvm字节码指令理解
? 理解JVM的指令的一个基础是理解JVM的栈内存,因此在开始之前最好先参阅一下《Java 栈内存介绍》。本篇将结合例子对JVM的主要指令进行描述。
???? 在开始之前,我们先了解一下如下的 “常识”:
字长是根据JVM不同而定的,一般(并非一定)在32位机上是4个字节,64位机上是8个字节(使用8个字节很可能会潜在地存在内存浪费的情况),JVM规范上要求1个字必须至少能容纳integer型的值(4字节),2个字必须至少能容纳long型的值(8个字节)。JVM有不少定义会以字为单位,譬如reference(引用)、本地变量和栈JVM操作由操作码和操作数组成,操作码是1字节的,因此最多只有256个操作码,操作数从0-n个字节不等(0表示没有操作数,一般是指令参数通过操作栈来获取,n不定,譬如像TABLESWITCH和LOOKUPSWITCH指令)每个操作如果需要从操作栈中读参数,则总是将这些参数出栈,如果操作有结果结果,总是会将结果入栈,后面可能会重复提到一点,如果没有提到,这是一个参考准则本地变量是以字为单位(如上,32位机一般是4个字节,也有一些64位的JVM字长是8个字节)为单位的,即使值是byte或short,对于long、double型的数据,在本地变量区中会占用2个位置(slot)操作栈是以字为单位(如上,32位机一般是4个字节) ,即使值是byte或short,而对于long、double型的数据,在操作栈中会占用2个位置(slot)在如下的描述中,指令=操作码+操作数
????? 1.主要运算指令
????? 1)常量操作指令
BIPUSH? x :将x(单字节,-128—127)入操作栈,注意,如上常识说明,入栈的值在操作栈中会占用1个字长,BIPUSH就是将B(Byte,1个字节)转换成I(Integer ,4个字节),然后PUSH到操作栈SIPUSH? x :将x(双字节,?32768— 32767)入操作栈,注意,如上常识说明,在操作栈中会占用1个字长,BIPUSH就是将S(Short,2个字节)转换成I(Integer ,4个字节),然后PUSH到操作栈ICONST_n(n是-1—5)/ LCONST_n(n是0-1):对于x比较小的情况的BIPUSH的高效版本,没有操作数,指令是单字节的。另外,LCONST_n在操作栈中会占用2个位置(long型)FCONST_n(n是0-2)/DCONST_n(n是0-1):与上面类似,F表示float、D表示double,float会占用1个操作栈位置(slot,1个字长),double会占用2个操作栈位置ACONST_NULL:将null指针入栈LDC cst:将常量池偏移量为cst的值入栈,譬如LDC #12,在操作栈中会占用1个字长LDC2_W cst:将常量池偏移量为cst的值入栈,在栈中会占用2个位置,譬如LDC _W #13。
????? 可能会很奇怪地发现,JVM使用了前缀I、F、A这种实际效果差不多的不同指令,实际上其目的是为了JVM在进行字节码验证的时候更好地检查类型。
????? 2)Local变量操作指令
ILOAD/LLOAD/FLOAD/DLOAD/ALOAD x:将第x个本地变量入栈,其中I是integer、L是long、F是float、D是double、A是地址(指对象地址),需要注意的是,本地变量是以字长为单位的,因此像LLOAD x/DLOAD x,实际上会将本地变量第x+1和x+2两个位置的值入栈。ILOAD_n/LLOAD_n/FLOAD_n/DLOAD_n/ALOAD_n(n是0-3):单字节指令,如上操作的高效版本,将第n个本地变量入栈ISTORE/LSTORE/FSTORE/DSTORE/ASTORE x:从栈顶pop出值,存到第x个本地变量中,与上类似,本地变量是以4个字节为单位,因此像LSTORE x/DSTORE x,实际上会pop出两个值,分别存储到地变量第x+1和x+2两个位置上ISTORE_n/LSTORE_n/FSTORE_n/DSTORE_n/ASTORE_n(n是0-3):单字节指令,如上操作的高效版本,从栈顶pop出值存储到第n个本地变量中IINC x n:将第x个本地变量递增n
????? 与上类似的,JVM使用了前缀I、F、A这种实际效果差不多的不同指令,实际上其目的是为了JVM在进行字节码验证的时候更好地检查类型。
????? 3)栈操作指令
POP /POP2:从栈中弹出1/2个位置内容DUP/DUP2/DUP_X1/DUP_X2/DUP2_X1/DUP2_X2:栈复制指令,DUP/DUP2表示复制栈顶1/2个字的内容,并入到栈顶,DUP_X1表示复制堆顶偏移1个位置的1个字的内容,并入到顶栈(譬如堆顶是word1, word2,word1是栈顶,执行后变成word2,word1,word2), DUP_X2表示复制堆顶偏移2个位置的1个字的内容,并入到顶栈(譬如堆顶是word1, word2,word3,word1是栈顶,执行后变成word3,word1,word2,word3),DUP2_X1表示复制堆顶偏移1个位置的2个字的内容,并入到顶栈(譬如堆顶是word1, word2,word3,word1是栈顶,执行后变成word2,word3,word1,word2,word3),DUP2_X2表示复制堆顶偏移2个位置的2个字的内容,并入到顶栈(譬如堆顶是word1, word2,word3,word4,word1是栈顶,执行后变成word3,word4,word1,word2,word3,word4)SWAP:交换栈顶2个字的内容,譬如堆顶是word1, word2,交换后是word2,word1
????? 4)运算指令
????? 运算指令非常简单,单指令,没有操作数,操作的参数放在栈中,运算的时候都是从栈顶弹出2个参数(对于D开头的指令,每个参数是2个字,则会弹出2*2个字的信息),运算完后把结果入栈顶(根据类型不同,结果会占用1-2个字),更详细可以看看《Java 栈内存介绍》中的范例
IADD/LADD/FADD/DADD:? a + bISUB/LSUB/FSUB/DSUB:? a - bIMUL/LMUL/FMUL/DMUL: a * bIDIV/LDIV/FDIV/DDIV: a / bIREM/LREM/FREM/DREM:? a % bINEG/LNEG/FNEG/DNEG: -aISHL/LSHL: a < < nISHR/LSHR: a > > nIUSHR/LUSHR: a > > > nIAND/LAND :a & bIOR/LOR: a | bIXOR/LXOR:? a ^ bLCMP:a == b ? 0 : (a < b ? -1 : 1)FCMPL, FCMPG: a == b ? 0 : (a < b ? -1 : 1)DCMPL, DCMPG ... , a , b ... , a == b ? 0 : (a < b ? -1 : 1)
????? 5)类型转换指令
????? 类型转换指令,非常简单,看后面例子就能明白
I2B/I2C/I2S/I2L/I2F/I2DL2I/F2I/L2F/L2DD2I/D2L/D2FF2D/F2LCHECKCAST class:检查栈顶的元素是否为指定的类型,class是常量池的偏移量
????? 6)范例:
???? 下面的例子演示了如上的大部分类型的字节码的功能
public int test(int i) { if (i > 100) { return 200; } //case语句比较连续,会翻译成tableswitch switch (i) { case 1: return 1; case 2: return 2; } //case语句不连续,会翻译成lookupswitch switch (i) { case 1: return 1; case 100: return 100; } return 0; }
public int test(int);
Code:
Stack=2, Locals=2, Args_size=2
0: iload_1 //将第2个参数入栈,即i
1: bipush 100 //将100入栈
3: if_icmple 10 //如果i<=100,则跳转到第10条语句
6: sipush 200
9: ireturn //返回200
10: iload_1 //将第2个参数入栈,即i
11: tableswitch{ //1 to 2
1: 32;
2: 34;
default: 36 }
//case语句比较连续,使用tableswitch
32: iconst_1
33: ireturn
34: iconst_2
35: ireturn
36: iload_1
37: lookupswitch{ //2
1: 64;
100: 66;
default: 69 }
//case语句不连续,使用lookupswitch
64: iconst_1
65: ireturn
66: bipush 100
68: ireturn
69: iconst_0
70: ireturn
??? 4)返回指令
??? 返回指令没什么好说,上面几个例子都会涉及到
IRETURN/LRETURN/FRETURN/DRETURN/ARETURN:将栈顶值返回RETURN:返回ATHROW:抛出栈顶对象
??? 5)指令quick版本
???? 在上面指令中,有很多指令是需要涉及到引用解析的,譬如NEW、LDC、NEWARRAY等,虽然整个过程只需要一次解析,但每次都需要去判断需不需要解析。为了避免这种无谓的判断,Sun JVM在实现上对这些指令有一个快速的指令版本,在运行的时候,如果已经引用已经解析过了,则把相应的指令替换快速的指令版本,快速引用会直接使用解析后的结果。
LDC_QUICK/LDC_W_QUICK/LDC2_W_QUICK GETFIELD_QUICK/PUTFIELD_QUICK/GETFIELD2_QUICK/PUTFIELD2_QUICK/GETSTATIC_QUICK/PUTSTATIC_QUICK/GETSTATIC2_QUICK/PUTSTATIC2_QUICK/GETFIELD_QUICK_W/PUTFIELD_QUICK_W INVOKEVIRTUAL_QUICK/INVOKENONVIRTUAL_QUICK/INVOKESUPER_QUICK/INVOKESTATIC_QUICK/INVOKEINTERFACE_QUICK/INVOKEVIRTUALOBJECT_QUICK/INVOKEVIRTUAL_QUICK_W NEW_QUICK/ANEWARRAY_QUICK/MULTIANEWARRAY_QUICK CHECKCAST_QUICK/INSTANCEOF_QUICK
???? 2.其他相关
???? 1)Exception表
???? 异常的支持是JVM级别的,对于每个方法,class文件中会携带该方法的异常和处理handler信息,如下,第1行表示代码在执行0-8行的时候,如果有java.lang.Exception抛出,则跳转到11条指令,第2行表示代码在执行0-20行的时候,如果有异常抛出(不管什么类型的异常),则跳转到第29条指令
Exceptions:
[0-8): 11 - java.lang.Exception
[0-20): 29
??? 关于异常更多信息,可参见《[字节码系列]ObjectWeb ASM构建Method Monitor》
??? 2)LineNumber表
??? LineNumber存储了字节码与源码的行数的对应关系,其存在是为了支持Exception或者调试的时候,可以正确地获得代码所在的行数,如下
LineNumberTable:
line 8: 0
line 9: 8
line 11: 12
line 9: 17
line 13: 25
??? 这个表不是必要的,有些编译器或者选项会选择不编译到class文件中
??? 3)LocalVariable表
??? LocalVariable存储了本地变量在源码中的实际名字,如下
LocalVariableTable:
Start Length Slot Name Signature
0 25 0 this Ltest/Test3;
5 20 1 iarray [I
13 12 2 length I
17 8 3 result I
24 1 4 objs [Ljava/lang/Object;
??? 这个表不是必要的,有些编译器或者选项会选择不编译到class文件中