在 2、JVM运行时数据区是如何工作的 这节中,我们已经见过运行时栈帧结构的面貌了,现在我们再来深入的了解一下有关它的故事。
1、栈帧结构
我们再拿出这张图来看看,不过这次我们是要更深入的来了解下了。
假设有如下调用关系: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 | // 类中自定义的init方法 |
可见,编译为class文件之后,这些内容都确定了:
- 操作数栈大小;
- 本地变量表大小;
至于线程执行的时候,创建对象,都是在堆中分配的,栈帧指针保存了对象的引用,引用大小是固定的。
slot复用问题引出的:不使用的对象应该手动赋值为null?
如下代码,我们在作用域外执行gc,启动参数添加-verbose:gc:
1 | public static void main(String[] args) { |
执行结果:
1 | [GC (System.gc()) 69530K->66344K(125952K), 0.0012475 secs] |
发现局部变量表应用的对象并没有被回收。
我们取消上面的注释,在执行,结果:
1 | [GC (System.gc()) 69529K->66330K(125952K), 0.0132218 secs] |
发现回收了。
或者我们不取消注释,在启动参数添加参数:
-verbose:gc -Xcomp
发现也可以正常回收:
1 | [GC (System.gc()) 70861K->66360K(125952K), 0.0031858 secs] |
这是因为JIT编译优化的结果。
即使离开了方法的作用域,方法里面局部变量表里面的Slot没有被其他变量复用,它仍然是一个GC Root,不会作为垃圾被回收。
类变量有零值而局部变量无零值
局部变量表不想类变量有准备阶段赋值初始化,如果没有显示的在方法中给局部变量赋值,是读取不了变量的。