2.1 运行时存储:栈帧、对象、值语义与逃逸
类型系统回答“值属于什么类型”,运行时还要回答“对象在哪里、活多久、由谁释放”。Java 常用垃圾收集追踪可回收对象,C 把分配与释放责任交给程序,Rust 则用所有权与借用规则在编译期约束许多生命周期关系。三条路线各自有代价与边界。
“栈上快、堆上慢”是过度简化。语言规范描述可观察语义,虚拟机规范描述抽象运行时,具体 JIT/编译器再决定寄存器、栈槽、对象布局和是否消除分配。
JVM Frame 与 Heap 是规范抽象
每次方法调用创建 frame,包含局部变量数组、operand stack 和运行时常量池引用。JVM heap 是所有线程共享、供 class instance 和 array 分配的运行时区域。
引用类型的局部变量里保存对象 reference,不代表对象物理上一定存在某块固定 heap 内存。JVMS 明确允许实现选择对象表示和优化。JIT 可通过 escape analysis 发现对象不逃出方法/线程,进行 scalar replacement:对象字段直接成为标量,甚至不发生可观察分配。
因此“Java 对象永远在堆上”适合作为字节码抽象入门,不适合作为性能结论;“类型擦除导致泛型对象不能放栈上”也没有因果关系。
Stack allocation 的核心是生命周期嵌套
函数调用严格嵌套时,frame 可按 LIFO 回收。局部 primitive、reference 和编译器临时值通常映射到寄存器或栈槽,但闭包捕获、协程暂停和逃逸引用可能让状态寿命超过调用。
编译器可以把逃逸状态提升到 heap,也可以在证明不逃逸时消除原本的 heap allocation。源码语法不是最终布局证明,应通过 profiler、JIT diagnostic 和生成代码观察。
Value 与 Reference semantics 是复制规则
Value semantics:赋值/传参得到独立值(实现可优化拷贝);reference semantics:复制的是到同一对象的引用。
Java 参数传递始终是 pass-by-value:传对象时复制 reference value。被调函数可通过副本修改同一对象,却不能让调用方变量改指向另一个对象。
下面例子把“修改对象”和“替换局部引用”分开。保存为 ReferenceDemo.java,用 JDK 17 或更高版本运行 java ReferenceDemo.java:
class ReferenceDemo {
static final class Box { int value; }
static void change(Box box) {
box.value = 7;
box = new Box();
box.value = 99;
}
public static void main(String[] args) {
Box original = new Box();
change(original);
System.out.println(original.value);
}
}输出为 7。参数副本仍能找到原对象,所以第一次修改可见;把参数改指向新 Box 后,改动只落在新对象上。引用值确实被复制了,对象没有因此分裂成两份。这只验证传参语义,不证明对象最终位于寄存器、栈还是堆。
Rust 值可能 move、copy 或 borrow。Copy 类型按位复制仍受类型语义约束;非 Copy 值被完整移走后,不能再通过原绑定使用那个值;可变绑定仍可在重新初始化后使用。&T 与 &mut T 把共享/独占借用写进类型检查,减少悬垂引用和数据竞争。
Object layout 不由源码字段简单相加
实际大小可能包含 object header、alignment padding、compressed reference、数组长度与 GC metadata。字段重排规则、CPU cache line 和虚调用布局依赖实现。
优化数据局部性时关注访问模式:数组内连续值通常比指针图更 cache-friendly;但结构体数组(AoS)与数组结构体(SoA)优劣取决于算法读哪些字段。先用 profiler 找 cache miss/分配热点,再改变表示。
FFI 需要重新建立生命周期契约
跨 Java/Rust/C/Python 边界时,GC 是否能移动对象、谁拥有 buffer、回调能否超过调用、异常怎样传播都要明确。移动式 GC 的 pinning 会限制对象移动,长期固定或遗留的 native reference 可能妨碍回收。Rust 的 Pin 是另一种约束:限制值从固定位置移走,并不表示有一个 GC 在管理它。
安全 wrapper 应把 raw pointer 与长度、allocator、释放函数和线程规则封装起来,确保所有路径只释放一次并避免使用后释放。
下一课比较 tracing GC、reference counting 与 ownership 怎样判断对象可回收,以及回收工作由谁承担。
参考资料
Oracle, JVMS: Run-Time Data Areas
Oracle, HotSpot Escape Analysis
Rust, Understanding Ownership
Rust, Pinning