局部变量表是Java虚拟机栈帧的核心组成部分,每个栈帧都包含一个局部变量表,用于存储方法参数和方法内定义的局部变量。局部变量表的内存布局以slot为基本单位,不同数据类型的变量会占用不同数量的slot,数组作为引用类型变量,在局部变量表中的存储和优化有其特殊的逻辑。

局部变量表的基础内存布局
局部变量表的容量在编译期就确定下来,存储在方法的Code属性的max_locals数据项中。slot是局部变量表的最小存储单元,一个slot的大小通常是32位,不同虚拟机实现可能会有差异,但规范中要求boolean、byte、char、short、int、float、reference、returnAddress类型的数据占用1个slot,long和double类型的数据占用2个连续的slot。
局部变量表的索引从0开始,对于实例方法,第0个slot默认存储的是当前对象的引用this,后续依次是方法参数,再之后是方法内定义的局部变量。当变量超出其作用域后,对应的slot可以被后续定义的变量复用。
slot分配示例
下面通过一个简单的实例方法来看局部变量表的slot分配情况,先给出Java代码:
public class LocalVariableTableDemo {
public void testMethod(int a, String b) {
int c = 10;
long d = 20L;
String[] arr = new String[5];
}
}
编译后通过javap -v命令查看该方法的局部变量表,可以得到如下信息:
LocalVariableTable:
Start Length Slot Name Signature
0 8 0 this Lcom/LocalVariableTableDemo;
0 8 1 a I
0 8 2 b Ljava/lang/String;
0 8 3 c I
0 8 4 d J
0 8 6 arr [Ljava/lang/String;
可以看到,this占0号slot,int类型的a占1号slot,reference类型的b占2号slot,int类型的c占3号slot,long类型的d占4和5两个连续slot,String数组类型的arr占6号slot,符合前面提到的slot分配规则。
数组变量在局部变量表中的存储特点
数组属于引用类型,因此在局部变量表中,数组变量本身只占1个slot,存储的是指向堆中数组对象的引用地址。数组的实际元素数据都存储在堆中,局部变量表只保存引用,这一点和普通的对象引用是一致的。
但是数组和普通对象引用的区别在于,数组的长度信息、元素类型信息都存储在堆中的数组对象头中,局部变量表不需要额外存储这些信息,因此数组变量在局部变量表中的slot占用和普通引用类型没有区别,不会因为数组长度大就占用更多slot。
数组引用的slot复用
和普通引用类型一样,当数组变量的作用域结束后,对应的slot会被复用。比如下面的代码:
public class ArraySlotReuse {
public void test() {
// 第一个数组作用域
{
String[] arr1 = new String[10];
System.out.println(arr1.length);
}
// 新的局部变量会复用arr1的slot
int num = 20;
}
}
编译后查看局部变量表,arr1和num会占用同一个slot,因为arr1的作用域在大括号结束后就结束了,后续定义的num可以复用该slot的内存空间,这是局部变量表层面的基础优化。
虚拟机对数组变量的层面优化
1. 基于逃逸分析的栈上分配优化
逃逸分析是JVM在即时编译阶段做的优化分析,判断对象的作用域是否只会存在于方法内部,有没有被外部线程引用或者作为方法返回值返回。如果数组对象没有发生逃逸,JVM可能会将其分配到栈上,而不是堆中,这样可以减少堆内存的分配和垃圾回收压力。
当数组在栈上分配时,对应的数组引用在局部变量表中依然只占1个slot,但是数组的实际数据会跟随栈帧的销毁而自动回收,不需要GC介入。下面的代码如果开启逃逸分析,数组可能会被栈上分配:
public class ArrayEscapeAnalysis {
public void test() {
// 数组只在方法内部使用,没有逃逸
int[] localArr = new int[5];
for (int i = 0; i < localArr.length; i++) {
localArr[i] = i;
}
System.out.println(localArr[0]);
}
}
可以通过JVM参数-XX:+DoEscapeAnalysis开启逃逸分析,JDK1.7之后默认是开启的,需要注意的是栈上分配的前提是对象可以被拆解为标量,数组如果元素都是基本类型且长度不大,更容易被标量替换优化。
2. 标量替换优化
如果数组对象满足逃逸分析的条件,并且数组的元素都是基本类型,JVM可能会进行标量替换,把数组拆解为多个单独的局部变量,这些单独的局部变量会直接存储在局部变量表的slot中,不再需要在堆中分配数组对象。
比如上面的int[] localArr = new int[5]; 经过标量替换后,可能会被拆解为5个int类型的局部变量,分别占用不同的slot,这样就完全不需要数组对象的引用,局部变量表中也不会有对应的数组引用slot,进一步优化了内存使用。
3. 数组访问的边界检查消除
Java数组访问会自动做边界检查,防止数组越界,但是频繁的边界检查会带来性能开销。JVM在即时编译时,如果发现数组的访问索引在编译期就可以确定是合法的,就会消除边界检查,提升数组访问的效率。
比如下面的代码,访问索引i在编译期可以确定是0到4之间,符合数组长度,JVM可能会消除边界检查:
public class ArrayBoundsCheck {
public void test() {
int[] arr = new int[5];
for (int i = 0; i < 5; i++) {
arr[i] = i;
}
}
}
这种情况下,数组访问的效率会接近C语言的数组访问效率,边界检查的开销被消除。
4. 局部变量表的slot复用优化
对于数组变量来说,如果其作用域较短,后续的变量可以复用其slot,减少局部变量表的总大小。比如下面的情况:
public class ArraySlotOpt {
public void test() {
String[] arr = new String[3];
arr[0] = "a";
// arr的作用域结束,后面的list复用arr的slot
int[] list = new int[2];
list[0] = 10;
}
}
编译后arr和list会占用同一个slot,因为arr在第二个数组定义之前已经不再使用了,JVM的编译器会做这样的slot复用优化,减少局部变量表的内存占用。
不同场景下的优化效果对比
我们可以通过一个简单的测试来看不同情况下数组的优化效果,测试代码如下:
public class ArrayOptTest {
// 情况1:数组逃逸,分配到堆
public int[] escapeArray() {
int[] arr = new int[1000];
for (int i = 0; i < arr.length; i++) {
arr[i] = i;
}
return arr;
}
// 情况2:数组不逃逸,可能被栈上分配或标量替换
public int noEscapeArray() {
int[] arr = new int[1000];
int sum = 0;
for (int i = 0; i < arr.length; i++) {
arr[i] = i;
sum += arr[i];
}
return sum;
}
public static void main(String[] args) {
ArrayOptTest test = new ArrayOptTest();
long start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
test.escapeArray();
}
long end1 = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
test.noEscapeArray();
}
long end2 = System.currentTimeMillis();
System.out.println("逃逸数组耗时:" + (end1 - start) + "ms");
System.out.println("非逃逸数组耗时:" + (end2 - end1) + "ms");
}
}
运行后可以观察到,非逃逸的数组方法执行耗时明显低于逃逸的数组方法,因为非逃逸的数组可以被栈上分配或者标量替换,减少了堆分配和GC的开销。
开发中的注意事项
为了能让数组变量获得更好的虚拟机优化,开发中可以注意以下几点:
- 尽量缩小数组变量的作用域,避免不必要的长期持有数组引用,这样更容易触发slot复用和逃逸分析优化
- 如果数组只在方法内部使用,不要将其作为返回值或者传递给外部线程,避免数组逃逸,无法触发栈上分配和标量替换
- 对于短的基本类型数组,优先在方法内部定义使用,更容易被标量替换优化
- 访问数组时尽量使用编译期可确定范围的循环,帮助JVM消除边界检查
通过理解局部变量表的内存布局和数组变量的优化逻辑,开发者可以写出更符合JVM优化特性的代码,提升程序的运行效率。