前言
本文主要介绍JVM运行时,内存中数据的布局情况,主要是介绍JVM栈的状态更迭。
本文会通过查看一个类文件编译后的字节码文件,来分析当一个方法执行的时候,JVM中的栈空间所发生的变化。
JVM内存的整体布局
当JVM运行时,内存主要由程序计数器
(Program Counter),栈
(stack),直接内存区
(Direct Memory),方法区
(Method Area),堆
(Heap)。
栈又可以分成两部分,分别是JVM Stack
和Native Method Stack
。
每个线程都有自己独立的程序计数器和栈,同时共享方法区和堆,具体情况如下图所示:
上图中需要我们注意的重点是:
每个线程有自己独立的栈空间。栈空间中的数据都是当前线程独有的。
JVM中的栈空间
JVM中每个线程的栈分为两部分,分别是JVM Stack和Native Method Stack。
由于Native Method Stack涉及到C和C++的知识,超出本文的范围,所以这里不做讨论,只讨论JVM Stack的情况。
当讨论JVM 栈的时候,就不得不说另一个概念——栈帧
。
栈帧
栈里的每一个元素就是一个栈帧
(Stack Frame),一个方法的调用就会产生一个栈帧。
先介绍一下栈帧的内部结构。
栈帧的内部结构
栈帧内部其实分成了以下四个部分,
Local Variables:记录数据和中间结果。
Operand Stack:操作数栈,每个栈帧内部还存在一个栈,这个栈中存放的是执行操作需要的数据。
Dynamic Linking:栈中指向常量池的引用,如果常量池中的对象没有被解析就动态解析,如果被解析了就直接拿过来用。有点类似于ClassLoader中Linking步骤的resolusion执行的操作
Return Address:指生成当前栈帧的方法,执行完毕后应该去哪个地址接着执行。
字节码与栈动态
首先我们有下面这段java源代码:
public class ByteCodeDemo {
public static void main(String[] args) {
int i = 1;
i = i++;
System.out.println(i);
}
}
上面的源代码编译后的字节码文件如下:
这段代码只有一个方法,就是main
方法,所以只有一个栈帧。
先来看看栈帧中的Local Variables Table
中的内容:
可以看到,在本地变量表中有两个数据,索引为0
的变量是方法的参数args
,索引为1
的是方法内声明的变量i
。
接下来我们通过字节码文件来演示Operand Stack
中的变化。
-
当方法刚开始执行的时候,执行数栈是一个空栈。
-
执行字节码中的第一行命令
iconst_0
后,常量值0添加到栈中。
-
执行第二条命令时
istore_1
,是指将栈顶元素弹出,把该元素的值赋予本地变量表中索引为1的变量,在示例代码中,就是把0赋值给i。 -
执行第三条命令,也就是
iload_1
,是指将局部变量表中索引为1的变量值加入到操作数栈中,也就是将0压到栈中。 -
执行
iinc 1 by 1
,是指将局部变量表中索引为1的变量,自增1次。也就是在局部变量表中在索引1位置上的0,变为1,注意,此时操作数栈中的元素值仍是0。 -
再次执行
istore_1
,将栈顶元素弹出,把该元素的值赋予本地变量表中索引为1的变量。 -
接下来就是执行打印语句了,这里就不在进行介绍。
所以最后的输出的i的值是0,而不是1。
补充:如果一个方法有返回值的,会将返回值放在该方法的调用方法栈帧中的操作数栈的栈顶。调用方法再调用pop()方法就能拿到该方法的返回值。
总结
本文首先从整体来介绍了JVM运行时,内存中各组件的分配布局,包括线程独有的栈空间和程序计数器,以及线程共享的方法区与堆空间。
随后通过一段java代码编译后生成的字节码,介绍了JVM运行时,栈空间中的各部件的状态演变,主要是本地变量表与操作数栈之间的交互与演变。