面试中聊到了值传递,临场尝试用虚拟机栈的角度去解释值传递的过程,讲的稍微有些混乱,在这里对这次讨论做一下整理。
值传递 & 引用传递
首先先明确,程序设计语言中有两类将值传递给方法的方式:
- 值传递:方法的形参是实参值的拷贝
- 引用传递:方法接收的直接是实参所引用的对象在堆中的地址,不会创建副本,对形参的修改将影响到实参。
在 Java 中,只提供了值传递的方式。
从 JVM 虚拟机栈的角度看值传递
从 JVM 虚拟机栈的角度来看,方法调用时的参数传递本质上是通过「调用方法栈帧的操作数栈」向「被调用方法栈帧的局部变量表」复制数据的过程,且所有参数传递均是「值传递」。以下通过一个具体案例和 JVM 字节码解析完整过程:
示例代码与 JVM 内存结构
public class ParameterDemo {
public static void main(String[] args) {
int a = 10;
int[] arr = {1, 2, 3};
modify(a, arr);
}
public static void modify(int num, int[] array) {
num = 20;
array[0] = 100;
}
}
JVM 内存结构关键组件
- 虚拟机栈(Stack):每个线程独有,存储栈帧(Frame)。
- 栈帧(Frame):包含局部变量表(Local Variables)、操作数栈(Operand Stack)、动态链接等。
- 堆(Heap):存储对象实例和数组。
参数传递的详细过程
步骤 1:main 方法栈帧初始化
- 局部变量表:
- 操作数栈:初始为空。
步骤 2:调用 modify(a, arr)
前的准备
- 加载参数到操作数栈:
- 通过
iload_1
将局部变量表索引 1(a=10
)压入操作数栈。 - 通过
aload_2
将局部变量表索引 2(arr
的引用地址)压入操作数栈。 操作数栈状态:
| 10 |
| arr@ |
- 触发方法调用指令:
invokestatic ParameterDemo.modify(int, int[]) // 调用静态方法
步骤 3:创建 modify
方法栈帧
- 新建栈帧:
- 局部变量表大小 = 参数数量 + 可能的额外空间(如
this
,但此处是静态方法无this
)。 - 参数复制:将操作数栈中的值按顺序复制到新栈帧的局部变量表。
modify
栈帧的局部变量表:
步骤 4:执行 modify
方法体
- 修改
num
:
num = 20; // 对应字节码:bipush 20 -> istore_0
- 将 20 压入操作数栈,再存储到局部变量表索引 0。
- 仅修改副本,原
main
中的a
仍为 10。 局部变量表更新: 索引 变量名 值 0 num 20
- 修改
array[0]
:
array[0] = 100; // 对应字节码:aload_1 -> iconst_0 -> bipush 100 -> iastore
- 通过局部变量表索引 1(
array
)获取引用地址,找到堆中的数组对象。 - 修改堆中数组第 0 个元素为 100。
- 堆数据被修改,
main
中的arr
指向同一对象,因此变化可见。 堆内存变化:
原数组:[1, 2, 3]
修改后:[100, 2, 3]
步骤 5:方法返回与栈帧销毁
modify
方法执行完毕:
- 弹出
modify
栈帧,回到main
方法栈帧。 - 操作数栈恢复为空,继续执行后续指令(此处无更多操作)。
- 最终内存状态:
main
中的a
保持 10(基本类型值未变)。main
中的arr[0]
变为 100(堆数据被修改)。
关键结论
- 值传递的本质:
- 基本类型:传递值的副本,修改副本不影响原值。
- 引用类型:传递引用地址的副本,副本与原引用指向同一堆对象,因此通过副本修改对象内容会影响原数据,但修改引用本身(如
array = null
)不影响原引用。
- JVM 操作规范:
- 参数通过操作数栈传递,复制到被调用方法的局部变量表。
- 方法调用通过
invokestatic
、invokevirtual
等指令实现,参数按顺序压栈。 - 堆数据由所有线程共享,栈帧销毁不影响堆中已修改的数据。
字节码验证
通过 javap -c ParameterDemo.class
查看字节码:
public static void main(java.lang.String[]);
Code:
0: bipush 10 // 将10压入操作数栈
2: istore_1 // 存储到局部变量表索引1(a=10)
3: iconst_3 // 准备创建数组
4: newarray int
6: dup
7: astore_2 // 存储数组引用到索引2(arr)
...
16: iload_1 // 加载a的值到操作数栈
17: aload_2 // 加载arr的引用到操作数栈
18: invokestatic #2 // 调用modify方法
21: return
public static void modify(int, int[]);
Code:
0: bipush 20 // 将20压入操作数栈
2: istore_0 // 存储到局部变量表索引0(num=20)
3: aload_1 // 加载array引用
4: iconst_0
5: bipush 100
7: iastore // 修改堆中数组元素
8: return
结论:字节码明确展示了参数传递和修改过程,与上述分析完全一致。