Java虚拟机既然是一台虚拟的计算机,那必有一套完善指令集,指令由一个字节长度,代表某种特定操作含义的操作码构成。这里在最初接触计算机的时候,我是有一些疑问的。例如为什么计算机都有它的指令集,这些指令是怎么被CPU执行的。由于我当初学的是电子科学,包含模电数电,单片机等内容,其中单片机就是一个小型的片上系统,随着学习的深入这些疑问自然就迎刃而解了。我谈一下自己的理解,也顺便做一下记录。
对于CPU而言它只认识0和1,对应的是数字电路中的高低电平,实现原理是把MOS管当做开关使用,MOS管是一种半导体晶体管,主要成分硅(这也是美国硅谷这个名字的由来吧)。通过MOS管可以实现各种寄存器,累加器,计数器,与非门,或非门等逻辑门电路。每一条指令都是特定的门电路组成的,门电路是CPU的基本工作单元,数以亿计的门电路才造就了CPU的强大运算能力,这些门电路输入是一位或多位二进制数,输出也是一位或多位二进制数。在最早的计算机上,程序员需要将0101这种二进制代码给CPU计算,这里的二进制代码会控制CPU进行加减乘除或者逻辑运算,对应于在MOS管上的状态就是开和关,这种二进制代码也叫机器语言,是计算机能直接读懂的语言。但是这对于程序员来说简直是噩梦,一个小小的计算功能,用机器语言来编写需要写一大串0和1。最关键的是这些0和1代码代表什么意思也非常难以记忆,开发成本很大。于是科学家们用了一种高级的语言来代替它 ,然后汇编语言诞生了,汇编语言更接近人类自然语言的习惯。然后再做一个编译器工具帮我们把汇编语言翻译成机器语言就行了。
那么问题来了,编译器按照什么规则来翻译汇编代码呢?答案就是指令集,指令集是由设计CPU的生产厂商给出。CPU指令集是描述CPU能实现什么功能的一个集合, 就是描述”CPU能使用哪些机器码”的集合。指令集的作用, 就是告诉程序员或者说编译器, 汇编一定要有什么格式. 支持什么指令, 指令带什么限制条件, 用什么操作数, 用什么地址, 都是指令集规范的内容, 要是写错了, 就无法翻译成机器码。不同的CPU生产厂商产生了不同的指令集,Intel阵营的8086指令集,ARM阵营的RISC指令集。不同阵营,其对应的汇编语言也是不一样的。
1 . JVM的指令集
前面说了那么多,都是为这里做铺垫。作为一个Java程序员怎么能不认识虚拟机的指令集,指令集不仅可以帮我们看懂字节码文件,同时也有助于理解JVM的运行原理。我总结了一下JVM的指令集,主要分为以下几种类型的指令。
(1) . 空指令
字节码 | 指令助记符 | 指令含义 |
---|---|---|
0x00 | nop | 什么都不做 |
nop指令相信接触过汇编语言的都知道,它什么都不做但是占用一个指令周期的时间。指令周期的时间不是固定的,不同的指令其指令周期时间不同,一个指令周期由如干个机器周期组成,每个机器周期能完成一项基本操作,比如取指令、存储器读、存储器写等。而机器周期又由如干个时钟周期组成,在一个时钟周期内,CPU只能完成一个最基本的动作,一个时钟周期的时间计算也比较简单,例如CPU的频率是2.7GHz,这里要注意换算单位,频率单位1GHz =1000MHz,1MHz就是1百万赫兹,所以频率为2.7GHz的CPU。一个时钟周期为 1/2700000000 秒 。 显然,时钟频率越高,CPU的工作速度就越快,这也是我们购买CPU时的一个重要指标。
(2) . 基本数据类型的取值和存储指令
字节码 | 指令助记符 | 指令含义 |
---|---|---|
0x01 | aconst_null | 将null推送至栈顶 |
0x02 | iconst_m1 | 将int型-1推送至栈顶 |
0x0i(i=3,4,5,6,7,8) | iconst_j(j=0,1,2,3,4,5) | 将int型 j 推送至栈顶 |
0x0i(i=9,a) | lconst_j(j=0,1) | 将long型 j 推送至栈顶 |
0x0i(i=b,c,d) | fconst_j(j=0,1,2) | 将float型 j 推送至栈顶 |
0x0i(ie,f) | dconst_j(j=0,1) | 将double型 j 推送至栈顶 |
0x10 | bipush | 将单字节常量(-127~128)推送至栈顶 |
0x11 | sipush | 将短整型常量(-32768~32767)推至栈顶 |
0x12 | ldc | 将int,float,String常量从常量池中推至栈顶 |
0x13 | ldc_w | 将int,float,String常量从常量池中推至栈顶(宽索引) |
0x14 | ldc2_w | 将long,double常量从常量池中推至栈顶(宽索引) |
0x15 | iload | 将指定的int型局部变量推至栈顶 |
0x16 | lload | 将指定的long型局部变量推至栈顶 |
0x17 | fload | 将指定的float型局部变量推至栈顶 |
0x18 | dload | 将指定的double型局部变量推至栈顶 |
0x1i(i=a,b,c,d) | iload_j(j=0,1,2,3) | 将第 j 个int型的局部变量推至栈顶 |
0x1i(i=e,f) | lload_j(j=0,1) | 将第 j 个long 型局部变量推至栈顶 |
0x2i(i=0,1) | lload_j(j=3,4) | 将第 j 个long 型局部变量推至栈顶 |
0x2i(i=2,3,4,5) | fload_j(j=0,1,2,3) | 将第 j 个float 型局部变量推至栈顶 |
0x2i(i=6,7,8,9) | dload_j(j=0,1,2,3) | 将第 j 个double 型局部变量推至栈顶 |
0x36 | istore | 将栈顶 int 型数值存入指定的本地变量 |
0x37 | lstore | 将栈顶 long 型数值存入指定的本地变量 |
0x39 | dstore | 将栈顶 dloat 型数值存入指定的本地变量 |
0x38 | fstore | 将栈顶 float 型数值存入指定的本地变量 |
0x3i(i=b,c,d,e) | istore_j(j=0,1,2,3) | 将栈顶 int 型数值存入第 j 个本地变量 |
0x3f | lstore_0 | 将栈顶 long 型数值存入第 0 个本地变量 |
0x4i(i=0,1,2) | lstore_j(j=1,2,3) | 将栈顶 long 型数值存入第 j 个本地变量 |
0x4i(i=3,4,5,6) | fstore_j(j=0,1,2,3) | 将栈顶 float 型数值存入第 j 个本地变量 |
0x4i(i=7,8,9,a) | dstore_j(j=0,1,2,3) | 将栈顶 double 型数值存入第 j 个本地变量 |
0xc4 | wide | 扩展本地变量的宽度 |
总的概括来说就是
将一个本地变量加载到操作数栈的指令有
iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>
将一个数值从操作数栈存储到本地变量表的指令由
istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>
将一个常量加载到操作数栈的指令有
bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>
扩充局部变量表的访问索引的指令
wide
这里要注意的是随着数值取值范围的改变,指令会随之改变。例如当int取值-1~5采用iconst指令,取值-128~127采用bipush指令,取值-32768~32767采用sipush指令,取值-2147483648~2147483647采用 ldc 指令
(3) . 引用数据类型的取值与存储操作
字节码 | 指令助记符 | 指令含义 |
---|---|---|
0x19 | aload | 将指定的引用数据类型本地变量推至栈顶 |
0x2i(i=a,b,c,d) | aload_j(j=0,1,2,3) | 将第 j 个引用类型本地变量推至栈顶 |
0x2e | iaload | 将int型数组指定的索引的值推至栈顶 |
0x2f | laload | 将long型数组指定的索引的值推至栈顶 |
0x30 | faload | 将float型数组指定的索引的值推至栈顶 |
0x31 | daload | 将double型数组指定的索引的值推至栈顶 |
0x32 | aaload | 将引用数据类型数组指定的索引的值推至栈顶 |
0x33 | baload | 将boolean或byte型数组指定的索引的值推至栈顶 |
0x34 | caload | 将char型数组指定的索引的值推至栈顶 |
0x35 | saload | 将short型数组指定的索引的值推至栈顶 |
0x3a | astore | 将栈顶的引用型数值存入指定的本地变量 |
0x4i(i=b,c,d,e) | astore_j(j=0,1,2,3) | 将栈顶引用型数值存入第 j 个本地变量 |
0x4f | iastore | 将栈顶 int 型数值存入指定数组的指定索引位置 |
0x50 | lastore | 将栈顶 long 型数值存入指定数组的指定索引位置 |
0x51 | fastore | 将栈顶 float 型数值存入指定数组的指定索引位置 |
0x52 | dastore | 将栈顶 double 型数值存入指定数组的指定索引位置 |
0x53 | aastore | 将栈顶引用型数值存入指定数组的指定索引位置 |
0x54 | bastore | 将栈顶boolean或byte型数值存入指定数组的指定索引位置 |
0x55 | castore | 将栈顶 char 型数值存入指定数组的指定索引位置 |
0x56 | sastore | 将栈顶 short 型数值存入指定数组的指定索引位置 |
0x57 | pop | 将栈顶弹出(数值不能是 long 或 double类型) |
0xbb | new | 创建一个对象,并将其压入栈顶 |
0xbc | newarray | 创建一个基本数据类型(如int,char)的数组,并将其引用的值压入栈顶 |
0xbd | anewarray | 创建一个引用型数组(如类,接口,数组),并将其引用的值压入栈顶 |
0xc5 | multianewarray | 创建指定类型和指定维度的多维数组,并将其引用值压入栈顶 |
0xbe | arraylength | 获取数组长度值并压入栈顶 |
0xc0 | checkcast | 校验类型转换,校验未通过抛出ClassCastException |
0xc1 | instanceof | 校验对象是否是指定的类的实例,如果是,则将1压入栈顶,否则将0压入栈顶 |
0xb2 | getstatic | 获取指定类的静态域,并将其值压人栈顶 |
0xb3 | putstatic | 为指定的类的静态域赋值 |
0xb4 | getfield | 获取指定类的实例域,并将其压入栈顶 |
0xb5 | putfield | 为指定类的实例域赋值 |
这里总结概括一下,创建实例的指令
new
创建数组对象以及获取数组长度的指令
newarray、anewarray、multianewarray、arraylength
访问类字段(static字段就是类变量)和实例字段(非static字段就是实例变量)指令
getfield、putfield、getstatic、putstatic
把一个数组中指定索引处的值加载到操作数栈的指令
baload、caload、saload、iaload、laload、faload、daload、aaload
将一个操作数栈的值储存到数组中指定索引处的指令
bastore、castore、sastore、iastore、fastore、dastore、aastore
检查类实例类型的指令
instanceof、checkcast
虽然实例和数组都是对象,但是虚拟机对实例和数组实例的创建与操作使用了不同的字节码指令
(4) . 操作数栈管理指令
字节码 | 指令助记符 | 指令含义 |
---|---|---|
0x57 | pop | 将栈顶弹出(数值不能是 long 或 double类型) |
0x58 | pop2 | 将栈顶一个(对于 long 或double类型)或两个(非 long 或double类型)数值弹出 |
0x59 | dup | 复制栈顶数值并将复制值压入栈顶 |
0x5a | dup_x1 | 复制栈顶数值并将两个复制值压入栈顶 |
0x5f | swap | 将栈最顶端的两数值互换(数值不能是 long 或 double类型) |
0x5b | dup_x2 | 复制栈顶数值并将三个(或两个)复制值压入栈顶 |
0x5c | dup2 | 复制栈顶一个(对于 long 或double类型)或两个(非 long 或double类型)值并压栈 |
0x5d | dup2_x1 | dup_x1 指令的双倍版本 |
x05e | dup2_x2 | dup_x2 指令的双倍版本 |
(5) . 控制转移指令
字节码 | 指令助记符 | 指令含义 |
---|---|---|
0x99 | ifeq | 当栈顶int型数值等于0时跳转 |
0x9a | ifnq | 当栈顶int型数值不等于0时跳转 |
0x9b | iflt | 当栈顶int型数值小于0时跳转 |
0x9c | ifge | 当栈顶int型数值小于或等于0时跳转 |
0x9d | ifgt | 当栈顶int型数值大于0时跳转 |
0x9e | ifle | 当栈顶int型数值小于或等于0时跳转 |
0x9f | if_icmpeq | 比较栈顶两个int型数值的大小,结果等于0时跳转 |
0xa0 | if_icmpne | 比较栈顶两个int型数值的大小,结果不等于0时跳转 |
0xa1 | if_icmplt | 比较栈顶两个int型数值的大小,结果小于0时跳转 |
0xa2 | if_icmpge | 比较栈顶两个int型数值的大小,结果大于或等于0时跳转 |
0xa3 | if_icmpgt | 比较栈顶两个int型数值的大小,结果大于0时跳转 |
0xa4 | if_icmple | 比较栈顶两个int型数值的大小,结果小于或等于0时跳转 |
x0a5 | if_acmpeq | 比较栈顶两个引用型数值,结果相等时跳转 |
0xa6 | if_acmpne | 比较栈顶两个引用型数值,结果不相等时跳转 |
0xa7 | goto | 无条件跳转 |
0xa8 | jsr | 跳转至指定的16位offset位置,并将jsr的下一条指令地址压入栈顶 |
0xa9 | ret | 返回至本地变量指定的index的指令位置(一般与 jsr 或 jsr_w联合使用) |
0xaa | tableswitch | 用于switch条件跳转,case值连续(可变长度指令) |
0xab | lookupswitch | 用于switch条件跳转,case值不连续(可变长度指令) |
0xc6 | ifnull | 为null时跳转 |
0xc7 | ifnonull | 不为null时跳转 |
oxc8 | goto_w | 无条件跳转(宽索引) |
0xc9 | jsr_w | 跳转至指定32位offset位置,并将jsr_w的下一条指令地址压入栈顶 |
条件分支
ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、 if_icmpgt、if_icmple、if_icmpge、if_acmpeq、if_acmpne
复合条件分支
tableswitch、lookupswitch
无条件分支
goto、goto_w、jsr、jsr_w、ret
Java条件分支比较包括三种类型值的比较,int类型,reference 类型,null 值。而其它boolean 类型、byte 类型、char 类型和 short 类型的条件分支比较操作,都使用 int 类型的比较指令来完成,而对于 long 类型、float 类型和 double 类型的条件分支比较操作,则会先执行相应类型的比较运算指令,运算指令会返回一个整型值到操作数栈中,随后再执行 int 类型的条件分支比较操作来完成整个分支跳转。由于各种类型的比较最终都会转化为 int 类型的比较操作,所以Java 虚拟机提供了非常丰富的 int类型的条件分支指令。另外所有 int 类型的条件分支转移指令进行的都是有符号的比较操作。
(6) . 线程同步
字节码 | 指令助记符 | 指令含义 |
---|---|---|
0xc2 | monitorenter | 获得对象的锁,用于同步方法或同步代码块 |
0xc3 | monitorexit | 释放对象的锁,用于同步方法或同步代码块 |
Java 虚拟机可以支持方法级的同步和方法内一段代码块的同步,这两种同步结构都是使用监视器 (monitor)来支持实现的。
方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池中的方法表结构(method_info Structure)中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有管程,然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获得同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的管程将在异常抛到同步方法之外时自动释放
(7) . 运算指令
字节码 | 助记符 | 指令含义 |
---|---|---|
0x60 | iadd | 将栈顶两个 int 型数值相加并将结果压入栈顶 |
0x61 | ladd | 将栈顶两个 long 型数值相加并将结果压入栈顶 |
0x62 | fadd | 将栈顶两个 float 型数值相加并将结果压入栈顶 |
0x63 | dadd | 将栈顶两个 double 型数值相加并将结果压入栈顶 |
0x64 | isub | 将栈顶两个 int 型数值相减并将结果压入栈顶 |
0x65 | lsub | 将栈顶两个 long 型数值相减并将结果压入栈顶 |
0x66 | fsub | 将栈顶两个 float 型数值相减并将结果压入栈顶 |
0x67 | dsub | 将栈顶两个 double 型数值相减并将结果压入栈顶 |
0x68 | imul | 将栈顶两个 int 型数值相乘并将结果压入栈顶 |
x069 | lmul | 将栈顶两个 long 型数值相乘并将结果压入栈顶 |
0x6a | fmul | 将栈顶两个 float 型数值相乘并将结果压入栈顶 |
0x6b | dmul | 将栈顶两个 double 型数值相乘并将结果压入栈顶 |
0x6c | idiv | 将栈顶两个 int 型数值相除并将结果压入栈顶 |
0x6d | ldiv | 将栈顶两个 long 型数值相除并将结果压入栈顶 |
0x6e | fdiv | 将栈顶两个 float 型数值相除并将结果压入栈顶 |
0x6f | ddiv | 将栈顶两个 double 型数值相除并将结果压入栈顶 |
0x70 | irem | 将栈顶两个int 型数值作取模运算并将结果压入栈顶 |
0x71 | lrem | 将栈顶两个long 型数值作取模运算并将结果压入栈顶 |
0x72 | frem | 将栈顶两个float 型数值作取模运算并将结果压入栈顶 |
0x73 | drem | 将栈顶两个double 型数值作取模运算并将结果压入栈顶 |
0x74 | ineg | 将栈顶两个int 型数值取负并将结果压入栈顶 |
0x75 | lneg | 将栈顶两个long 型数值取负并将结果压入栈顶 |
0x76 | fneg | 将栈顶两个float 型数值取负并将结果压入栈顶 |
0x77 | dneg | 将栈顶两个double 型数值取负并将结果压入栈顶 |
0x78 | ishl | 将 int 型数值取左移指定位数并将结果压入栈顶 |
0x79 | lshl | 将 long 型数值取左移指定位数并将结果压入栈顶 |
0x7a | ishr | 将 int 型数值(带符号)取右移指定位数并将结果压入栈顶 |
0x7b | lshr | 将 long 型数值(带符号)取右移指定位数并将结果压入栈顶 |
0x7c | iushr | 将 int 型数值(无符号)取右移指定位数并将结果压入栈顶 |
0x7d | lushr | 将 long 型数值(无符号)取右移指定位数并将结果压入栈顶 |
0x7e | iand | 将栈顶两 int 型数值作 按位与 并将结果压入栈顶 |
0x7f | land | 将栈顶两 long 型数值作 按位与 并将结果压入栈顶 |
0x80 | ior | 将栈顶两 int 型数值作 按位或 并将结果压入栈顶 |
0x81 | lor | 将栈顶两 long 型数值作 按位或 并将结果压入栈顶 |
0x82 | ixor | 将栈顶两 int 型数值作 按位异或 并将结果压入栈顶 |
0x83 | lxor | 将栈顶两 long 型数值作 按位异或 并将结果压入栈顶 |
0x84 | iinc | 将指定int 型数值增加指定值(如 i++,i–,j += 1等) |
0x94 | lcmp | 比较栈顶两long型数值大小,并将结果(1,0,-1)压入栈顶 |
0x95 | fcmpl | 比较栈顶两float型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将-1压入栈顶 |
0x96 | fcmpg | 比较栈顶两 float 型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将1压入栈顶 |
0x97 | dcmpl | 比较栈顶两double 型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将-1压入栈顶 |
0x98 | dcmpg | 比较栈顶两 double 型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将1压入栈顶 |
概括来说就是下面这几种分类
加法指令:iadd、ladd、fadd、dadd
减法指令:isub、lsub、fsub、dsub
乘法指令:imul、lmul、fmul、dmul
除法指令:idiv、ldiv、fdiv、ddiv
求余指令:irem、lrem、frem、drem
取反指令:ineg、lneg、fneg、dneg
位移指令:ishl、ishr、iushr、lshl、lshr、lushr
按位或指令:ior、lor
按位与指令:iand、land
按位异或指令:ixor、lxor
局部变量自增指令:iinc
比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp
Java 虚拟机没有明确规定整型数据溢出的情况,但是规定了在处理整型数据时,只有除法指令(idiv 和 ldiv)以及求余指令(irem 和 lrem)出现除数为零时会导致虚拟机抛出异常,如果发生了这种情况,虚拟机将会抛出 ArithmeitcException 异常。 例如下面的代码
public class Test {
public static void main(String[] args){
int num = Integer.MAX_VALUE ; //获得int的最大值
System.out.println("num = "+num);
System.out.println("num+1 = "+(num+1));
System.out.println("num+2 = "+(num+2));
}
}
得到的结果
num = 2147483647
num+1 = -2147483648
num+2 = -2147483647
这段代码获取了int型数据的最大值,并在最大值的基础上进行了加法运算,程序不会报错,但结果和预期却截然相反,其原因就是整型数据出现了溢出。
Java 虚拟机在处理浮点数运算时,不会抛出任何运行时异常,当一个操作产生溢出时,将会使用有符号的无穷大来表示,如果某个操作结果没有明确的数学定义的话,将会时候 NaN 值来表示。所有使用 NaN 值作为操作数的算术操作,结果都会返回 NaN 。
(8) . 类型转换指令
字节码 | 指令助记符 | 指令含义 |
---|---|---|
0x85 | i2l | 将栈顶 int 型数值强制转换成 long 型数值并将结果压入栈顶 |
0x86 | i2f | 将栈顶 int 型数值强制转换成 float 型数值并将结果压入栈顶 |
0x87 | i2d | 将栈顶 int 型数值强制转换成 double 型数值并将结果压入栈顶 |
0x88 | l2i | 将栈顶 long 型数值强制转换成 int 型数值并将结果压入栈顶 |
0x89 | l2f | 将栈顶 long 型数值强制转换成 float 型数值并将结果压入栈顶 |
0x8a | l2d | 将栈顶 long 型数值强制转换成 double 型数值并将结果压入栈顶 |
0x8b | f2i | 将栈顶 float 型数值强制转换成 int 型数值并将结果压入栈顶 |
0x8c | f2l | 将栈顶 float 型数值强制转换成 long 型数值并将结果压入栈顶 |
0x8d | f2d | 将栈顶 float 型数值强制转换成 double 型数值并将结果压入栈顶 |
0x8e | d2i | 将栈顶 double 型数值强制转换成 int 型数值并将结果压入栈顶 |
0x8f | d2l | 将栈顶 double 型数值强制转换成 long 型数值并将结果压入栈顶 |
0x90 | d2f | 将栈顶 double 型数值强制转换成 float 型数值并将结果压入栈顶 |
0x91 | i2b | 将栈顶 int 型数值强制转换成 byte 型数值并将结果压入栈顶 |
0x92 | i2c | 将栈顶 int 型数值强制转换成 char 型数值并将结果压入栈顶 |
0x93 | i2s | 将栈顶 int 型数值强制转换成 short 型数值并将结果压入栈顶 |
这里需要注意的是强制转换可能会丢失精度的问题
(9) . 方法调用与返回指令
字节码 | 指令助记符 | 指令含义 |
---|---|---|
0xb6 | invokevirtual | 调用实例方法 |
0xb7 | invokespecial | 调用超类构造方法,实例初始化方法,私有方法 |
0xb8 | invokestatic | 调用静态方法 |
0xb9 | invokeinterface | 调用接口方法 |
0xba | invokedynamic | 调用动态方法 |
0xac | ireturn | 从当前方法返回 int |
0xad | lreturn | 从当前方法返回 long |
0xae | freturn | 从当前方法返回 float |
0xaf | dreturn | 从当前方法返回 double |
0xb1 | areturn | 从当前方法返回对象引用 |
invokevirtual 指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是 Java 语言中最常见的方法分派方式。 返回类型中 ,返回类型为boolean、byte、char、short 和 int 类型时都通过ireturn指令实现。
(10) . 异常
字节码 | 指令助记符 | 指令含义 |
---|---|---|
0xbf | athrow | 将栈顶的异常抛出 |
2 . 如何通过class文件查看指令代码
这个方法通过一个指令即可,以windows系统为例,首先打开windows的终端,然后进入到.class文件所在的目录,通过 javap -c 文件名 即可查看指令代码。例如下面的代码
public class Test {
public static void main(String[] args){
int m2 = -1;
int m0 =5;
int n = 2;
int m = 6;
int a1 = 128;
int a2 = 32768;
long k1 = 1;
long k2 = 5;
double d = 1;
Integer ii = new Integer(8);
try{
synchronized (Test.class){
int x = m/n;
}
}catch (NullPointerException e){
e.printStackTrace();
}catch (Exception e){
e.printStackTrace();
}
}
}
然后通过打开终端
回车之后就可以看到指令代码了,具体结果如下
F:\workSpace\ideaSpace\busted\out\production\busted\com\lsj\test>javap -c Test
Compiled from "Test.java"
public class com.lsj.test.Test {
public com.lsj.test.Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_m1
1: istore_1
2: iconst_5
3: istore_2
4: iconst_2
5: istore_3
6: bipush 6
8: istore 4
10: sipush 128
13: istore 5
15: ldc #2 // int 32768
17: istore 6
19: lconst_1
20: lstore 7
22: ldc2_w #3 // long 5l
25: lstore 9
27: dconst_1
28: dstore 11
30: new #5 // class java/lang/Integer
33: dup
34: bipush 8
36: invokespecial #6 // Method java/lang/Integer."<init>":(I)V
39: astore 13
41: ldc #7 // class com/lsj/test/Test
43: dup
44: astore 14
46: monitorenter
47: iload 4
49: iload_3
50: idiv
51: istore 15
53: aload 14
55: monitorexit
56: goto 67
59: astore 16
61: aload 14
63: monitorexit
64: aload 16
66: athrow
67: goto 87
70: astore 14
72: aload 14
74: invokevirtual #9 // Method java/lang/NullPointerException.printStackTrace:()V
77: goto 87
80: astore 14
82: aload 14
84: invokevirtual #11 // Method java/lang/Exception.printStackTrace:()V
87: return
Exception table:
from to target type
47 56 59 any
59 64 59 any
41 67 70 Class java/lang/NullPointerException
41 67 80 Class java/lang/Exception
}
通过指令代码就能看到java代码编译之后每一步究竟做了什么事情了,也方便我们去解决问题。