前面我们讲栈虚拟机和寄存器虚拟机的时候,提到过栈帧结构;在讲字节码指令的时候,又提到了栈帧中的操作数栈,那今天我们就来详细地讲一讲 JVM 的栈帧结构,好让大家对栈帧有一个更加清晰的认知。
我们从下面这幅图开始讲起。
Java 的源码文件经过编译器编译后会生成字节码文件,然后由 JVM 的类加载器进行加载,再交给执行引擎执行。在执行过程中,JVM 会划出一块内存空间来存储程序执行期间所需要用到的数据,这块空间一般被称为运行时数据区。
栈帧(Stack Frame)是运行时数据区中用于支持虚拟机进行方法调用和方法执行的数据结构。每一个方法从调用开始到执行完成,都对应着一个栈帧在虚拟机栈/本地方法栈里从入栈到出栈的过程。
本地方法,也就是 native 方法,我们前面有详细地讲过,由 C/C++ 实现。
每一个栈帧都包括了局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息。
在编译程序代码时,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的 Code 属性之中。
方法表、局部变量表我们在讲字节码的时候有讲过,可以戳链接再回头看一下,这篇内容也会继续盘一盘。
一个线程中的方法调用链可能会很长,很多方法都处于执行状态。在当前线程中,位于栈顶的栈帧被称为当前栈帧(Current Stack Frame),与这个栈帧相关联的方法成为当前方法。执行引擎运行的所有字节码指令都是对当前栈帧进行操作,在概念模型上,栈帧的结构如下图所示:
局部变量表
局部变量表(Local Variables Table)用来保存方法中的局部变量,以及方法参数。当 Java 源代码文件被编译成 class 文件的时候,局部变量表的最大容量就已经确定了。
我们来看这样一段代码。
public class LocalVaraiablesTable {
private void write(int age) {
String name = "沉默王二";
}
}
write()
方法有一个参数 age,一个局部变量 name。
然后用 Intellij IDEA 的 jclasslib 查看一下编译后的字节码文件 LocalVaraiablesTable.class。可以看到 write()
方法的 Code 属性中,Maximum local variables(局部变量表的最大容量)的值为 3。
按理说,局部变量表的最大容量应该为 2 才对,一个 age,一个 name,为什么是 3 呢?
当一个成员方法(非静态方法)被调用时,第 0 个变量其实是调用这个成员方法的对象引用,也就是那个大名鼎鼎的 this。调用方法 write(18)
,实际上是调用 write(this, 18)
。
点开 Code 属性,查看 LocalVaraiableTable 就可以看到详细的信息了。
第 0 个是 this,类型为 LocalVaraiablesTable 对象;第 1 个是方法参数 age,类型为整型 int;第 2 个是方法内部的局部变量 name,类型为字符串 String。
当然了,局部变量表的大小并不是方法中所有局部变量的数量之和,它与变量的类型和变量的作用域有关。当一个局部变量的作用域结束了,它占用的局部变量表中的位置就被接下来的局部变量取代了。
来看下面这段代码。
public static void method() {
// ①
if (true) {
// ②
String name = "沉默王二";
}
// ③
if(true) {
// ④
int age = 18;
}
// ⑤
}
method()
方法的局部变量表大小为 1,因为是静态方法,所以不需要添加 this 作为局部变量表的第一个元素;- ②的时候局部变量有一个 name,局部变量表的大小变为 1;
- ③的时候 name 变量的作用域结束;
- ④的时候局部变量有一个 age,局部变量表的大小为 1;
- ⑤的时候局 age 变量的作用域结束;
关于局部变量的作用域,《Effective Java》 中的第 57 条建议:
将局部变量的作用域最小化,可以增强代码的可读性和可维护性,并降低出错的可能性。
在此,我还有一点要提醒大家。为了尽可能节省栈帧耗用的内存空间,局部变量表中的槽是可以重用的,就像 method()
方法演示的那样,这就意味着,合理的作用域有助于提高程序的性能。是不是很有
真诚点赞 诚不我欺
回复