The idea: frames per thread, objects in one shared heap
A Java program keeps data in two places. Every thread has its own stack:
one frame per method call, holding that call's local variables. All threads share one
heap: every object made with new lives there. A local variable of a primitive type
(int, boolean, …) holds its value right in the frame. A local variable of a class type
holds a reference: a small pointer to an object on the heap. On the canvas that reference is an arrow.
The page runs real javac bytecode, one instruction per step, on a small model of HotSpot
(64-bit, compressed references). The sizes of objects and frames follow HotSpot's rules; the stack and heap
capacities are scaled down so they fit on the screen.
A JVM frame: local variables and the operand stack
A frame has two parts. The local variable array has numbered slots: slot 0 is the first
parameter (args for main, this for an instance method), then the other
parameters, then the locals. The operand stack is a scratch area: bytecode pushes values on it,
works on them and stores them back. int a = 5; becomes iconst_5 (push 5) and
istore_1 (pop into slot 1). The frame size is fixed when the method is compiled
(max_locals, max_stack).
A call pushes a new frame and copies the arguments into its first slots. A return pops the frame: all its locals are gone in one move of the stack pointer, with nothing to free (Demo: primitives vs references).
Where objects go: TLABs and the bump pointer
New objects are made in the young generation's eden. To avoid a lock on every
new, each thread gets a private piece of eden, a TLAB (thread-local allocation
buffer). Allocating is then just "move my TLAB's top pointer forward by the object's size" — a few instructions.
When the TLAB is full, the thread retires it (the unused tail gets a filler object) and takes a new one from eden.
The red line in each row is the TLAB's top.
| Object | Layout (compressed oops) | Size |
|---|---|---|
new Object() | mark word 8 + class pointer 4, padded | 16 B |
Point(int x, int y) | 12 header + 4 + 4 = 20, padded to 8 | 24 B |
Integer | 12 + 4 | 16 B |
int[3] | 12 + 4 length + 3 × 4 = 28, padded | 32 B |
Node(int val, Node next) | 12 + 4 + 4 (compressed reference) | 24 B |
References are values: pass by value
Java always passes arguments by value. For an object, the value is the reference. In
Demo: pass by value, move(p) gets a copy of p's reference: q.x = 9
follows the reference and changes the shared object, but q = new Point(0, 0) only changes
move's own slot. After the call, p.x is 9 and the new point is garbage.
Lifetime: reachability, not scope
A frame dies when its method returns. An object dies when nothing reaches it any more: the
GC roots are the stack slots and operand stacks of every thread plus the static fields. In
Demo: frames die, objects stay, makeList returns and its frame is gone, but its three Nodes
stay alive because main's slot reaches them. After list = null nothing does, and the
GC frees them. Grey objects on the canvas are unreachable; the JVM itself only finds out at the next GC.
A young GC copies the live young objects to the survivor space (or to the old generation when
that is full) and empties eden at once: the cost depends on the live objects, not on the garbage. If the old
generation has no room either, a full GC marks and compacts the whole heap. When even that frees
nothing, new throws OutOfMemoryError: Java heap space (Demo: OutOfMemoryError:
a static field keeps every chunk reachable). Java Garbage Collectors shows Parallel,
G1 and ZGC in detail.
Two threads, one heap
Each thread has its own stack, so locals are never shared. Objects are: in Demo: two threads, one
heap both threads hold a reference to the same Counter. c.n = c.n + 1 is three
steps (read, add, write), so two threads can lose an update; that is what locks and atomics are for. Each thread
also allocates in its own TLAB. How the CPU switches between the two stacks is on
Linux Context Switch.
Boxing and the Integer cache
Integer a = 127; compiles to Integer.valueOf(127). Values from -128 to 127 come from a
cache made at startup, so a == b is true. 128 is outside the cache: two separate 16 B objects, and
c == d is false because == compares references (Demo: Integer cache).
Compare boxed values with equals().
Stack allocation without the stack: escape analysis
Java has no way to put an object on the stack. The C2 JIT compiler can do something similar on its own: when
escape analysis proves that an object never leaves a method (it is not stored in a field, not
returned, not passed to code it cannot see), C2 applies scalar replacement: the object's fields
become plain locals and the allocation disappears. In Demo: escape analysis the interpreter allocates
three 24 B Points (72 B); the C2 version allocates 0 B. It is on by default (-XX:+DoEscapeAnalysis)
and only applies to hot, compiled methods.
StackOverflowError
A thread's stack has a fixed size (-Xss, 1 MiB by default on 64-bit Linux). Below it lie guard
pages (the yellow and red zones). A frame that would reach them makes the JVM throw
StackOverflowError, an ordinary exception object on the heap: it unwinds the frames and can be caught
(Demo: StackOverflowError). With 1 MiB a simple recursive method gets thousands of frames deep; the page
scales the stack to 1 KiB.
C and Java side by side
| C (Stack vs Heap in C) | Java (this page) | |
|---|---|---|
| Who frees heap memory | the programmer, free() | the garbage collector |
| Pointer to a stack variable | possible (&local) | impossible: objects are never on the stack |
| Dangling pointer, use-after-free, double free | possible, undefined behaviour | impossible |
| Memory leak | forgotten free | forgotten reference (e.g. a static collection) |
| Heap allocation cost | malloc: search a free list | bump a TLAB pointer |
| Stack overflow | SIGSEGV, the process dies | StackOverflowError, can be caught |
| Per-object overhead | 8 B chunk header (glibc) | 12 B object header |
See also Stack vs Heap in C, Java Garbage Collectors, How Linux Loads a Program (where a process's stack comes from), Linux Virtual Memory (both stack and heap are just mapped memory), Linux Context Switch and From a Java Thread to a CPU Core (which kernel task and CPU runs each thread).
What the page leaves out
Constructor frames (<init> runs in one step), object ages and tenuring thresholds, the card
table, humongous objects, class metadata in Metaspace, the Thread and lambda objects of the threads demo, compiled frames
and inlining, virtual threads (whose stacks are copied to the heap when they park), and
-XX:-UseCompressedOops (8 B references, 16 B headers). The heap and stack capacities are scaled;
real TLABs are kilobytes and real eden is megabytes.