Skip to content

VOL 09 / CH 02 / LESSON 01

2.1 Runtime Storage: Stack Frames, Objects, Value Semantics, and Escape ​

A type system answers “What type does this value have?” The runtime must also answer “Where does the object live, how long does it live, and who releases it?” Java commonly uses garbage collection, C delegates allocation and deallocation to the program, and Rust constrains many lifetime relationships through ownership and borrowing rules at compile time. Each approach has costs and boundaries.

“The stack is fast, the heap is slow” is an oversimplification. Language specifications describe observable semantics, virtual machine specifications define abstract runtime behavior, and concrete JIT or compilers ultimately decide on register allocation, stack slot layout, object layout, and whether allocations can be eliminated.

JVM Frame and Heap Are Specification Abstractions ​

Each method invocation creates a frame, containing a local variable array, an operand stack, and references to the runtime constant pool. The JVM heap is a shared runtime region accessible to all threads, used for allocating class instances and arrays.

Reference-typed local variables store object references, but this does not mean that the corresponding objects are physically located in a fixed block of heap memory. The JVMS explicitly allows implementations to choose how to represent objects and to apply optimizations. JIT compilers can use escape analysis to detect when objects do not escape from a method or thread, enabling scalar replacement: object fields are directly replaced with scalars, and in some cases, no observable allocation occurs at all.

Thus, the statement "Java objects are always on the heap" is suitable as an introductory abstraction for bytecode-level understanding, but it is not a valid performance conclusion. Similarly, the claim that "type erasure prevents generic objects from being stored on the stack" lacks a causal relationship.

The Core of Stack Allocation is Lifecycle Nesting ​

When function calls are strictly nested, frames can be safely reclaimed in a last-in, first-out (LIFO) order. Primitive values, references, and compiler-generated temporaries are typically mapped to registers or stack slots. However, features like closure captures, coroutine suspensions, and escape references can extend the lifetime of state beyond the scope of a single call.

Compilers can elevate escaped state to the heap, or eliminate heap allocations entirely if they can prove that no escaping occurs. Source syntax alone does not establish memory layout. Observe the implementation through profiling, JIT diagnostics, and generated code.

Value and Reference Semantics Are Copy Rules ​

Value semantics: assignment or parameter passing results in a standalone copy (enabling optimized copying); reference semantics: what is copied is a reference to the same object.

In Java, parameter passing is always pass-by-value: when passing an object, a reference value is copied. The called function can modify the object through this copy, but it cannot cause the caller's variable to point to a different object.

This example separates mutation of an object from reassignment of a local reference. Save it as ReferenceDemo.java and run java ReferenceDemo.java with JDK 17 or later:

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);
    }
}

The output is 7. The copied reference still reaches the original object, so the first mutation is visible. Reassigning the parameter directs later changes to a new Box. The reference was copied; the object did not split into two. This verifies parameter semantics without establishing whether the optimized objects occupy registers, stack slots, or the heap.

In Rust, values may be moved, copied, or borrowed. Copy types are bit-wise copied, but their behavior is still constrained by type semantics; after a complete move of a non-Copy value, the original binding cannot be used to access that value. A mutable binding can be used again after reinitialization. &T and &mut T encode shared and exclusive borrowing directly into type checking, reducing dangling references and data races.

Object layout Goes Beyond the Sum of Source Field Sizes ​

The actual size may include object header, alignment padding, compressed references, array length, and GC metadata. Field reordering rules, CPU cache line alignment, and virtual method call layouts depend on the specific implementation.

When optimizing data locality, focus on access patterns: values accessed consecutively within an array are typically more cache-friendly than pointer-based structures. However, the performance advantage of array-of-structures (AoS) versus structure-of-arrays (SoA) depends on which fields the algorithm actually reads. Begin by using a profiler to identify cache misses and allocation hotspots, then reconsider the data representation.

FFI Requires Reestablishing a Lifecycle Contract ​

When crossing boundaries between Java, Rust, C, and Python, it's essential to clearly define whether the garbage collector (GC) can move objects, who owns the buffer, whether callbacks can outlive their original calls, and how exceptions propagate. Pinning in a moving GC restricts object movement; long-lived pins or forgotten native references can interfere with reclamation. Rust’s Pin has a different role: it constrains moving a value out of its pinned location and does not imply that a GC manages it.

A safe wrapper should keep raw pointers together with their lengths, allocators, release functions, and thread rules. Every owning path must release the resource exactly once and avoid using it after release.

The next lesson compares how tracing GC, reference counting, and ownership determine what can be reclaimed and who performs that work.

References ​

Built with VitePress | Software Systems Atlas