「JVM 执行引擎」 虚拟机栈的栈帧结构

虚拟机是一个相对于物理机的概念,两种机器都有代码执行能力;
物理机的执行引擎是直接建立在处理器缓存指令集操作系统层面上的;

虚拟机的执行引擎则是由软件自行实现的,可以不受物理条件制约地定制指令集执行引擎的结构体系,能够执行一些不被硬件直接支持的指令集格式;

《Java 虚拟机规范》中制定了 JVM 字节码执行引擎的概念模型,即执行引擎的统一外观(Facade);

执行引擎在执行字节码时,有解释执行(通过解释器执行,Sun Classic VM 只有解释器)和编译执行(通过即时编译器产生本地代码执行,BEA JRockit 只有即时编译器)两种;但对 JVM 外部而言,执行引擎的输入(字节码二进制流)、输出(执行结果)都是一致的;


方法是 JVM 的最基本执行单元,栈帧(Stack Frame)则是用于支持 JVM 进行方法调用和方法执行背后的数据结构,它也是 JVM Runtime AreaVirtual Machine Stack 的栈元素;

栈帧存储了方法的局部变量表操作数栈动态连接方法返回地址等信息;方法从调用开始至执行结束对应着一个栈帧在虚拟机栈中入栈到出栈的过程;

在前端编译阶段,栈帧的局部变量表,操作数栈所需的容量会被分析计算出来,并写入方法表的 Code 属性;一个栈帧所需内存大小不会受程序运行期变量数据的影响,仅仅取决于程序源码和 JVM 实现的栈内存布局;

对于一个线程的方法调用链,Java 程序的角度看可能是同一时间该线程所有方法都是在执行状态的,但对 JVM 执行引擎而言,只有栈顶的方法(当前栈帧)才是运行的;

当前栈帧Current Stack Frame),位于一个线程方法调用链所对应栈的栈顶的栈帧,也只有这个栈帧是线程中正在运行的;与之对应的方法被称为当前方法Current Method

请添加图片描述

1. 局部变量表

  • 局部变量表Local Variables Table)是用于存放方法参数、方法内部定义的局部变量的存储空间;Class 文件中方法的 Code 属性的 max_locals 数据项表示了局部变量表的最大容量;
  • 变量槽Variable Slot),局部变量表的最小单位;变量槽的内存空间由处理器、操作系统和 JVM 具体实现决定,但每个变量槽都应该能存放一个 booleanbytecharshortintfloatreferencereturnAddress 这 8 种类型的数据;

reference 表示对一个对象实例的引用,JVM 通过这个引用,直接或间接查找到对象在 Java Heap 中的数据存放起始地址或索引,并直接或间接查找到对象所属数据类型在 Method Area 中存放的类型信息;
returnAddress 为 jsr、jsr_w、ret 等字节码指令服务,指向一条字节码指令的地址,目前使用很少(曾用来处理异常的调整,现已被异常表替代);
longdouble 这样 64 位的数据类型,JVM 会以高位对齐的方式分配两个连续的变量槽,这里与 long 和 double 的非原子性协定 有些类似,不过是否原子操作在线程私有区域都不会引起数据竞争和线程安全问题;

JVM 通过索引(0 至变量槽容量)定位局部变量表中的变量,64 位的变量则通过首位变量槽定位;JVM 不允许任何单独访问 64 位数据的两个变量槽其中一个的行为,一旦遇到,在类加载阶段就会抛出异常;
实例方法的局部变量表第 0 位变量槽默认是传递方法的对象本身(this 变量);

局部变量表的变量槽可重用,超过作用域的变量所占用的槽位可以被其他变量重用;但这可能影响到 GC 行为;

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

运行结果

// -verbose:gc
[GC 66846K->65824K(125632K), 0.0032678 secs] [Full GC 65824K->65746K(125632K), 0.0064131 secs]

System.gc() 显示 GC 并没有回收掉 placeholder 的 64 MB 内存,因为此时变量 placeholder 还处于作用域之内;

// 局部变量表 Slot 复用对垃圾收集的影响
public static void main(String[] args)() {
    {
        byte[] placeholder = new byte[64 * 1024 * 1024];
    }
    System.gc();
}

运行结果

// -verbose:gc
[GC 66846K->65888K(125632K), 0.0009397 secs] [Full GC 65888K->65746K(125632K), 0.0051574 secs]

