JVM学习-字节码指令
目录
- 1.入门
- 2 javap 工具
- 3 图解方法执行流程
- 3.1.原始 java 代码
- 3.2.编译后的字节码文件
- 3.3.常量池载入运行时常量池
- 3.4.方法字节码载入方法区
- 3.5.main 线程开始运行,分配栈帧内存
- 3.6.执行引擎开始执行字节码
- 4 练习 - 分析 i++
- 5.条件判断
- 6.循环控制指令
- 7 练习 - 判断结果
- 8 构造方法
- 9 方法调用
- 10.多态的原理
- 11.异常处理
- 11.1.try-catch
- 11.2.多个single-catch
- 11.3.finally
- 11.4.finally面试题
- 11.4.1.finally中的return
- 11.4.2.被吞掉的异常
- 11.4.3.finally不带return
- 12.synchronized
1.入门
接着上一节类文件结构,研究一下两组字节码指令,一个是public cn.itcast.jvm.t5.HelloWorld(); 构造方法的字节码指令
2a b7 00 01 b1
它实际上对应字节码的指令,java虚拟机内部有解释器,解释器会识别这些平台无关的字节码指令,把它们最终解释为机器码,然后执行。
那么怎么知道机器码对应的字节码指令呢。
请参考
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5
查找0x2a
0x2a aload_0
b7 invokespecial
b1 return
- 2a => aload_0 加载 slot 0 的局部变量,即 this,做为下面的 invokespecial 构造方法调用的参数 (把局部变量表中0号槽位的变量加载到操作数栈上)
- b7 => invokespecial 预备调用构造方法,哪个方法呢?(准备进行方法的调用)
- 00 01 引用常量池中 #1 项,即【 Method java/lang/Object." ": () V 】
- b1 表示返回
所以这个是通过this调用了父类的无参构造方法。最后b1是方法执行了要返回。
另一个是 public static void main(java.lang.String[]); 主方法的字节码指令
b2 00 02 12 03 b6 00 04 b1
- b2 => getstatic 用来加载静态变量,哪个静态变量呢?
- 00 02 引用常量池中 #2 项,即【Field java/lang/System.out:Ljava/io/PrintStream;】
- 12 => ldc 加载参数,哪个参数呢?
- 03 引用常量池中 #3 项,即 【String hello world】
- b6 => invokevirtual 预备调用成员方法,哪个方法呢?
- 00 04 引用常量池中 #4 项,即【Method java/io/PrintStream.println:(Ljava/lang/String;)V】
- b1 表示返回
注意,这里字节码是先准备参数,再调用方法。
2 javap 工具
自己分析类文件结构太麻烦了,Oracle 提供了 javap 工具来反编译 class 文件
使用IDEA反编译
F:\IDEA\projects\jvm>javap -v F:\IDEA\projects\jvm\out\production\untitled\cn\yj\jvm\HelloWorld.class
Classfile /F:/IDEA/projects/jvm/out/production/untitled/cn/yj/jvm/HelloWorld.classLast modified 2021-2-2; size 553 bytesMD5 checksum 6b7033e0eab7845f9c8aa7b8e1f2d44fCompiled from "HelloWorld.java"
public class cn.yj.jvm.HelloWorldminor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER
Constant pool:#1 = Methodref #6.#20 // java/lang/Object."<init>":()V#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;#3 = String #23 // hello world#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V#5 = Class #26 // cn/yj/jvm/HelloWorld#6 = Class #27 // java/lang/Object#7 = Utf8 <init>#8 = Utf8 ()V#9 = Utf8 Code#10 = Utf8 LineNumberTable#11 = Utf8 LocalVariableTable#12 = Utf8 this#13 = Utf8 Lcn/yj/jvm/HelloWorld;#14 = Utf8 main#15 = Utf8 ([Ljava/lang/String;)V#16 = Utf8 args#17 = Utf8 [Ljava/lang/String;#18 = Utf8 SourceFile#19 = Utf8 HelloWorld.java#20 = NameAndType #7:#8 // "<init>":()V#21 = Class #28 // java/lang/System#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;#23 = Utf8 hello world#24 = Class #31 // java/io/PrintStream#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V#26 = Utf8 cn/yj/jvm/HelloWorld#27 = Utf8 java/lang/Object#28 = Utf8 java/lang/System#29 = Utf8 out#30 = Utf8 Ljava/io/PrintStream;#31 = Utf8 java/io/PrintStream#32 = Utf8 println#33 = Utf8 (Ljava/lang/String;)V
{public cn.yj.jvm.HelloWorld();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 2: 0LocalVariableTable:Start Length Slot Name Signature0 5 0 this Lcn/yj/jvm/HelloWorld;public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=1, args_size=10: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #3 // String hello world5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: returnLineNumberTable:line 4: 0line 5: 8LocalVariableTable:Start Length Slot Name Signature0 9 0 args [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"
3 图解方法执行流程
3.1.原始 java 代码
package cn.yj.jvm;/** * 演示 字节码指令 和 操作数栈、常量池的关系 */
public class Demo3_1 {public static void main(String[] args){int a = 10;int b = Short.MAX_VALUE + 1;int c = a + b;System.out.println(c);}
}
3.2.编译后的字节码文件
F:\IDEA\projects\jvm>javap -v F:\IDEA\projects\jvm\out\production\untitled\cn\yj\jvm\Demo3_1.class
Classfile /F:/IDEA/projects/jvm/out/production/untitled/cn/yj/jvm/Demo3_1.classLast modified 2021-2-2; size 603 bytesMD5 checksum 9bdbe178a29e07556915f368dbf7def1Compiled from "Demo3_1.java"
public class cn.yj.jvm.Demo3_1minor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER
Constant pool:#1 = Methodref #7.#25 // java/lang/Object."<init>":()V#2 = Class #26 // java/lang/Short#3 = Integer 32768#4 = Fieldref #27.#28 // java/lang/System.out:Ljava/io/PrintStream;#5 = Methodref #29.#30 // java/io/PrintStream.println:(I)V#6 = Class #31 // cn/yj/jvm/Demo3_1#7 = Class #32 // java/lang/Object#8 = Utf8 <init>#9 = Utf8 ()V#10 = Utf8 Code#11 = Utf8 LineNumberTable#12 = Utf8 LocalVariableTable#13 = Utf8 this#14 = Utf8 Lcn/yj/jvm/Demo3_1;#15 = Utf8 main#16 = Utf8 ([Ljava/lang/String;)V#17 = Utf8 args#18 = Utf8 [Ljava/lang/String;#19 = Utf8 a#20 = Utf8 I#21 = Utf8 b#22 = Utf8 c#23 = Utf8 SourceFile#24 = Utf8 Demo3_1.java#25 = NameAndType #8:#9 // "<init>":()V#26 = Utf8 java/lang/Short#27 = Class #33 // java/lang/System#28 = NameAndType #34:#35 // out:Ljava/io/PrintStream;#29 = Class #36 // java/io/PrintStream#30 = NameAndType #37:#38 // println:(I)V#31 = Utf8 cn/yj/jvm/Demo3_1#32 = Utf8 java/lang/Object#33 = Utf8 java/lang/System#34 = Utf8 out#35 = Utf8 Ljava/io/PrintStream;#36 = Utf8 java/io/PrintStream#37 = Utf8 println#38 = Utf8 (I)V
{public cn.yj.jvm.Demo3_1();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 4: 0LocalVariableTable:Start Length Slot Name Signature0 5 0 this Lcn/yj/jvm/Demo3_1;public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=4, args_size=10: bipush 102: istore_13: ldc #3 // int 327685: istore_26: iload_17: iload_28: iadd9: istore_310: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;13: iload_314: invokevirtual #5 // Method java/io/PrintStream.println:(I)V17: returnLineNumberTable:line 7: 0line 8: 3line 9: 6line 10: 10line 11: 17LocalVariableTable:Start Length Slot Name Signature0 18 0 args [Ljava/lang/String;3 15 1 a I6 12 2 b I10 8 3 c I
}
SourceFile: "Demo3_1.java"
3.3.常量池载入运行时常量池
当我们java代码被执行时,它会由java虚拟机的类加载器把我们main方法所在的类进行类加载的操作,类加载实际上把这些字节的class的数据读取到内存里,常量池的数据被放入运行时常量池,运行时常量池属于方法区的组成部分,只是因为相对比较特殊,其实就是把class文件中的数据存入到运行时常量池的地方。将来找其中一些常量池信息就到运行时常量池中找。
如图只列出了3,4,5几项,第3项是原码中的int b = Short.MAX_VALUE + 1;而int a=10;比较小的数字与方法字节码存储在一起,不存在常量池中。一旦超过了Short整数的最大值的范围,就存到常量池中。
常量池也属于方法区,只不过这里单独提出来了
3.4.方法字节码载入方法区
3.5.main 线程开始运行,分配栈帧内存
(stack=2,locals=4) 对应操作数栈有2个空间(每个空间4个字节),局部变量表中有4个槽位
3.6.执行引擎开始执行字节码
bipush 10
- 将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有
- sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)
- ldc 将一个 int 压入操作数栈
- ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节)
- 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池
istore 1
将操作数栈栈顶元素弹出,放入局部变量表的slot 1中0
对应代码中的
a = 10
ldc #3
读取运行时常量池中#3,即32768(超过short最大值范围的数会被放到运行时常量池中),将其加载到操作数栈中
注意 Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算好的
istore 2
将操作数栈中的元素弹出,放到局部变量表的2号位置
iload_1 iload_2
将局部变量表中1号位置和2号位置的元素放入操作数栈中
因为只能在操作数栈中执行运算操作
iadd
将操作数栈中的两个元素弹出栈并相加,结果在压入操作数栈中
istore 3
将操作数栈中的元素弹出,放入局部变量表的3号位置
getstatic #4
在运行时常量池中找到#4,发现是一个对象
在堆内存中找到该对象,并将其引用放入操作数栈中
iload 3
将局部变量表中3号位置的元素压入操作数栈中
invokevirtual 5
找到常量池 #5 项,定位到方法区 java/io/PrintStream.println:(I)V 方法
生成新的栈帧(分配 locals、stack等)
传递参数,执行新栈帧中的字节码
执行完毕,弹出栈帧
清除 main 操作数栈内容
return
完成 main 方法调用,弹出 main 栈帧
程序结束
4 练习 - 分析 i++
目的:从字节码角度分析 a++ 相关题目
源码:
package cn.yj.jvm;/** * 从字节码角度分析 a++ 相关题目 */ public class Demo3_2 {public static void main(String[] args) {int a = 10;int b = a++ + ++a + a--;System.out.println(a);System.out.println(b);}
}
字节码:
F:\IDEA\projects\jvm>javap -v F:\IDEA\projects\jvm\out\production\untitled\cn\yj\jvm\Demo3_2.class
Classfile /F:/IDEA/projects/jvm/out/production/untitled/cn/yj/jvm/Demo3_2.classLast modified 2021-2-3; size 578 bytesMD5 checksum c5e9d3ebbd57d36a03305a1c3a5d9b4cCompiled from "Demo3_2.java"
public class cn.yj.jvm.Demo3_2minor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER
Constant pool:#1 = Methodref #5.#22 // java/lang/Object."<init>":()V#2 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream;#3 = Methodref #25.#26 // java/io/PrintStream.println:(I)V#4 = Class #27 // cn/yj/jvm/Demo3_2#5 = Class #28 // java/lang/Object#6 = Utf8 <init>#7 = Utf8 ()V#8 = Utf8 Code#9 = Utf8 LineNumberTable#10 = Utf8 LocalVariableTable#11 = Utf8 this#12 = Utf8 Lcn/yj/jvm/Demo3_2;#13 = Utf8 main#14 = Utf8 ([Ljava/lang/String;)V#15 = Utf8 args#16 = Utf8 [Ljava/lang/String;#17 = Utf8 a#18 = Utf8 I#19 = Utf8 b#20 = Utf8 SourceFile#21 = Utf8 Demo3_2.java#22 = NameAndType #6:#7 // "<init>":()V#23 = Class #29 // java/lang/System#24 = NameAndType #30:#31 // out:Ljava/io/PrintStream;#25 = Class #32 // java/io/PrintStream#26 = NameAndType #33:#34 // println:(I)V#27 = Utf8 cn/yj/jvm/Demo3_2#28 = Utf8 java/lang/Object#29 = Utf8 java/lang/System#30 = Utf8 out#31 = Utf8 Ljava/io/PrintStream;#32 = Utf8 java/io/PrintStream#33 = Utf8 println#34 = Utf8 (I)V
{public cn.yj.jvm.Demo3_2();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 3: 0LocalVariableTable:Start Length Slot Name Signature0 5 0 this Lcn/yj/jvm/Demo3_2;public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=3, args_size=10: bipush 102: istore_13: iload_14: iinc 1, 17: iinc 1, 110: iload_111: iadd12: iload_113: iinc 1, -116: iadd17: istore_218: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;21: iload_122: invokevirtual #3 // Method java/io/PrintStream.println:(I)V25: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;28: iload_229: invokevirtual #3 // Method java/io/PrintStream.println:(I)V32: returnLineNumberTable:line 6: 0line 7: 3line 8: 18line 9: 25line 10: 32LocalVariableTable:Start Length Slot Name Signature0 33 0 args [Ljava/lang/String;3 30 1 a I18 15 2 b I
}
SourceFile: "Demo3_2.java"
分析:
注意 iinc 指令是直接在局部变量 slot 上进行运算
a++ 和 ++a 的区别是先执行 iload 还是 先执行 iinc
5.条件判断
指令 | 助记符 | 含义 |
---|---|---|
0x99 | ifeq | 判断是否 == 0 |
0x9a | ifne | 判断是否 != 0 |
0x9b | iflt | 判断是否 < 0 |
0x9c | ifge | 判断是否 >= 0 |
0x9d | ifgt | 判断是否 > 0 |
0x9e | ifle | 判断是否 <= 0 |
0x9f | if_icmpeq | 两个int是否 == |
0xa0 | if_icmpne | 两个int是否 != |
0xa1 | if_icmplt | 两个int是否 < |
0xa2 | if_icmpge | 两个int是否 >= |
0xa3 | if_icmpgt | 两个int是否 > |
0xa4 | if_icmple | 两个int是否 <= |
0xa5 | if_acmpeq | 两个引用是否 == |
0xa6 | if_acmpne | 两个引用是否 != |
0xc6 | ifnull | 判断是否 == null |
0xc7 | ifnonnull | 判断是否 != null |
几点说明:
byte,short,char 都会按 int 比较,因为操作数栈都是 4 字节 goto 用来进行跳转到指定行号的字节码
字节码:
0: iconst_01: istore_12: iload_13: ifne 126: bipush 108: istore_19: goto 1512: bipush 2014: istore_115: return
注意,比较小的数用iconst来表示,ifne 12。判断操作数中的栈是不是不等于0,如果不等于0就会跳转到12行,如果不成立,就会执行后面的代码,接着往下走。goto 15是直接跳转到15行。
思考
细心的同学应当注意到,以上比较指令中没有 long,float,double 的比较,那么它们要比较怎 么办?
参考 https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.lcmp
6.循环控制指令
其实循环控制还是前面介绍的那些指令,例如 while 循环:
public class Demo3_4 {public static void main(String[] args) {int a = 0;while (a < 10) { a++;} }
}
字节码是:0: iconst_0 1: istore_1 2: iload_1 3: bipush 10 5: if_icmpge 14 8: iinc 1, 1 11: goto 2 14: return
public class Demo3_5 {public static void main(String[] args) {int a = 0;do {a++;}while (a < 10);}}
}
后再看看 for 循环:
public class Demo3_6 {public static void main(String[] args) {for (int i = 0; i < 10; i++) { }}
}
0: iconst_0 1: istore_1 2: iload_1 3: bipush 10 5: if_icmpge 14 8: iinc 1, 1 11: goto 214: return
注意 比较 while 和 for 的字节码,你发现它们是一模一样的,殊途也能同归
7 练习 - 判断结果
请从字节码角度分析,下列代码运行的结果:
public class Demo3_6_1 {public static void main(String[] args) {int i = 0;int x = 0;while (i < 10) {x = x++;i++;}System.out.println(x);}
}
x=x++;
x++的过程对应两条字节码指令
iload_x
iinc x,1
初始x(0)
iload是把局部变量表中的0读进操作数栈。读完以后,我们iinc进行自增。自增的结果是局部变量表中的x变为1,它然后又执行了赋值操作,把操作数栈中的0取出来,再覆盖掉局部变量中的x.等第一次循环之后,局部变量表中的x仍然是0,即使再循环多少次,值仍然为0。
8 构造方法
public class Demo3_8_1 {static int i = 10;static {i = 20;}static {i = 30;}public static void main(String[] args) {System.out.println(Demo3_8_1.i);}
}
编译器会按从上至下的顺序,收集所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方法 < cinit> ()V :
stack=1, locals=0, args_size=00: bipush 102: putstatic #2 // Field i:I5: bipush 207: putstatic #2 // Field i:I10: bipush 3012: putstatic #2 // Field i:I15: return
最后赋值的是30,所以结果是30
init()V
public class Demo4 {private String a = "s1";{b = 20;}private int b = 10;{a = "s2";}public Demo4(String a, int b) {this.a = a;this.b = b;}public static void main(String[] args) {Demo4 d = new Demo4("s3", 30);System.out.println(d.a);System.out.println(d.b);}
}
编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在后
Code:stack=2, locals=3, args_size=30: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: aload_05: ldc #2 // String s17: putfield #3 // Field a:Ljava/lang/String;10: aload_011: bipush 2013: putfield #4 // Field b:I16: aload_017: bipush 1019: putfield #4 // Field b:I22: aload_023: ldc #5 // String s225: putfield #3 // Field a:Ljava/lang/String;//原始构造方法在最后执行28: aload_029: aload_130: putfield #3 // Field a:Ljava/lang/String;33: aload_034: iload_235: putfield #4 // Field b:I38: return
执行顺序:静态代码块->非静态代码块->类的构造方法
9 方法调用
public class Demo5 {public Demo5() {}private void test1() {}private final void test2() {}public void test3() {}public static void test4() {}public static void main(String[] args) {Demo5 demo5 = new Demo5();demo5.test1();demo5.test2();demo5.test3();Demo5.test4();}
}
不同方法在调用时,对应的虚拟机指令有所区别
私有、构造、被final修饰的方法,在调用时都使用invokespecial指令
普通成员方法在调用时,使用invokespecial指令。因为编译期间无法确定该方法的内容,只有在运行期间才能确定
静态方法在调用时使用invokestatic指令
invokespecial在调用时无法确定是调用哪个对象的方法,也许是父类的,也许是子类的。invokespecial称之为动态绑定,在运行的时候确定调用哪个对象的方法。invokestatic静态绑定直接就能找到方法的入口地址了。
Code:stack=2, locals=2, args_size=10: new #2 // class com/nyima/JVM/day5/Demo5 3: dup4: invokespecial #3 // Method "<init>":()V7: astore_18: aload_19: invokespecial #4 // Method test1:()V12: aload_113: invokespecial #5 // Method test2:()V16: aload_117: invokevirtual #6 // Method test3:()V20: invokestatic #7 // Method test4:()V23: return
new 是创建【对象】,给对象分配堆内存,执行成功会将【对象引用】压入操作数栈 Java虚拟机的指令由一个字节长度的.代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成. 基本数据类型 1.除了l ... 1.字节码 Java能发展到现在,其"一次编译,多处运行"的功能功不可没,这里最主要的功劳就是JVM和字节码了,在不同平台和操作系统上根据JVM规范的定制JVM可以运行相同字节码( ... 前言简介 前文已经对虚拟机进行过了简单的介绍,并且也对class文件结构,以及字节码指令进行了详尽的说明 想要了解JVM的运行机制,以及如何优化你的代码,你还需要了解一下,java编译器到底是如何编译 ... 文章目录 什么是字节码指令 javap的用法 字节码与数据类型 字节码指令集 加载和存储指令 运算指令 类型转换指令 对象创建与访问指令 操作数栈管理指令 控制转移指令 方法调用和返回指令 异常处理指 ... Java 虚拟机的指令由一个字节长度的.代表着某种特定操作含义的数字(称为操作码)以及跟随其后的零至多个代表此操作所需参数(操作数)而构成.由于 Java 虚拟机采用面向操作数栈而不是寄存器的架构,所 ... 本文由HeapDump性能社区首席讲师鸠摩(马智)授权整理发布 第17章-x86-64寄存器 不同的CPU都能够解释的机器语言的体系称为指令集架构(ISA,Instruction Set Archit ... 作者简介:泽恩,美团到店住宿业务研发团队工程师.文章转载于公众号:美团技术团队 1. 字节码 1.1 什么是字节码? Java之所以可以"一次编译,到处运行",一是因为JVM针对各 ... 配套视频: 为什么推荐大家学习Java字节码 https://www.bilibili.com/video/av77600176/ 一.背景 本文主要探讨:为什么要学习 JVM 字节码? 可能很多人会 ... 来自:烟雨星空 前言 我们平时编码过程中,可能很少去查看 Java 文件编译后的字节码指令.但是,不管你是因为对技术非常热爱,喜欢刨根问底,还是想在别人面前装X .我认为,都非常有必要了解一下常见的字 ...
dup 是复制操作数栈栈顶的内容,本例即为【对象引用】,为什么需要两份引用呢,一个是要配合 invokespecial 调用该对象的构造方法 “init”
JVM学习-字节码指令相关推荐
最新文章
热门文章