0%

深入理解运行时栈帧结构 | 栈帧,操作数栈,本地变量表

2、JVM运行时数据区是如何工作的 这节中,我们已经见过运行时栈帧结构的面貌了,现在我们再来深入的了解一下有关它的故事。

1、栈帧结构

我们再拿出这张图来看看,不过这次我们是要更深入的来了解下了。

image-20200107224256906

假设有如下调用关系:ClassA.invokeA() -> ClassB.invokeB() -> ClassB.doInvokeB() -> ClassC.execute(),则会生成以上的虚拟机栈,每个方法调用,都有一个栈帧入栈,调用完成,栈帧从虚拟机栈上出栈;虚拟机栈是一个LIFO的栈。

栈帧

每个栈帧都包括局部变量表、操作数栈、动态链接、方法会地址和其他的附加信息。

1.1、局部变量表

变量操:Slot,最小单位。

一个本地变量(Slot)可以存32位以内的数据,可以保存类型为 int, short, reference, byte, char, floath和returnAddress的数据,两个本地变量可以保存类型为long和double的数据;

**线程安全问题:**局部变量表建立在线程的堆栈上面,线程私有的,无论读写两个连续的Slot是否为原子操作,都不会引起数据安全问题。

何时确定栈帧需要多大内存空间?

在程序编译完成生成class文件之后,其实就已经确定了局部变量表的大小,以及操作数栈的大小。

以这个例子为例:Class文件16进制背后的秘密#3、解析Class文件实例

编译为class文件之后,通过反汇编指令,即可查看到对应的Java汇编指令,其中init方法的指令如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 类中自定义的init方法
public void init(java.lang.String);
descriptor: (Ljava/lang/String;)V
// 访问标记
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=2
0: ldc #3 // String test method 将常量”test method“的在常量池中的索引压入栈
2: astore_2 // 从栈中取出刚刚的索引,存储到本地变量表的tmp中
3: aload_0 // this引用入栈
4: iconst_1 // 数值1入栈
5: putfield #2 // Field a:I 数值1出栈,赋值给 常量池#2,这里是一个Fieldref,对应代码中的a变量
8: return
LineNumberTable:
line 9: 0
line 10: 3
line 11: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/itzhai/classes/TestA;
0 9 1 title Ljava/lang/String;
3 6 2 tmp Ljava/lang/String;

可见,编译为class文件之后,这些内容都确定了:

  • 操作数栈大小;
  • 本地变量表大小;

至于线程执行的时候,创建对象,都是在堆中分配的,栈帧指针保存了对象的引用,引用大小是固定的。

slot复用问题引出的:不使用的对象应该手动赋值为null?

如下代码,我们在作用域外执行gc,启动参数添加-verbose:gc:

1
2
3
4
5
6
7
public static void main(String[] args) {
{
byte[] placeholder = new byte[64 * 1024 * 1024];
}
// int a = 1; // 新加一个赋值操作
System.gc();
}

执行结果:

1
2
[GC (System.gc())  69530K->66344K(125952K), 0.0012475 secs]
[Full GC (System.gc()) 66344K->66223K(125952K), 0.0070326 secs]

发现局部变量表应用的对象并没有被回收。

我们取消上面的注释,在执行,结果:

1
2
[GC (System.gc())  69529K->66330K(125952K), 0.0132218 secs]
[Full GC (System.gc()) 66330K->687K(125952K), 0.0267554 secs]

发现回收了。

或者我们不取消注释,在启动参数添加参数:

-verbose:gc -Xcomp

发现也可以正常回收:

1
2
[GC (System.gc())  70861K->66360K(125952K), 0.0031858 secs]
[Full GC (System.gc()) 66360K->741K(125952K), 0.0103660 secs]

这是因为JIT编译优化的结果。

即使离开了方法的作用域,方法里面局部变量表里面的Slot没有被其他变量复用,它仍然是一个GC Root,不会作为垃圾被回收。

类变量有零值而局部变量无零值

局部变量表不想类变量有准备阶段赋值初始化,如果没有显示的在方法中给局部变量赋值,是读取不了变量的。

1.2、操作数栈

1.3、动态链接

1.4、方法返回地址

欢迎关注我的其它发布渠道