栈帧结构之局部变量表

背景

摘自《深入理解 Java 虚拟机:JVM 高级特性与最佳实践》(第2版)

概述

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

通过学习局部变量表,解答了之前项目开发过程中的一些疑惑,所以摘抄记录一下。

局部变量表

局部变量表(Local Variable Table) 是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在 Java 程序编译为 Class 文件时,就在方法的 Code 属性的 max_locals 数据项中确定了该方法所需要分配的局部变量表的最大容量。

        局部变量表的容量以变量槽(Variable Slot,下称 Slot)为最小单位,虚拟机规范中并没有明确指明一个 Slot 应占用的内存空间大小,只是很有导向性地说到每个 Slot 都应该能存放一个 boolean、byte、char、short、int、float、reference (注:Java 虚拟机规范中没有明确规定 reference 类型的长度,它的长度与实际使用 32 还是 64 位虚拟机有关,如果是 64 位虚拟机,还与是否开启某些对象指针压缩的优化有关,这里暂且只取 32 位虚拟机的 reference 长度)或 returnAddress 类型的数据,这 8 种数据类型,都可以使用 32 位或更小的物理内存来存放,但这种描述与明确指出 “每个 Slot 占用 32 位长度的内存空间” 是有一些差别的,它允许 Slot 的长度可以随着处理器、操作系统或虚拟机的不同而发送变化。只要保证即使在 64 位虚拟机中使用了 64 位的物理内存空间去实现一个 Slot,虚拟机仍要使用对齐和补白的手段让 Slot 在外观上看起来与 32 位虚拟机中的一致。

        既然前面提到了 Java 虚拟机的数据类型,在此再简单介绍一下它们。一个 Slot 可以存放一个 32 位以内的数据类型,Java 中占用 32 位以内的数据类型有 boolean、byte、char、short、int、float、reference 和 returnAddress 8 种类型。前面 6 种不需要多加解释,读者可以按照 Java 语言中对应数据类型的概念去理解它们(仅是这样理解而已,Java 语言与 Java 虚拟机中的基本数据类型是存在本质差别的),而第 7 种 reference 类型表示对一个对象实例的引用,虚拟机规范既没有说明他的长度,也没有明确指出这种引用应有怎样的结构。但一般来说,虚拟机实现至少都应当能通过这个引用做到两点,一是从此引用中直接或间接地查找到对象在 Java 堆中的数据存放的起始地址索引,二是此引用中直接或间接地查找到对象所属数据类型在方法区中的存储的类型信息,否则无法实现 Java 语言规范中定义的语法约束约束。第 8 种即 returnAddress 类型目前已经很少见了,它是为字节码指令 jsr、jsr_w 和 ret 服务的,指向了一条字节码指令的地址,很古老的 Java 虚拟机曾经使用这几条指令来实现异常处理,现在已经由异常表代替。

        对于 64 位的数据类型,虚拟机会以高位对齐的方式为其分配两个连续的 Slot 空间。Java 语言中明确的(reference 类型则可能是 32 位也可能是 64 位)64 位的数据类型只有 long 和 double 两种。

        虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从 0 开始至局部变量表最大的 Slot 数量。如果访问的是 32 位数据类型的变量,索引 n 就代表了使用第 n 个 Slot,如果是 64 位数据类型的变量,则说明会同时使用 n 和 n+1 两个 Slot。对于两个相邻的共同存放一个 64 位数据的两个 Slot,不允许采用任何方式单独访问其中的某一个,Java 虚拟机规范中明确要求了如果遇到进行这种操作的字节码序列,虚拟机应该在类加载的校验阶段抛出异常。

        在方法执行时,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果执行的是实例方法(非 static 的方法),那局部变量表中第 0 位索引的 Slot 默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字 “this” 来访问到这个隐含的参数。其余参数则按照参数表顺序排列,占用从 1 开始的局部变量 Slot,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的 Slot。

        为了尽可能节省栈帧空间,局部变量中的 Slot 是可以重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码 PC 计数器的值已经超出了某个变量的作用域,那这个变量对应的 Slot 就可以交给其他变量使用。不过,这样的设计除了节省栈帧空间以外,还会伴随一些额外的副作用,例如,在某些情况下,Slot 的复用会直接影响到系统的垃圾收集行为,首先看如下代码


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

如上代码很简单,即申请了了 64 MB 的数据,然后通知虚拟机进行垃圾收集。我们在虚拟机运行参数中加上 “-verbose:gc”  来看看垃圾收集的过程,发现 在 System.gc() 运行后并没有回收这 64 MB 的内存。

没有回收 placeholder 所占的内存能说得过去,因为在执行 System.gc() 时,变量 placeholder 还处于作用域之内,虚拟机自然不

敢回收 placeholder 的内存。那我们把代码修改如下:

public static void main(String[] args) {
	{
		byte[] placeholder = new byte[64 * 1024 * 1024];
	}
	System.gc();
}
  加入了花括号之后,placeholder 的作用域被限制在花括号之内,从代码逻辑上讲,在执行 System.gc() 的时候,placeholder 已经不可能再被访问了,但执行一下这段程序,会发现运行结果如下,还是有 64MB 的内存没有被回收,这又是为什么呢?

在解释为什么之前,我们先对这段代码进行第二次修改如下:

public static void main(String[] args) {
	{
		byte[] placeholder = new byte[64 * 1024 * 1024];
	}
	int a = 0;
	System.gc();
}
这个修改看起来很莫名其妙,但运行一下程序,却发现这次内存真的被正确回收了。

在如上代码中,placeholder 能否被回收的根本原因是:局部变量中的 Slot 是否还存在关于 placeholder 数组对象的引用。第一次

改中,代码虽然已经离开了 placeholder 的作用域,但在此之后,没有任何局部变量表的读写操作,placeholder 原本占用的 Slot 还没有

被其他变量所复用,所以作为 GC Roots 一部分的局部变量表仍然保持着对它的关联。这种关联没有被及时打断,在绝大部分情况下影响都

很轻微。但如果遇到一个方法,其后面的代码有一些耗时很长的操作,而前面又定义了占用了大量的内存、实际上已经不会再使用的变量,

手动将其设置为 null 值(用来代替那句 int a=0,把变量对应的局部变量表 Slot 清空)便不见得是一个绝对无意义的操作,这种操作可

以作为一种在极特殊情形(对象占用内存大、此方法的栈帧长时间不能被回收、方法调用次数达不到 JIT 的编译条件)下的 “奇技” 来

使用。Java 语言的一本著名书籍《Practical Java》中把 “不使用的对象应手动赋值为 null” 作为一条推荐的编码规则。

        关于局部变量表,还有一点可能会对实际开发产生影响,就是局部变量不像前面介绍的类变量那样存在 “准备阶段”。通过之前的

讲解,我们已经知道类变量有两次赋初始值的过程,一次在准备阶段,赋予系统初始化;另外一次在初始化阶段,赋予程序员定义的初始

值。因此,即使在初始化阶段程序没有为类变量赋值也没有关系,类变量仍然具有一个确定的初始值。但局部变量就不一样,如果一个局

部变量定义了但没有赋初始值是不能使用的,不要认为 Java 中任何情况下都存在诸如整型变量默认为 0,布尔型变量默认为 false 等这样

的默认值。先来看看下面的代码:

public static void main(String[] args) {
	int a;
	System.out.println(a);
}
这段代码其实并不能运行,还好编译器能在编译期间就检查到并提示这一点,即便编译能通过或者手动生成字节码的方式制造出下面

代码的效果,字节码校验的时候也会被虚拟机发现而导致类加载失败。






  • 3
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值