作者:吕宗胜
在JVM的内存管理中,我们知道每个私有线程都有各自的程序计数器、虚拟机栈和本地方法栈。这里我们不再区分虚拟机栈和本地方法栈,但从这些命名中,我们也可以窥见一斑,JVM基于栈的执行引擎。
在了解JVM的执行原理之前,我们先了解一下栈帧的概率。栈帧(Stack Frame)是支持虚拟机进行方法调用和执行的数据结构,每一个方法对应于一个栈帧,栈帧存储于虚拟机栈中。栈帧中又可以细分为局部变量表、操作数栈、动态连接和方法返回地址。
我相信对于栈的数据结构大家都会很熟悉,是一个先进后出的数据结构。对于JVM来说,一个栈帧从入栈到出栈就代表着一个方法的开始执行到执行结束的过程。
局部变量表用于存储方法参数和方法内定义的局部变量。这些变量都是线程私有和方法私有的。在JVM中,局部变量表中变量存储的最小单位是变量槽(Slot),一般来说,一个变量槽大小是32位,可以存储boolean, byte, char, short, int ,float, reference, returenAddress,而long和double则会使用2个slot来存储。
虚拟机中是通过索引定位的方式来使用局部变量表的。索引的范围是0~slot的最大值。但0默认存储是该栈帧所对应的方法所属实例的引用,从1开始按方法参数顺序存储参数,接下去是方法内的局部变量。如果变量是在32位以内,索引对应的slot就对应了该变量,否则,则索引对应的slot和连续的下一个slot为一个变量。
为了节约空间,JVM允许对那些已经不再使用的Slot进行复用。但是复用有时候也会产生一定的垃圾回收问题。我们知道JVM垃圾回收可以采用可达性分析来判断对象的存活与否,由于Slot的复用问题,可能会存在Slot中的变量已经不再使用但未清理,导致垃圾也无法回收的问题。
操作数栈是虚拟机栈中用于进行字节码执行的数据结构。在方法执行过程中,各种字节码指令不断的往操作数栈进行数据的写入和提取,以完成方法的执行。JVM是基于栈的执行引擎,其中所指的栈就是操作数栈,稍后会具体分析操作数栈的执行过程。
在类的加载过程中,有一部分符号引用转化成直接引用,这部分被称之为静态解析,而另外一部分就是在运行期间转化成直接引用,被称之为动态连接。
方法的退出包括两部分:一部分是遇到方法正常结束的指令退出;另一部分是于方法遇到异常的退出。方法在退出之后,都需要返回到该方法被调用的位置。正如下面所说,方法的退出对应于栈帧的出栈过程,大概的操作有:恢复上层方法的局部变量表和操作数栈,把该方法返回值压入调用者的操作数栈,调用程序计数器执行后面的指令。
在开始介绍JVM基于栈的解释器执行之前,我们先简单的看看基于栈的指令集和基于寄存器的指令集之间的区别。对于这两种不同的指令集,我们只能说各有利弊。对于基于栈的指令集来说,最大的优势是可移植,因为它的实现不依赖硬件,不足之处在于其运行效率相对于寄存器指令集来说会慢一点;而寄存器指令集的优缺点则刚好与基于栈的指令集相反。
下面我们通过一个例子来实际讲解一下JVM中基于栈的执行过程(参考《深入理解JVM虚拟机》)
public int calc(){
int a=100;
int b=200;
int c=300;
return (a+b)*c
}
//这个简单的方法对于JVM来说,就是一个栈帧,它的执行就代表着该栈帧从入栈到出栈的过程。
//它转化成相对应的字节码如下
public int calc();
Code:
Stack=2, Local=4, Args_size=1
0: bipush 100
2: istore_1
3: sipush 200
6: istore_2
7: sipush 300
10: istore_3
11: iload_1
12: iload_2
13: iadd
14: iload_3
15: imul
16: ireturn
根据这份字节码,我们来看看这段字节码的大概执行过程。
这里的bipush和sipush把变量压入操作数栈,istore把变量弹出栈并存入局部变量表,istore_1表示把变量压入局部变量表的第一个slot。所以从第3幅图我们知道所有的变量都进行了局部变量表。iload变量把局部变量表中的数据从新压入操作数栈,iadd则把操作数栈的两个栈顶数据出栈相加,并把加过压入栈,其他的操作一次类推。
上面只是最简单的解释了JVM的基于栈的执行过程,实际的过程会远比这个复杂更多。
本文来自网易实践者社区,经作者吕宗胜授权发布