前言
JVM内存模型中,程序计数器和虚拟机是线程私有的,程序计数器保存了线程执行到的下一条指令的地址,虚拟机保存了什么,我们来看看
虚拟机栈
我们先从一个整体的视角看JVM的虚拟机栈
-
方法是Java虚拟机中最基本的执行单位,以栈帧的格式作为栈元素存在于虚拟机栈中
-
每个栈帧都包括了局部变量表、操作数栈、方法返回地址、动态链接和一些额外的附加信息
-
同一时刻、同一个线程里面,只有位于栈顶的栈帧是有效的,被称为
当前栈帧
,与这个栈帧关联的方法被称为当前方法
局部变量表
局部变量表是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量,局部变量表的大小是在编译期就确定下来的
slot
先看一段代码,感受一下局部变量表存储的东西
public class LocalVariablesTableTest {
public static void main(String[] args) {
LocalVariablesTableTest localVariablesTableTest = new LocalVariablesTableTest();
localVariablesTableTest.methodA();
}
// 这个方法中定义了不同类型的变量
public void methodA() {
byte by = 127;
char c = 'a';
boolean b = false;
short s = 15;
int i = 10;
long l = 100L;
float f = 10;
double d = 20.2;
String str = "str";
}
}
复制代码
局部变量表最大槽数在编译期就确定了
从上面的实验中,我们可以得出一下结论
- 局部变量表是以变量槽(Variable Slot) 为基本单位,一个变量槽可以存放一个32位以内的数据类型,Java中占用不超过32位存储空间的数据类型有boolean、byte、char、short、int、float、reference(一个对象实例的引用),对于64位的数据类型,JVM会以高位对齐的方式为其分配两个连续的变量槽空间,java中明确的64位的数据类型只有long和double
- JVM通过索引定位的方式访问局部变量表,如果是32位以内的数据类型,索引N表示变量槽N,如果是64位的数据类型,会使用前一个索引
- 实例方法中,局部变量表的第0位变量槽默认是所属对象实例的引用,也就是this
重复利用的slot
public class LocalVariablesTableTest {
public static void main(String[] args) {
LocalVariablesTableTest localVariablesTableTest = new LocalVariablesTableTest();
localVariablesTableTest.methodA();
}
public void methodB() {
int a = 10;
{
int b = 20;
}
int c = 30;
}
}
复制代码
通过上图可以看到b变量跟c变量使用了同一个slot
操作数栈
操作数栈的作用有点类似于寄存器,用来存储临时数据,它有一下特点
- 32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2
- 操作数栈的最大深度在编译期的时候已经确定
public class OperatorTest {
public int add() {
int i = 10;
int j = 20;
return i + j;
}
}
复制代码
对应字节码
0 bipush 10
2 istore_1
3 bipush 20
5 istore_2
6 iload_1
7 iload_2
8 iadd
9 ireturn
复制代码
根据这些字节码,局部变量表、操作数栈、程序计数器之间的配合执行如下
- 程序计数器存储了下一条执行指令的地址
- 局部变量表存储了方法参数和方法内部定义的局部变量
- 操作数栈存储了临时数据
动态链接
了解动态链接之前,先要了解什么是运行时常量池,运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
看下面一段代码
public class DynamicLinkingTest {
public static void main(String[] args) {
DynamicLinkingTest dynamicLinkingTest = new DynamicLinkingTest();
dynamicLinkingTest.methodA();
}
public void methodA() {
int a = 10;
int b = 20;
int c = a + b;
}
}
复制代码
对应字节码
➜ jvm javap -v DynamicLinkingTest.class
Classfile /Users/zhangxiaobin/IdeaProjects/jvm-demo/target/classes/com/example/jvm/DynamicLinkingTest.class
Last modified 2021-8-4; size 630 bytes
MD5 checksum bd9d215eb905d941f1241febea17a3fc
Compiled from "DynamicLinkingTest.java"
public class com.example.jvm.DynamicLinkingTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
# 这里就是运行时常量区
Constant pool:
#1 = Methodref #5.#25 // java/lang/Object."<init>":()V
#2 = Class #26 // com/example/jvm/DynamicLinkingTest
#3 = Methodref #2.#25 // com/example/jvm/DynamicLinkingTest."<init>":()V
#4 = Methodref #2.#27 // com/example/jvm/DynamicLinkingTest.methodA:()V
#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 Lcom/example/jvm/DynamicLinkingTest;
#13 = Utf8 main
#14 = Utf8 ([Ljava/lang/String;)V
#15 = Utf8 args
#16 = Utf8 [Ljava/lang/String;
#17 = Utf8 dynamicLinkingTest
#18 = Utf8 methodA
#19 = Utf8 a
#20 = Utf8 I
#21 = Utf8 b
#22 = Utf8 c
#23 = Utf8 SourceFile
#24 = Utf8 DynamicLinkingTest.java
#25 = NameAndType #6:#7 // "<init>":()V
#26 = Utf8 com/example/jvm/DynamicLinkingTest
#27 = NameAndType #18:#7 // methodA:()V
#28 = Utf8 java/lang/Object
{
public com.example.jvm.DynamicLinkingTest();
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 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/example/jvm/DynamicLinkingTest;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class com/example/jvm/DynamicLinkingTest
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method methodA:()V
12: return
LineNumberTable:
line 6: 0
line 7: 8
line 8: 12
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 args [Ljava/lang/String;
8 5 1 dynamicLinkingTest Lcom/example/jvm/DynamicLinkingTest;
public void methodA();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: bipush 10
2: istore_1
3: bipush 20
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: return
LineNumberTable:
line 11: 0
line 12: 3
line 13: 6
line 14: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/example/jvm/DynamicLinkingTest;
3 8 1 a I
6 5 2 b I
10 1 3 c I
}
SourceFile: "DynamicLinkingTest.java"
复制代码
在字节码中,main方法通过invokevirtual调用了methodA,调用的过程是这样的
- invokevirtual旁边有#4,在Constant pool找到#4,#4指向了#2.#27
- 在Constant pool中继续寻找,#2指向了#26,#27指向了#18:#7
- 在Constant pool中继续寻找,#26指向了com/example/jvm/DynamicLinkingTest,#18:#7 分别是methodA和()V(void)
动态链接
- 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接
- 如上,每个Class文件中都包含了大量的符号引用,动态链接会将这些符号引用转换为直接引用
为什么要使用运行时常量池
不同的方法可能调用相同的常量或者方法,使用运行时常量池可以只保存一份数据,节省资源
方法返回地址
一个方法在执行后,有两种方式退出这个方法
- 正常返回,执行引擎遇到任意一个方法返回的字节码指令
- 异常返回,方法执行过程中遇到了异常,并且这个异常没有在方法体内得到妥善处理
正常退出时
字节码指令中会包含以下的某个指令,主调方法的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值
- ireturn:当返回值是boolean,byte,char,short和int类型时使用
- lreturn:Long类型
- freturn:Float类型
- dreturn:Double类型
- areturn:引用类型
- return:返回值类型为void的方法、实例初始化方法、类和接口的初始化方法
异常退出时
先看一段代码,再看对应的字节码,感受一下处理的方式
public class ErrorReturnTest {
public static void main(String[] args) {
ErrorReturnTest errorReturnTest = new ErrorReturnTest();
try {
errorReturnTest.methodA();
} catch (Exception e) {
System.out.println("error");
}
}
public void methodA() {
int i = 1;
int j = 0;
int k = i / j;
}
}
复制代码
对应字节码
➜ jvm javap -v ErrorReturnTest.class
.......
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 #2 // class com/example/jvm/ErrorReturnTest
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method methodA:()V
12: goto 24
15: astore_2
16: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
19: ldc #7 // String error
21: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
24: return
Exception table:
from to target type
8 12 15 Class java/lang/Exception
LineNumberTable:
line 7: 0
line 9: 8
line 12: 12
line 10: 15
line 11: 16
line 14: 24
LocalVariableTable:
Start Length Slot Name Signature
16 8 2 e Ljava/lang/Exception;
0 25 0 args [Ljava/lang/String;
8 17 1 errorReturnTest Lcom/example/jvm/ErrorReturnTest;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 15
locals = [ class "[Ljava/lang/String;", class com/example/jvm/ErrorReturnTest ]
stack = [ class java/lang/Exception ]
frame_type = 8 /* same */
......
SourceFile: "ErrorReturnTest.java"
复制代码
可以在字节码中看到
Exception table:
from to target type
8 12 15 Class java/lang/Exception
复制代码
表示8到12行的字节码指令,跳转到15行进行处理,处理的类型是java/lang/Exception
一些附加信息
某些虚拟机实现会增加一些规范里没有描述的信息到栈帧之中,例如与调试、 性能收集相关的信息
参考资料
《深入理解java虚拟机》
尚硅谷视频