Java虚拟机运行时栈帧结构--《深入理解Java虚拟机》学习笔记及个人理解(二)

时间:2022-01-19 03:38:50

Java虚拟机运行时栈帧结构(周志明书上P237页)

栈帧是什么?

栈帧是一种数据结构,用于虚拟机进行方法的调用和执行。

栈帧是虚拟机栈的栈元素,也就是入栈和出栈的一个单元。


2018.1.2更新(在网上看到一个更好的解释):

栈帧(Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接 (Dynamic Linking)、方法返回值和异常分派(Dispatch Exception)。

栈帧在什么地方?

内存 -> 运行时数据区 -> 某个线程对应的虚拟机栈 -> 这里就是栈帧了

栈帧的含义?

每个方法的执行和结束对应着栈帧的入栈和出栈。

入栈表示被调用,出栈表示执行完毕或者返回异常。

一个虚拟机栈对应一个线程,当前CPU调度的那个线程叫做活动线程;一个栈帧对应一个方法,活动线程的虚拟机栈里最顶部的栈帧代表了当前正在执行的方法,而这个栈帧也被叫做‘当前栈帧’。

栈帧既然是个数据结构,都有哪些数据?

局部变量表、操作数栈、动态链接、方法返回地址、附加信息。

栈帧的大小是什么时候确定的?

编译程序代码的时候,就已经确定了局部变量表和操作数栈的大小,而且在方法表的Code属性中写好了。不会受到运行期数据的影响。

什么是局部变量表

是一片逻辑连续的内存空间,最小单位是Slot,用来存放方法参数和方法内部定义的局部变量。我觉得可以想成Slot数组....JVMS7:“any parameters are passed in consecutive local variables starting from local variable 0”

虚拟机没有明确指明一个Slot的内存空间大小。但是boolean、byte、char、short、int、float、reference、returnAddress类型的数据都可以用32位空间或更小的内存来存放。这些类型占用一个Slot。Java中的long和double类型是64位,占用两个Slot。(只有double和long是jvms里明确规定的64位数据类型)

虚拟机如何调用这个局部变量表?

局部变量表是有索引的,就像数组一样。从0开始,到表的最大索引,也就是Slot的数量-1。

要注意的是,方法参数的个数 + 局部变量的个数 ≠ Slot的数量。因为Slot的空间是可以复用的,当pc计数器的值已经超出了某个变量的作用域时,下一个变量不必使用新的Slot空间,可以去覆盖前面那个空间。(这部分内容在P183页)

特别地,JVMS7:

On instance method invocation, local variable 0 is always used to pass a reference to the object on which the instance method is being invoked (this in the Java programming language)

手动翻译:在一个实例方法的调用时,局部变量表的第0位是一个指向当前对象的引用,也就是Java里的this。

局部变量表Slot复用对垃圾收的影响(书上的P239页)

先了解一下System.gc()机制:

public class Main{
public static void main(String [] args){
byte[] placeholder = new byte[64*1024*1024];
System.gc();
}
}

Java虚拟机运行时栈帧结构--《深入理解Java虚拟机》学习笔记及个人理解(二)

对于上面dos输出的结果,我是这样理解的:

第一行,Allocation Filure(空间分配失败)引起了Minor GC。因为创建的对象太大,新生代装不下,所以进行了一次GC。

第二行,由于新生代GC完了后,还是装不下,这时就应该把它直接放到老年代,为了老年代又足够的空间来迎接这个大对象,所以老年代进行一次Full GC。

第三行,是代码中的手动gc,发现这次手动gc并没有回收掉这个大对象。因为,placeholder这个对象,还在作用域....就不该回收....


这回System.gc()该回收掉placeholder了吧?

public class Main{
public static void main(String [] args){
{
byte[] placeholder = new byte[64*1024*1024];
}
System.gc();
}
}

Java虚拟机运行时栈帧结构--《深入理解Java虚拟机》学习笔记及个人理解(二)

要不是回收时间不一样...还真看不出什么区别...

明显,还是没有回收掉这个placeholder大对象。

为什么呢?

因为虚拟机并不急着让placeholder回收掉,因为,在我这个程序中,对虚拟机来说,回不回收placeholder,对内存没有丝毫影响,剩余的空间一样都是浪费(空闲)着,回收了反倒还浪费时间。


这样做才能成功回收:

public class Main{
public static void main(String [] args){
{
byte[] placeholder = new byte[64*1024*1024];
}
int a = 0;
System.gc();
}
}

Java虚拟机运行时栈帧结构--《深入理解Java虚拟机》学习笔记及个人理解(二)

其实服用之前,虽然placeholder退出了作用域,但是虚拟机并没有做什么事,只是知道pc指针已经超出了placeholder的作用域,知道placeholder过期了。所以placeholder仍保持者GC Roots之间的关联。

当a=0复用了前面对象的空间时,就打断了GC Roots与局部变量表中的placeholder之间的关联。因为a复用了这片空间(虽然只是用了一小部分)。此时GC Root无法达到placeholder对象,满足回收条件。

然后System.gc()就成功回收了。


也就是说在复用之前并不会判定为‘垃圾’,在复用后才会被判定为‘垃圾’。刚才使用一个int a来复用,这个复用看起来很轻量。

如果使用一个新的大对象来复用,那么GC是如何发生的呢?看下面代码:

public class Main{
public static void main(String [] args)throws InterruptedException{
{
byte[] placeholder = new byte[64*1024*1024];
}
byte[]arr= new byte[20*1024*1024];
System.gc();
}
} 

Java虚拟机运行时栈帧结构--《深入理解Java虚拟机》学习笔记及个人理解(二)

解读dos下的输出:

第一行,因为即将创建的placeholder太大,新生代装不下,所以进行一次GC。

第二行, 因为GC之后还是装不下placeholder,所以把这个大对象直接放进老年代里。迎接这个大对象之前,先清一清自己的空间(Full GC),怕自己装不下。

第三行,因为即将创建的arr太大,新生代装不下,所以进行一次GC。

第四行,因为GC之后还是装不下arr, 所以把这个大对象直接放进老年代里。迎接这个大对象之前,先清一清自己的空间(Full GC),怕自己装不下。

但是,可以看到这一次Full GC并没有把placeholder清理掉,因为还没开始复用呢。

随后创建好了arr, 也就是复用了placeholder的空间。这时才把placeholder判定为垃圾。

第五行,是代码里手写的System.gc()方法。这时把placeholder这个垃圾清理掉。


有没有发现这个Full GC来的不是很恰到好处?因为没有及时清理掉placeholder。

为什么没有清理掉呢?因为局部变量表里的placeholder数据还和GC Root连着,导致没有判定它为垃圾。

能不能及时断开这个连接,让这个Full GC起到它该起的作用呢?

可以巧用null来解决,看下面代码:

public class Main{
public static void main(String [] args)throws InterruptedException{
{
byte[] placeholder = new byte[64*1024*1024];
placeholder = null;
}
byte[]arr= new byte[20*1024*1024];
System.gc();
}
}  

Java虚拟机运行时栈帧结构--《深入理解Java虚拟机》学习笔记及个人理解(二)

解读dos下的输出:

第一行,因为即将创建的placeholder太大,新生代装不下,所以进行一次GC。

第二行, 因为GC之后还是装不下placeholder,所以把这个大对象直接放进老年代里。迎接这个大对象之前,先清一清自己的空间(Full GC),怕自己装不下。

随后placeholder= null;

第三行,因为即将创建的arr太大,新生代装不下,所以进行一次GC。

第四行,因为GC之后还是装不下arr, 所以把这个大对象直接放进老年代里。迎接这个大对象之前,先清一清自己的空间(Full GC),怕自己装不下。

可以看到这一次Full GC把placeholder清理掉了。

随后创建好了arr,复用了placeholder。

第五行,是代码里手写的System.gc()方法。

什么事

什么是操作数栈(参考JVMS7)

Each frame (§2.6) contains a last-in-first-out (LIFO) stack known as its operand stack.

翻译:每个栈帧都包含一个被叫做操作数栈的后进先出的栈。叫操作栈,或者操作数栈。

Where it is clear by context, we will sometimes refer to the operand stack of the current frame as simply the operand stack.

翻译:通常情况下,操作数栈指的就是当前栈桢的操作数栈。

操作数栈有什么用?

The operand stack is empty when the frame that contains it is created. The Java virtual machine supplies instructions to load constants or values from local variables or fields onto the operand stack. Other Java virtual machine instructions take operands from the operand stack, operate on them, and push the result back onto the operand stack. The operand stack is also used to prepare parameters to be passed to methods and to receive method results.

翻译+归纳:

1.栈桢刚创建时,里面的操作数栈是空的。

2.Java虚拟机提供指令来让操作数栈对一些数据进行入栈操作,比如可以把局部变量表里的数据、实例的字段等数据入栈。

3.同时也有指令来支持出栈操作。

4.向其他方法传参的参数,也存在操作数栈中。

5.其他方法返回的结果,返回时存在操作数栈中。

操作数栈本身就是一个普通的栈吗?

其实栈就是栈,再加上数据结构所支持的一些指令和操作。

但是,这里的栈也是有约束的。

操作数栈是区分类型的,操作数栈中严格区分类型,而且指令和类型也好严格匹配。

栈桢和栈桢是完全独立的吗?

本来栈桢作为虚拟机栈的一个单元,应该是栈桢之间完全独立的。

但是,虚拟机进行了一些优化:为了避免过多的 方法间参数的复制传递、方法返回值的复制传递 等一些操作,就让一部分数据进行栈桢间共享。

什么是动态链接?

一个方法调用另一个方法,或者一个类使用另一个类的成员变量时,总得知道被调用者的名字吧?(你可以不认识它本身,但调用它就需要知道他的名字)。符号引用就相当于名字,这些被调用者的名字就存放在Java字节码文件里。

名字是知道了,但是Java真正运行起来的时候,真的能靠这个名字(符号引用)就能找到相应的类和方法吗?

需要解析成相应的直接引用,利用直接引用来准确地找到。

举个例子,就相当于我在0X0300H这个地址存入了一个数526,为了方便编程,我把这个给这个地址起了个别名叫A, 以后我编程的时候(运行之前)可以用别名A来暗示访问这个空间的数据,但其实程序运行起来后,实质上还是去寻找0X0300H这片空间来获取526这个数据的。

这样的符号引用和直接引用在运行时进行解析和链接的过程,叫动态链接。

动态链接的前提

每一个栈帧内部都要包含一个指向运行时常量池的引用,来支持动态链接的实现。

附加信息

JVMS里没看到啊....但是书里提了,然后说"JVM里没有明文规定"....

方法返回地址

方法正常调用完成

返回一个值给调用它的方法,方法正常完成发生在一个方法执行过程 中遇到了方法返回的字节码指令(§2.11.8)的时候,使用哪种返回指令取决于方法返回值的数 据类型(如果有返回值的话)。

JVMS7中的2.6.4 Normal Method Invocation Completion中写道:

This occurs when the invoked method executes one of the return instructions (§2.11.8), the choice of which must be appropriate for the type of the value being returned (if any).

手动翻译+理解:Java虚拟机根据不同数据类型有不同的底层return指令。当被调用方法执行某条return指令时,会选择相应的return指令来让值返回(如果该方法有返回值的话)。

The current frame (§2.6) is used in this case to restore the state of the invoker, including its local variables and operand stack, with the program counter of the invoker appropriately incremented to skip past the method invocation instruction. Execution then continues normally in the invoking method's frame with the returned value (if any) pushed onto the operand stack of that frame.

手动翻译:在这种情况,当前栈桢就被用来恢复调用者的状态,都恢复哪些呢?恢复局部变量表、操作数栈 和 程序计数器(pc指针),而这个程序计数器要适当地增加,来指向下一条指令(也就是调用函数的下一句)。使调用者方法能够正常地继续执行下去,而且返回值push到了调用方法的操作数栈中。

方法异常调用完成

异常时不会返回值给调用者。

未完待续。。。这方面我再学习学习。。

参考博客:

英中繁簡編程術語對照http://www.moon-soft.com/doc/30155.htm

http://blog.csdn.net/dd864140130/article/details/49515403

http://blog.csdn.net/u013678930/article/details/51980460

https://www.zhihu.com/question/29056872

https://segmentfault.com/a/1190000010648021

http://blog.csdn.net/captian_900331/article/details/52512204

https://www.zhihu.com/question/53822079/answer/136699108

http://hllvm.group.iteye.com/group/topic/33366

http://blog.csdn.net/renfufei/article/details/49230943

http://blog.csdn.net/newhappy2008/article/details/7596027