局部变量表(slot/静态vs局部变量)
局部变量表是什么
局部变量表(Local Variable Table)是栈帧中用于存储方法参数和局部变量的区域。它是 JVM 字节码执行的重要支撑,理解它有助于理解方法的参数传递、变量作用域,以及为什么局部变量不存在线程安全问题。
Slot:局部变量表的基本单位
局部变量表是一个以 Slot 为单位的数组。Slot 是 JVM 分配给变量的最小存储单位,大小为 4 字节(32 位)。
类型与 Slot 的关系
| 类型 | 占用 Slot 数 | 说明 |
|---|---|---|
int、short、byte、char、boolean、float | 1 Slot | 单个 Slot |
long、double | 2 Slot | 需要连续的 2 个 Slot |
reference(对象引用) | 1 Slot | 4 字节,存储对象地址 |
public class SlotUsage {
public void method(
int i, // slot 1
long l, // slot 2-3(占2个Slot)
double d, // slot 4-5(占2个Slot)
Object o // slot 6(reference)
) {
int a = 1; // slot 7
long b = 2L; // slot 8-9(占2个Slot)
String s = ""; // slot 10(reference)
}
}Slot 复用与 GC
Slot 复用会影响 GC 的可达性分析:
public class SlotReuseGC {
Object obj = new Object(); // 类变量,存在堆中
public void method() {
// slot 1 分配给 objRef
objRef = new Object();
// ... 使用 objRef
}
}如果 objRef 出了作用域,但它的 Slot 没有被后续变量复用,Slot 中仍然保存着对对象的引用,GC 就会认为这个对象仍然可达(即使代码中已经无法访问它了)。这是 JVM 的保守策略:宁可多留对象,也不提前回收。
实际上,由于 Slot 复用,这种情况在实践中很少成为问题。但理解这一点有助于理解 GC 的根节点枚举过程。
实例方法中的 this
非 static 方法中,局部变量表的第一个 Slot(slot 0)总是存储 this 引用——当前对象的引用。
public class ThisSlot {
public void instanceMethod() {
// slot 0: this(隐式传入)
// slot 1: a
int a = 10;
}
public static void staticMethod() {
// 没有 this
// slot 0: a
int a = 10;
}
}为什么 static 方法没有 this?因为 static 方法属于类,不属于某个对象,自然没有 this 引用。
局部变量表与类变量(static 变量)的对比
这是面试中常见的问题:
| 维度 | 局部变量 | 类变量(static) |
|---|---|---|
| 存储位置 | 虚拟机栈(栈帧的局部变量表) | 方法区/元空间 |
| 生命周期 | 方法调用时创建,方法结束销毁 | 类加载时创建,类卸载时销毁 |
| 初始化 | 必须手动赋值,否则编译错误 | 默认零值 + 初始化块赋值 |
| 内存区域 | 线程私有 | 线程共享 |
| GC | 方法结束即回收 | 类卸载或 FullGC 时回收 |
public class VariableCompare {
// 类变量(static):存在于方法区
static int staticVar = 100;
static { System.out.println("static block"); }
public void method() {
// 局部变量:存在于虚拟机栈的栈帧中
int localVar = 200;
// 关键区别:
// - localVar 存在线程自己的栈上,另一个线程看不到
// - staticVar 存在方法区,所有线程共享
}
}参数传递:值传递还是引用传递
这是另一个经典面试题。Java 永远是值传递,但需要分清「基本类型」和「引用类型」的情况。
基本类型参数传递
public class PrimitivePassByValue {
public static void main(String[] args) {
int num = 10;
change(num);
System.out.println(num); // 输出 10
}
static void change(int num) {
num = 20; // 修改的是栈帧中 num 的副本
}
}引用类型参数传递
public class ReferencePassByValue {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("Hello");
change(sb);
System.out.println(sb); // 输出 HelloWorld
}
static void change(StringBuilder sb) {
// sb 存储的是引用的副本(副本也是地址)
// 通过这个副本访问的是同一个 StringBuilder 对象
sb.append("World");
// 重新指向新对象,不影响原调用者
sb = new StringBuilder("New");
}
}关键理解:把引用类型的变量理解为「遥控器」会更清晰。传递引用参数,相当于把你的遥控器的「复印件」传给了方法。方法可以用这个复印件换频道(修改对象内容),但没法把复印件换成另一个遥控器(重新赋值会影响复印件本身,但不影响原遥控器)。
局部变量表的字节码视角
加载到操作数栈
局部变量表中的值,需要通过 iload、aload 等指令加载到操作数栈才能使用:
public int loadDemo() {
int a = 1;
int b = 2;
return a + b;
}字节码:
iconst_1 // 常量 1 入栈(不使用局部变量)
istore_1 // 出栈存入 slot 1(a)
iconst_2 // 常量 2 入栈
istore_2 // 出栈存入 slot 2(b)
iload_1 // slot 1 的值入栈
iload_2 // slot 2 的值入栈
iadd // 相加
ireturn // 返回iload 系列指令
JVM 为局部变量表的前 4 个 int 值提供了专用指令(更短更高效):
| 指令 | 含义 | 等价于 |
|---|---|---|
iload_0 | slot 0 入栈 | iload 0 |
iload_1 | slot 1 入栈 | iload 1 |
iload_2 | slot 2 入栈 | iload 2 |
iload_3 | slot 3 入栈 | iload 3 |
lload_0/1/2/3 | long 类型 | - |
fload_0/1/2/3 | float 类型 | - |
dload_0/1/2/3 | double 类型 | - |
aload_0/1/2/3 | reference 类型 | - |
超过 4 个的情况,使用通用指令 iload n(n 为 slot 编号)。
局部变量表与调试
局部变量表使得调试器能够在断点处查看变量的值:
public class DebugDemo {
public static void main(String[] args) {
int a = 1; // ← 断点在这里
int b = 2; // IDE 能看到 a = 1
int c = a + b; // 依赖局部变量表信息
System.out.println(c);
}
}当用 javac -g 编译时,局部变量表会包含变量名信息;不带 -g 编译时,局部变量表只有类型信息,没有变量名(调试时 IDE 只显示 slot 编号)。
本节小结
局部变量表的核心要点:
| 关键点 | 说明 |
|---|---|
| Slot | 基本单位,4 字节;long/double 占 2 个 |
| this | 非 static 方法的 slot 0 |
| 必须初始化 | 局部变量必须赋值才能使用,编译器会检查 |
| 线程私有 | 存在虚拟机栈中,线程安全 |
| GC 影响 | Slot 中的引用影响 GC 的可达性判断 |
理解局部变量表,再结合前文说的「线程私有区域」,就能清楚理解为什么局部变量天然线程安全,而 static 变量却需要考虑并发问题。
下一节,我们来看 操作数栈(栈顶缓存/字节码指令分析)。