System.gc() 显示 GC 依旧没有回收掉 placeholder 的 64 MB 内存,因为局部变量表(作为 GC Roots 的一部分)中还存在对 placeholder 对象的引用(这是回收 placeholder 的关键条件);
此时若该方法的后续操作耗时很长,这 64 MB 内存可能会长时间无法回收(对象占用内存大、方法的栈帧长时间不能被回收、方法调用次数达不到即时编译器的编译条件),可以手动赋值 null 来优化;或者用其他变量来覆盖该变量槽;

// 局部变量表 Slot 复用对垃圾收集的影响
public static void main(String[] args)() {
    {
        byte[] placeholder = new byte[64 * 1024 * 1024];
    }
    int a = 0;
    System.gc();
}

运行结果

// -verbose:gc
[GC 66401K->65778K(125632K), 0.0035471 secs] [Full GC 65778K->218K(125632K), 0.0140596 secs]

placeholder 的 64 MB 内存被正确回收;

从编码角度讲,以恰当的变量作用域来控制变量回收时间才是最优雅的解决方法;
从执行角度讲,使用赋 null 操作来优化内存回收是建立在对字节码执行引擎概念模型的理解之上的,通过解释器执行时通常与概念模型还会比较接近,但经过即时编译器施加了各种编译优化措施后,即使不人为赋 null 值,placeholder 也会被正确回收;而且赋 null 值的操作在经过即时编译优化后几乎一定会被当作无效操作消除掉;而即时编译才是虚拟机执行代码的主要方式;

局部变量表中的局部变量不存在准备阶段,也就是不存在类似类变量在类加载的准备阶段赋零值的动作;如果一个局部变量定义了但没有赋初始值,那它是完全不能使用的;

// 未赋值的局部变量
public static void main(String[] args) {
    int a;
    System.out.println(a);
}

编译器会在编译期间提示异常,即便编译能通过,或者手动生成等效字节码,字节码校验阶段也会异常,从而导致类加载失败;

2. 操作数栈

操作数栈Operand Stack),一个后入先出(Last In First Out,LIFO)栈,栈元素是包括 double 和 long 在内的任意 Java 数据类型,32 位数据类型占一个栈容量,64 为数据类型占 3 个栈容量;最大深度由编译时写入到 Code 属性的 max_stacks 数据项;

通过各类字节码指令往操作数栈写入(入栈)和提取(出栈)内容,操作数栈中元素的类型必须保障与字节码指令的序列严格匹配(编译器会严格保证);

两个不同栈帧间(对应不同方法)的元素是完全独立的,但大多 JVM 的实现进行了优化,令两个栈帧出现一部分重叠(下面站着的部分操作数栈与上面栈帧的部分局部变量表重叠);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zzzn2eBd-1676084414974)(./images/Data%20Share%20between%20Stack%20Frame.png)]

3. 动态连接

每个栈帧都有一个指向运行时常量池中该栈帧对应的方法的引用,该引用用于支持方法调用过程中的动态连接Dynamic Linking);

  • 静态解析:部分符号引用在类加载阶段或第一次被调用时被转化为直接引用;
  • 动态连接:部分符号引用在每次运行期间都会被转化为直接引用;

4. 方法返回地址

方法退出的方式有两种:

  • 正常调用完成Normal Method Invocation Completion),执行引擎遇到任意方法返回的字节码指令,方法返回指令决定了是否有返回值、返回值类型,返回值将传递给上层的方法调用者;
  • 异常调用完成Abrupt Method Invocation Completion),方法执行过程中遇到异常,且该异常没有在方法体内得到妥善处理(在异常表没有搜索到匹配的异常处理器);这种退出方式将不会给上层调用者提供任何返回值;

方法正常退出时,根据主调方法的 PC 计数器的值找到退出的地址,返回到最初方法被调用的位置;
方法异常退出时,更加异常处理器确定退出的地址;
方法退出时,当前栈帧出栈,恢复上层方法的局部变量表和操作数栈,把返回值(若有)压入调用者栈帧的操作数栈,调整 PC 计数器的值执行方法调用指令后面的一条指令;

5. 附加信息

一些《Java 虚拟机规范》里没有描述的信息,如与调试、性能收集相关的信息(取决于 JVM 具体实现);
动态连接、方法返回地址、附加信息可统一归为栈帧信息;


上一篇:「JVM 执行子系统」Java 模块化系统
下一篇:「JVM 执行引擎」方法调用原理

PS:感谢每一位志同道合者的阅读,欢迎关注、评论、赞!


参考资料:

  • [1]《深入理解 Java 虚拟机》
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Aurelius-Shu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值