JVM_07 类加载与字节码技术(字节码指令)
Posted 兴趣使然の草帽路飞
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM_07 类加载与字节码技术(字节码指令)相关的知识,希望对你有一定的参考价值。
2、字节码指令
可参考: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5
接着上一节,研究一下两组字节码指令,一个是 public cn.itcast.jvm.t5.HelloWorld();
构造方法的字节码指令:
2a => aload_0
加载slot 0
的局部变量,即this
,做为下面的invokespecial
构造方法调用的参数b7 => invokespecial
预备调用构造方法,哪个方法呢?- 00 01 引用常量池中
#1
项,即【Method java/lang/Object."":()V
】 - 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 表示返回
javap工具
自己分析类文件结构太麻烦了,Oracle 提供了 javap 工具来反编译 class 文件Oracle 提供了 javap 工具来反编译 class 文件:javap -v HelloWorld.java
Classfile /D:/Note/笔记/JDK源码学习/IDEA-workspace/jdk8/out/production/jdk8/com/haust/jvm/HelloWorld.class
Last modified 2021-4-28; size 562 bytes
MD5 checksum f25f5eebfd7ea514993445cea4f6b104
Compiled from "HelloWorld.java"
public class com.haust.jvm.HelloWorld
minor version: 0
major version: 52
flags: 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 // com/haust/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 Lcom/haust/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 com/haust/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 com.haust.jvm.HelloWorld();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 8: 0
LocalVariableTable:
图解方法执行流程
示例代码如下:
/**
* @author csp
* @date 2021-05-01
* 演示:字节码指令,操作数栈,常量池的关系
*/
public class Demo3_1 {
public static void main(String[] args) {
// 数字比较小的变量,不存时常量池中,而是跟方法的字节码指令存在一起
int a = 10;
// Short.MAX_VALUE: 32767
// Short.MAX_VALUE+1,当超过Short.MAX_VALUE最大值,则将该变量存储在常量池中
int b = Short.MAX_VALUE + 1;
int c = a + b;
System.out.println(c);
}
}
反编译后字节码如下:
Classfile /D:/Note/笔记/JDK源码学习/IDEA-workspace/jdk8/out/production/jdk8/com/haust/jvm/Demo3_1.class
Last modified 2021-5-1; size 611 bytes
MD5 checksum 5367d11bd9bc1af8bf3b0b037ad7c13f
Compiled from "Demo3_1.java"
public class com.haust.jvm.Demo3_1
minor version: 0
major version: 52
flags: 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 // com/haust/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 Lcom/haust/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 com/haust/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 com.haust.jvm.Demo3_1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/haust/jvm/Demo3_1;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: bipush 10
2: istore_1
3: ldc #3 // int 32768
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
13: iload_3
14: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
17: return
LineNumberTable:
line 10: 0
line 11: 3
line 12: 6
line 13: 10
line 14: 17
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 args [Ljava/lang/String;
3 15 1 a I
6 12 2 b I
10 8 3 c I
}
SourceFile: "Demo3_1.java"
1.常量池载入运行时常量池
常量池也属于方法区,只不过这里单独提出来了:
2.方法字节码载入方法区
3.Main线程开始运行,分配栈帧内存
(stack=2 ,locals=4 ) 操作数栈有2个空间(每个空间4个字节),局部变量表中有4个槽位:
4.执行引擎开始执行字节码
bipush 10:将一个byte的数字压入操作数栈()~
- 将一个 byte 压入操作数栈,(操作数栈的宽度为4个字节,因此byte的数入栈时,其长度会补齐 4 个字节),类似的指令还有:
- sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)
- ldc 将一个 int 压入操作数栈
- ldc2_w 将一个 long 压入操作数栈(操作数栈的宽度为4个字节,因此long的数入栈时,分两次压入,long 是 8 个字节)
- 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池
istore 1:
- 将操作数栈栈顶元素弹出,放入局部变量表的slot 1(槽位)中,对应java代码中的:
a = 10;
ldc #3
读取运行时常量池中#3
,即32768(超过short最大值范围的数会被放到运行时常量池中),将其加载到操作数栈中
注意: Short.MAX_VALUE
是 32767,所以 32768 = Short.MAX_VALUE + 1
实际是在编译期间计算好的。
istore 2
将操作数栈中的元素弹出,放到局部变量表的2号位置。
iload1 与 iload2
将局部变量表中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 栈帧,程序结束。
5.练习:分析a++ + ++a
案例代码如下:
/**
* 从字节码角度分析 a++ 相关题目
*/
public class Demo3_2 {
public static void main(String[] args) {
int a = 10;
int b = a++ + ++a + a--;
System.out.println(a);// 11
System.out.println(b);// 34
}
}
为什么最终的x结果为0呢? 通过分析字节码指令即可知晓
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: bipush 10
2: istore_1
3: iload_1
4: iinc 1, 1
7: iinc 1, 1
10: iload_1
11: iadd
12: iload_1
13: iinc 1, -1
16: iadd
17: istore_2
21: iload_1
22: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
25: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
28: iload_2
29: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
32: return
LineNumberTable:
line 8: 0
line 9: 3
line 10: 18
line 11: 25
line 12: 32
LocalVariableTable:
Start Length Slot Name Signature
0 33 0 args [Ljava/lang/String;
3 30 1 a I
18 15 2 b I
分析:
- 注意 iinc 指令是直接在局部变量 slot 上进行运算。
a++
和++a
的区别是先执行 iload(读取a) 还是 先执行 iinc。a++
是先iload再iinc,++a
相反。
bipush 10
操作是把a = 10
放入操作数栈:
istore 1
操作,把操作数栈中的10弹出,放入到局部变量表的槽位1中:
接下来执行a++
操作,我们上边提前说明了,a++
是先执行iload
读取,再执行iinc
加1
iload 1
将 变量a=10
,读取到操作数栈stack中:
- 执行
iinc
指令,在局部变量表上对a进行+1操作,这时候a为11:
下面执行++a
操作,先iinc
在iload
:
- 执行
iinc
指令,在局部变量表上对a进行+1操作,这时候a为12:
iload 1
将局部变量表中a=12
,读取到操作数栈stack中:
下面进行 a++ + ++a
操作,在操作数栈中进行相加,得到结果22,这时候第1个加法完成:
下面执行第二个加法(a++ + ++a)+ a--
操作:
a--
先执行iload
命令,在执行inc 1,-1
命令,如下,先将局部变量表中的12读取到操作数栈:
- 接下来执行
inc 1,-1
命令,在局部变量表中进行-1操作,此时局部变量表中的值由12减为11:
- 在操作数栈中,执行第二次加法运算,得到结果为34:
最后将操作数栈中的数据弹出到局部变量表中,赋值2号槽位b=34:
因此程序运行结果得到:a为11,b为34。
以上是关于JVM_07 类加载与字节码技术(字节码指令)的主要内容,如果未能解决你的问题,请参考以下文章