这个是这个系列的上一个文章(传送门):
jvm深入研究文档--程序执行专业户-虚拟机栈--jvm底层探索(2)_一单成的博客-CSDN博客
在上一个文章中,主要探讨了虚拟机栈的主要组成成员以及中间的数据结构。那么这个文章就从代码的角度出发来看一看经过编译之后的代码,在jvm的影响下面到底是什么执行的,并且是怎么完成我们之前看着十分晦涩难懂的进站压栈出栈的。
在jvm的虚拟机栈中的组成为一个一个栈帧,在每个栈帧呢都是线程私有的,每个栈帧都包含了,局部变量表、操作数据表、动态链接和方法返回地址(值得注意的是方法返回地址在官方的说法下面一共有两个,一个是正常的方法出口,另一个是异常的方法出口)。
我们来探讨和研究一下在这个底层到底是如何试进行数据的调用的以及如何进行数据的操作影响的在一个简单的方法中。
JVM字节码指令大全
public class Demo01 {
private final int adn = 20;
public int compute(int num){
return adn + num;
}
public static void main(String[] args) {
Demo01 demo01 = new Demo01();
int res = demo01.compute(50);
System.out.println(res);
}
}
这段代码定义了一个名为Demo01
的公共类。在这个类中,有一个私有的常量adn
,其值为20。
接下来,定义了一个名为compute
的公共方法,该方法接受一个整数参数num
,并返回adn
与num
之和。
在main
方法中,创建了Demo01
类的一个实例demo01
,然后调用compute
方法并传入50作为参数,将返回的结果赋值给变量res
。最后,使用System.out.println
打印出res
的值。
这段代码很简单就是一个简简单单的数相加。
我们通过代码编译将这个代码转换成jvm编译过的代码。
我来描述一下在IDEA中的具体操作,如果动手能力强的小伙伴可以跟着我一起来尝试一下!
右击
java .\Demo01.java
javap -c -v -p -l .\Demo01.class > "Demo01.txt"
对这个代码进行解释一下:
-c对代码进行反编译
-v输出附加信息,常量池等
-l输出局部变量
-p显示私有内容
注意:还是在刚才的路径中和刚才的终端中的!
这个时候我们在本文件夹下面就能看到我们需要的文件内容了。
Classfile /E:/desktop/stack/Demo01.class
Last modified 2023-9-25; size 547 bytes
MD5 checksum 291623c4dbf535dee20c1688119b9603
Compiled from "Demo01.java"
public class Demo01
minor version: 0
major version: 61
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "":()V
#4 = Utf8 java/lang/Object
#5 = Utf8
#6 = Utf8 ()V
#7 = Fieldref #8.#9 // Demo01.adn:I
#8 = Class #10 // Demo01
#9 = NameAndType #11:#12 // adn:I
#10 = Utf8 Demo01
#11 = Utf8 adn
#12 = Utf8 I
#13 = Methodref #8.#3 // Demo01."":()V
#14 = Methodref #8.#15 // Demo01.compute:(I)I
#15 = NameAndType #16:#17 // compute:(I)I
#16 = Utf8 compute
#17 = Utf8 (I)I
#18 = Fieldref #19.#20 // java/lang/System.out:Ljava/io/PrintStream;
#19 = Class #21 // java/lang/System
#20 = NameAndType #22:#23 // out:Ljava/io/PrintStream;
#21 = Utf8 java/lang/System
#22 = Utf8 out
#23 = Utf8 Ljava/io/PrintStream;
#24 = Methodref #25.#26 // java/io/PrintStream.println:(I)V
#25 = Class #27 // java/io/PrintStream
#26 = NameAndType #28:#29 // println:(I)V
#27 = Utf8 java/io/PrintStream
#28 = Utf8 println
#29 = Utf8 (I)V
#30 = Utf8 ConstantValue
#31 = Integer 20
#32 = Utf8 Code
#33 = Utf8 LineNumberTable
#34 = Utf8 main
#35 = Utf8 ([Ljava/lang/String;)V
#36 = Utf8 SourceFile
#37 = Utf8 Demo01.java
{
private final int adn;
descriptor: I
flags: ACC_PRIVATE, ACC_FINAL
ConstantValue: int 20
public Demo01();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: aload_0
5: bipush 20
7: putfield #7 // Field adn:I
10: return
LineNumberTable:
line 1: 0
line 3: 4
public int compute(int);
descriptor: (I)I
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: bipush 20
2: iload_1
3: iadd
4: ireturn
LineNumberTable:
line 6: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #8 // class Demo01
3: dup
4: invokespecial #13 // Method "":()V
7: astore_1
8: aload_1
9: bipush 50
11: invokevirtual #14 // Method compute:(I)I
14: istore_2
15: getstatic #18 // Field java/lang/System.out:Ljava/io/PrintStream;
18: iload_2
19: invokevirtual #24 // Method java/io/PrintStream.println:(I)V
22: return
LineNumberTable:
line 10: 0
line 11: 8
line 12: 15
line 13: 22
}
SourceFile: "Demo01.java"
jvm字节码大全
我在这里提供了一个连接是字节码的内容,需要的可以在这里拿取,并且下面的研究会用到。
我们能发现这个字节码虽然不是很认识但是发现中间类的结构大体还是没变化的。
这里为类的定义
这里为我们的常量
这里为无参构造
这个就是我们自己写的计算方法
这里是我们写的main方法
解释:
在每个方法中分了三个部分
1、code:对应的指令集
2、LineNumberTable:代码的行数和前面的代码的偏移量之间的关系
3、LocalVariableTable:这个就是局部变量表了
我们根据JVM字节码指令查看一下
https://www.cnblogs.com/ageovb/p/15187314.html#%E5%8F%82%E8%80%83
能看到它是将我们的常量20推送到栈顶,也就是压栈
那么现在就引入了一个概念为局部变量,那么这个局部变量从哪里来的?
我们知道在本地应用数据可以直接使用this来完成本地应用。
并且我们的常量在这个文件生成的时候就已经读取进来了,本地变量的表中的数据是根据数据的数据来完成装载的。那么就是说此时的本地变量为:
常量可以使用this来表示并且引用。 那么这个我上面的图中的p1的名字实际上在我们的代码中应该是num!!!!
那此时的
就是将我们现在的赋值了的形参压入栈顶,我们将num复制为50
这个就是将数据拿出来放到主main方法中了。
那么在上述的方法中呢,只是对本地变量表读取。那么我们在mian方法中就有对于数据进行操作的。
第一个指向是方法的调用,就是上面我们在讨论的方法,当获取到返回值之后将数据存到本地的第二个局部变量。因为现在栈中只剩下了一个计算过的数据。
在本地方法中的第一个位置是常量,那么在这个线程中的第二个数据就是我们刚才计算出来的那个了。
这个操作数栈和本地变量表就像是我们在草稿纸上操作数进行了计算然后将答案写在了本地方法栈上,并将草稿纸删掉。
可以看到有些指令后面带了井号和数字,这个数字就是指向常量池中的引用。
引用的具体就是在后面的//注释的东西,就是引用的内容。
这里就是常量池,我们通过在-p之后添加-v的方法来将常量池进行打印!!!