在之前的章节中我们讲解了jvm的内存分配和管理,class的文件结构,就差之行了。那么从第十一章开始我们就开始讲java虚拟机是如何执行一个class文件的。
首先我们应该明确虚拟机是区别于物理机的一种说法,物理机的执行引擎是建立在处理器,硬件 ,指令集之上的。而我们的虚拟机则由自己实现。在虚拟机中大致分为两种执行方式:解释执行和编译执行。
我们之前讲过,虚拟机运行方法的时候运行在java虚拟机栈里面,里面的存储结构是栈帧,需要了解一个虚拟机如何运行字节码文件的,首先我们需要了解一个栈帧的结构。
栈帧:
栈帧作为一个方法的代替者(在执行时)它肯定需要有方法的所有特征。那么我们在方法中定义的变量和方法传递的参数是不能缺少的。这一部分在栈帧中叫做局部变量表,其次每一个+-*/和其余的各种赋值等操作还需要占有一个区域,这个区域叫做操作数栈,在运行的过程中我们仍然有可能调用了其他的方法,这个时候我们需要一个类似于指针的引用去寻找方法的入口,但是我们直到在jvm中所有的引用都是用符号引用实现的,所以我们必须还需要一个动态链接的部分在运行时动态的把符号引用转成内存地址。剩下的还有返回信息对应了一个方法返回地址。所以我们得到了如下的一种数据结构,成为栈帧。
接下来我们来详细讲解每一部分:
局部变量表:
主要用于存放方法参数和方法内部定义的局部变量。我们之前分析过方法表,方法表中Code属性有一个参数是max_locals就定义了这个方法所需要分配的局部变量表的最大值。
局部变量表使用Slot作为最小单位。jvm中说一个Slot应该能存放一个boolean ,byte,char,short,int,float,reference,returnAddress类型的数据,所以Slot并没有一个固定的大小,随着上述类型的数据的长度不同,Slot的长度也不同。在Jvm中上述数据类型占用32位,所以Slot也占用了32位。如果遇到64位的数据类型Slot选择以高位对齐的方式分配两个连续的Slot空间。在执行的时候使用索引来寻找Slot,Slot同样也是可以复用的,因为一个Slot有可能不占用整个方法的生命周期,但是这种复用会影响GC。我们举个例子:
public class SlotGCTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
byte[] holder = new byte[64*1024*1024];
System.gc();
}
}
使用上面的代码进行垃圾回收,应该符合我们的预期这64mb的内存不会被回收,因为依然可以通过GCROOT找到。使用-verbose:gc参数输出结果如下:
[GC (System.gc()) 67502K->65976K(188416K), 0.0007246 secs]
[Full GC (System.gc()) 65976K->65811K(188416K), 0.0039451 secs]
但是我们把gc的代码放在方法外呢?
public class SlotGCTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
{
System.out.println("构造代码块");
byte[] holder = new byte[64*1024*1024];
}
System.out.println("main");
System.gc();
}
}
我们从上述代码中可以看出,我们gc的位置已经在构造代码块之外了,GCROOT应该找不到holder了,这64mb的内存必定会被回收。输出如下:
构造代码块
main
[GC (System.gc()) 67502K->65928K(188416K), 0.0018770 secs]
[Full GC (System.gc()) 65928K->65811K(188416K), 0.0057137 secs]
我就问你蒙蔽吗?为什么还是没有回收呢?这就是Slot复用对GC的影响了,我们稍微加一行代码
public class SlotGCTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
{
System.out.println("构造代码块");
byte[] holder = new byte[64*1024*1024];
}
int a = 0;
System.out.println("main");
System.gc();
}
}
我们只加了int a = 0 ;这一行代码。最后输出如下:
构造代码块
main
[GC (System.gc()) 67502K->65992K(188416K), 0.0011489 secs]
[Full GC (System.gc()) 65992K->275K(188416K), 0.0038030 secs]
你会发现在Full GC中居然回收了。这就是因为虽然64mb的Slot已经离开了作用域但是Slot没有被覆盖,所以GCROOT中的局部变量表依然保存着holder的关联。所以我们有一个置空的说法,即holder = null;
在我们生成类变量的时候,会先在准备阶段赋值一次,在初始化的时候第二次赋值所以类变量直接int a而不赋值是可行的。
如果是局部变量不赋值,结果如下:
局部变量不赋值会报错。
操作数栈:
操作数栈是一个标准的栈,遵循后入先出的原则,和上一个参数,局部变量表一样,它的栈深也需要存在Code属性的max_stacks数据相中,操作数栈的元素可以是任意的Java数据类型。我们举个例子a++;在栈中是这样操作的。
动态链接:
链接分为两种,静态链接和动态链接,在编译期可知,运行期不变的属于静态链接,主要有static方法,私有方法,实例构造器和父类方法。动态链接因为不能确定具体调用的方法地址,所以往往要到执行时才会转换成内存,关于动态链接的具体内容因为篇幅问题我们之后会单独讲。
方法返回地址:
一个方法的结束只有两个情况,一个是return了(或执行完毕),另一种情况是遇到异常了,还throw出去了。一个方法结束之后肯定要返回之前调用方法的位置,所以我们需要记录一些信息,来存储调用位置的执行状态。一般情况下本方法执行完毕后,恢复调用方法的栈帧,并且压入返回值到操作数栈(如果有的话),最后把PC+1.