Memory Model Overview
A conceptual map of where Java's running data lives — thread stacks, the shared heap, and metaspace — and which parts are language guarantees versus JVM implementation choices.
Java programs work with several distinct kinds of runtime memory: each thread's own stack for local variables and call frames, a shared heap for objects, and metaspace for class metadata. This section gives the conceptual map most developers need day to day — while flagging clearly which parts (the existence of references, object identity, thread isolation of locals) are guaranteed by the Java Language Specification, and which parts (exact stack/heap layout, generational structure, metaspace internals) are JVM-implementation details that vary and can change between JVM versions.
The Stack/Heap Conceptual Model
A widely-used mental model divides runtime memory into two areas: a per-thread stack holding local variables, method parameters, and call-frame bookkeeping (return addresses, in effect); and a shared heap holding all objects (and arrays), which every thread with a reference to an object can access. This is a useful and broadly accurate simplification, but it is a conceptual model, not a literal specification of JVM internals — actual implementations can and do organize memory differently under the hood (e.g. escape analysis can let a JIT compiler allocate some objects that never escape a method entirely on the stack, as an optimization invisible to the program's observable behavior).
The JLS does not mandate a 'stack' and 'heap' by those names or with those exact boundaries — it defines observable behavior (like reference semantics, thread visibility rules) that this stack/heap picture is a helpful, but not literally binding, way to reason about.
Object References
A variable of a reference type does not hold the object's data directly — it holds a reference (conceptually, a handle or pointer the JVM manages) to an object located on the heap. Copying a reference copies the pointer, not the object; multiple variables can reference the identical object.
class Box { int value; }
Box a = new Box(); // 'a' (on some thread's stack) holds a reference to a Box object (on the heap)
a.value = 10;
Box b = a; // 'b' holds a copy of the SAME reference - not a new object
b.value = 20;
System.out.println(a.value); // 20 - a and b reference the identical objectThread Stacks
Each thread has its own stack, conceptually divided into frames — one per active method call — holding that method's local variables (including primitive values directly, and references to heap objects) and parameters. Because each thread has its own separate stack, local variables of one method invocation are never directly visible to another thread; only references stored into shared, heap-reachable locations (fields, arrays) can be seen across threads.
void method() {
int localCount = 0; // lives conceptually on THIS thread's stack frame for this call
Box localBox = new Box(); // reference lives on the stack; the Box object itself lives on the heap
}
// Another thread cannot see localCount or localBox directly - only shared heap state is
// visible across threads (and even then, subject to the Java Memory Model's visibility rules).The Heap
The heap is the shared memory area where all objects (including arrays) are allocated and where garbage collection operates. Unlike stack frames, which come and go with method calls, heap-allocated objects persist independently of any particular method call — their memory lifetime is governed by reachability, not by which method created them.
A typical HotSpot JVM may further subdivide the heap internally (e.g. young/old generations) for garbage collection efficiency — this internal subdivision is a JVM implementation detail, not something the Java language specifies or that application code observes directly.
Static Data: Conceptual Placement
Static fields belong to the class itself rather than to any instance, and conceptually there is exactly one copy per class per class loader, existing for as long as that class remains loaded. Historically some JVMs placed static field storage in a dedicated 'permanent generation'; modern HotSpot JVMs (since Java 8) store class metadata in metaspace and static field values as part of the class's representation, managed alongside ordinary heap-adjacent structures — the precise placement is again an implementation detail.
class Counter {
static int total = 0; // one copy shared by all instances and all code referencing Counter
}
Counter.total++; // no instance needed - accessed via the class itselfMetaspace: Conceptual Overview
Metaspace (introduced in Java 8, replacing the older 'PermGen') is where a typical HotSpot JVM stores class metadata — the loaded structure of each class: its method bytecode, constant pool, field/method descriptors, and similar per-class information, as opposed to per-instance data (which lives on the heap). Metaspace is allocated from native (off-heap) memory by default in HotSpot, bounded only by available system memory unless a -XX:MaxMetaspaceSize limit is set.
Metaspace, PermGen, and their exact behavior are HotSpot-specific implementation history/details — the Java Language Specification says nothing about where class metadata must live.
JVM vs Java Language Guarantees
It's essential to separate what the Java LANGUAGE guarantees from what a particular JVM happens to do. The language guarantees things like: reference semantics for object types, that unreachable objects eventually become GC-eligible, and (via the Java Memory Model) specific rules about when writes by one thread become visible to reads by another under proper synchronization. It does NOT guarantee any particular stack/heap memory layout, generational GC structure, metaspace design, or exact object header format — those vary by JVM vendor and version and are free to change.
Language guarantees (JLS): JVM implementation details (HotSpot example, not guaranteed by JLS):
- reference semantics - exact stack/heap layout
- GC-eligibility on unreachability - generational heap structure
- Java Memory Model visibility rules - object header format
(happens-before via synchronization, - specific collector algorithm (G1, ZGC, ...)
volatile, final field semantics) - metaspace vs PermGen internalsPractical Usage
This mental model is most useful for reasoning about: why passing an object to a method lets the method mutate the object's fields (both the caller's variable and the parameter reference the same heap object) but not reassign the caller's variable (parameters are local, stack-resident copies of the reference); why large objects/arrays don't blow a thread's stack (they're heap-allocated, referenced from the stack, not stored inline on it); and why deep, unbounded recursion causes StackOverflowError (each call frame consumes stack space, which is much smaller and more limited than heap space).
void mutate(Box b) {
b.value = 99; // mutates the shared heap object - visible to caller
b = new Box(); // reassigns the LOCAL parameter reference only - caller's variable is untouched
}
Box box = new Box();
box.value = 1;
mutate(box);
System.out.println(box.value); // 99 - the object was mutated, but box itself still refers to the original objectEdge Cases
Deep or infinite recursion exhausts a thread's stack, throwing StackOverflowError — this is a stack-specific failure mode, distinct from OutOfMemoryError (which typically indicates heap or metaspace exhaustion). Each is diagnostic of a different memory area being under pressure.
void recurse() { recurse(); } // no base case
// recurse(); // throws StackOverflowError - the per-thread stack, not the heap, is exhaustedMental Model
Picture each thread carrying its own small, fast notepad (the stack) for jotting down local variables and which method called which — private to that thread. All threads share one large warehouse (the heap) where the actual objects live; notepads only ever hold addresses/references pointing into that shared warehouse, never the objects' full contents. A separate filing cabinet (metaspace) holds the blueprints (class metadata) used to construct and interpret objects, shared across the whole program.
Quick Reference
Thread stack : per-thread, local variables + call frames, small, StackOverflowError on exhaustion
Heap : shared, all objects/arrays live here, GC-managed, OutOfMemoryError on exhaustion
Metaspace : shared, class metadata (bytecode, constant pool), typically native/off-heap (HotSpot)
Static fields : one copy per class per class loader, conceptually tied to the class's lifetime
Java language guarantees: reference semantics, GC-eligibility on unreachability, Java Memory Model rules
JVM implementation details: exact layout, generational structure, collector algorithm, object header formatKey Takeaways
The stack/heap split is a useful conceptual model: per-thread stacks hold locals and call frames; the shared heap holds all objects.
Reference variables hold a pointer to heap-allocated objects, not the object's data inline - copying a reference shares the same object.
Metaspace (replacing the older PermGen since Java 8) is where a typical HotSpot JVM stores class metadata, separate from per-instance object data on the heap.
Only some of this is a Java language guarantee (reference semantics, GC-eligibility, Java Memory Model visibility rules) - exact stack/heap layout and internal structures are JVM-implementation details that vary and can change.
StackOverflowError signals stack exhaustion (typically from deep recursion); OutOfMemoryError typically signals heap or metaspace exhaustion - they diagnose different memory areas.
// Assuming exact stack/heap layout as a hard language guarantee // e.g. writing code or documentation claiming Java's spec mandates a specific memory diagram
// Treat stack/heap/metaspace layout as a helpful conceptual model and typical // HotSpot behavior, not a Java Language Specification guarantee
The JLS specifies observable behavior (reference semantics, GC-eligibility, memory visibility rules), not literal memory layout - different JVMs are free to implement it differently.
void mutate(Box b) { b = new Box(); // developer expects this to change the caller's variable too } void mutate(Box b) { b.value = 99; // mutate the object's fields to affect what the caller sees }
Java passes the reference value by value - reassigning the parameter only changes the local copy of the reference, never the caller's variable. Only mutating the shared object's fields is visible to the caller.
// Confusing StackOverflowError and OutOfMemoryError as interchangeable catch (OutOfMemoryError e) { /* assumed this also catches infinite recursion */ } catch (StackOverflowError e) { /* specifically for stack exhaustion, e.g. from deep recursion */ } catch (OutOfMemoryError e) { /* specifically for heap/metaspace exhaustion */ }
StackOverflowError and OutOfMemoryError are distinct Error subclasses signaling exhaustion of different memory areas - conflating them obscures the actual cause of a failure.
What does a Java reference variable actually store: the object's data, or something else?
It stores a reference (conceptually a handle/pointer the JVM manages) to an object located on the heap - never the object's fields inline in the variable itself.
Why is the classic 'stack and heap' picture described as a conceptual model rather than a literal JVM specification?
Because the Java Language Specification defines observable program behavior (like reference semantics and GC-eligibility rules), not a mandated physical memory layout - real JVMs can and do implement memory differently under the hood (e.g. escape analysis allowing some objects to be optimized onto the stack) while remaining fully spec-compliant.
Why can't one thread directly see another thread's local variables?
Each thread has its own separate stack holding its own call frames and local variables; only state reachable through shared heap locations (fields, static variables, arrays) can potentially be seen by other threads, and even then only under the Java Memory Model's visibility rules.
What replaced PermGen in HotSpot, starting with Java 8, and what does it store?
Metaspace replaced PermGen. It stores class metadata - loaded classes' bytecode, constant pools, and field/method descriptors - as opposed to per-instance object data, which lives on the heap.
Why does deep, unbounded recursion throw StackOverflowError rather than OutOfMemoryError?
Each recursive call consumes space on the thread's stack for its call frame; the stack is a much smaller, per-thread resource than the heap, so it's exhausted first, producing a StackOverflowError that specifically signals stack exhaustion.
If you pass an object reference to a method and the method reassigns its local parameter to a new object, does the caller's variable change? Why?
No. The parameter is a local, stack-resident copy of the reference value; reassigning that local copy has no effect on the caller's own variable, which still refers to the original object. Mutating the object's fields through the parameter, however, IS visible to the caller, since both reference the same heap object.
Which parts of this memory picture are actual Java language guarantees, and which are JVM implementation details?
Guaranteed by the language: reference semantics, eventual GC-eligibility of unreachable objects, and the Java Memory Model's visibility/ordering rules under proper synchronization. Implementation details, not guaranteed: exact stack/heap layout, generational heap structure, collector algorithm, metaspace internals, and object header format.
What is printed, and why?
100 is printed. b.value = 100 mutates the shared heap object that both box and the parameter b reference, which the caller sees. The later b = new Box() only reassigns the local parameter b to a different object; it has no effect on the caller's box variable, which still points to the original object with value 100.
Write a method that recurses without a base case, and explain in a comment which specific error it throws and why that error rather than a heap-related one.
void recurse() { recurse(); // no base case - infinite recursion } // Calling recurse() throws StackOverflowError, not OutOfMemoryError, because // each call consumes space on the current thread's call stack for its frame // (return address, locals) - the stack is exhausted long before heap memory // would be, since the stack is a comparatively small, fixed-size, per-thread resource.
StackOverflowError is thrown because unbounded recursion exhausts the calling thread's stack (frame-by-frame), which is a distinct, much smaller resource from the shared heap that OutOfMemoryError typically concerns.
What is printed?
1 1 0. Counter.shared is a single copy shared by the whole class (conceptually not per-instance), so incrementing it once affects everyone who reads it. c1.local and c2.local are separate per-instance fields on separate heap objects, so incrementing c1's local doesn't affect c2's.
Explain in a comment why an array of one million ints does not overflow a thread's stack even though it's a large chunk of data.
int[] huge = new int[1_000_000]; // The array object itself is allocated on the shared heap, not the thread's stack. // The local variable 'huge' on the stack holds only a reference (a small, fixed-size // handle) to that heap-allocated array - so no matter how large the array's actual // data is, the stack frame's footprint for holding the reference stays tiny.
Because reference types (including arrays) store the object's data on the heap; the stack only ever holds the reference itself, which is small and fixed-size regardless of how large the referenced object is.
class Box { int value; }
void modify(Box b) {
b.value = 100;
b = new Box();
b.value = 999;
}
Box box = new Box();
box.value = 1;
modify(box);
System.out.println(box.value);class Counter { static int shared = 0; int local = 0; }
Counter c1 = new Counter();
Counter c2 = new Counter();
Counter.shared++;
c1.local++;
System.out.println(Counter.shared + " " + c1.local + " " + c2.local);