JVM Runtime Data Areas
The JVM Specification carves a running program's memory into distinct runtime data areas — some shared across all threads, some private to each — and understanding this split explains many everyday Java behaviors.
While a Java program executes, the JVM needs somewhere to keep its live state: objects, method call frames, static class data, and more. The JVM Specification defines a small set of runtime data areas for this purpose, split cleanly into per-JVM (shared across all threads) and per-thread (private to each thread) categories. This split isn't just an implementation curiosity — it directly explains why the heap needs synchronization for shared object access while each thread's own stack never does, why StackOverflowError and OutOfMemoryError are thrown from different areas, and how a method call's local variables and return address are actually tracked.
The Two Categories: Per-JVM vs. Per-Thread
The JVM Specification's runtime data areas fall into two categories. Per-JVM (shared) areas exist once for the whole running JVM instance and are visible to every thread: the heap and the method area. Per-thread (private) areas are created fresh for each thread when it starts and destroyed when it terminates: the JVM stack, the program counter register, and the native method stack.
object instances, arrays class metadata, runtime constant pool, static fields frames of local vars + operand stack address of current instruction separate frames, independent of Thread 1
Heap and Method Area are shared across all threads; each thread gets its own JVM stack, PC register, and native method stack.
The Heap
The heap is where all objects and arrays are allocated — every new Foo() and every array creation puts its data here, regardless of which thread created it, and it's visible to every thread. It is created when the JVM starts and is the primary target of garbage collection. Because it's shared, concurrent access to the same object's fields from multiple threads requires explicit synchronization to avoid data races — the JVM does not automatically protect heap object state across threads.
A typical modern HotSpot JVM further subdivides the heap internally (commonly into 'young' and 'old' generations, based on the empirical observation that most objects die young) purely as an implementation/GC-algorithm optimization — the specification itself says nothing about generations; a conforming JVM could organize its heap differently.
java.lang.OutOfMemoryError: Java heap space is thrown when the heap cannot allocate a requested object and the garbage collector cannot free enough space, after exhausting the JVM's configured maximum heap size (-Xmx).
The Method Area
The method area is a per-JVM, shared area storing per-class structures: each loaded class's runtime constant pool, field and method data, method bytecode, and (in most implementations) static variables. Conceptually it's part of what the specification calls a shared area analogous to the heap in lifecycle, though the specification treats it as its own logically distinct area.
In HotSpot specifically, before Java 8 this was implemented as 'PermGen' (Permanent Generation), a fixed-size region that was a notorious source of OutOfMemoryError: PermGen space in applications that loaded many classes dynamically (class-loader leaks in application servers being the classic case). Java 8 replaced PermGen with Metaspace, which by default grows into native (off-heap) memory rather than a fixed-size region, largely (though not entirely) eliminating that specific failure mode. Both PermGen and Metaspace are HotSpot implementation choices for realizing the specification's method area concept, not something the specification names directly.
The Runtime Constant Pool
Each loaded class carries its own runtime constant pool — a per-class table (conceptually part of the method area) holding numeric literals, String literals, and symbolic references to classes, fields, and methods that the class's bytecode refers to by index rather than by embedding the full data inline. This is a run-time construct derived from the constant_pool table stored in the class's .class file (see the Bytecode topic), with symbolic references progressively resolved to direct references as linking's resolution phase proceeds.
Every symbolic reference bytecode instructions use (like invoking println or referring to a String literal) is resolved through entries in this per-class table, not hardcoded addresses.
javap -v HelloWorld.class # -v (verbose) prints the constant pool, e.g.:
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #18 // Hello, World!
...JVM Stacks
Each thread gets its own JVM stack, created when the thread starts. It stores frames — one per active (not-yet-returned) method invocation — each holding that method's local variables array and operand stack (the working space bytecode instructions push to and pop from while executing, see the Bytecode topic). A frame is pushed when a method is invoked and popped when it returns (normally or via an exception).
Calling square pushes a new frame onto the current thread's JVM stack, holding n and result as local variables; the frame is popped and its return value handed back to main's frame when square returns.
java.lang.StackOverflowError is thrown when a thread's JVM stack runs out of space — most commonly from unbounded or excessively deep recursion, since each recursive call pushes another frame.
class StackDemo {
static int square(int n) {
int result = n * n; // lives in this frame's local variable array
return result;
}
public static void main(String[] args) {
int x = square(5); // pushes a new frame for square()
System.out.println(x);
}
}The Program Counter (PC) Register
Each thread has its own PC register, tracking the address of the JVM instruction the thread is currently executing. This is what lets the JVM correctly resume each thread's execution at the right instruction after a context switch. For a thread currently executing a native (non-Java) method, the PC register's value is left undefined by the specification, since native code isn't tracked by JVM bytecode addresses in the same way.
Native Method Stacks
Also per-thread, native method stacks support the execution of native (non-Java) methods called through JNI, analogous in role to the JVM stack but for native code frames rather than Java bytecode frames. The specification permits an implementation to combine this with the JVM stack rather than keeping them entirely separate, and many implementations do exactly that.
How These Areas Interact During a Method Call
Calling an instance method: the object itself lives on the heap; the calling thread's JVM stack gets a new frame with a local variables array (slot 0 holding the this reference for an instance method, then the method's parameters, then any local variables) and an operand stack for intermediate computation; the method's bytecode and constant pool entries are read from the method area; and the thread's PC register tracks which bytecode instruction within that method is currently executing.
Implementation-Specific Mappings: What HotSpot Actually Does
It bears repeating: the specification names these logical areas, but a conforming JVM has real latitude in how it physically realizes them. HotSpot's Metaspace (off-heap, growable native memory for the method area's data), its generational heap layout, and its combined native/JVM stack handling in some configurations are all specific implementation choices — useful to know when tuning a real JVM (-Xmx, -Xss, -XX:MaxMetaspaceSize and similar flags), but not guarantees the JVM Specification itself makes.
Key Takeaways
The heap and method area are per-JVM and shared across all threads; JVM stacks, PC registers, and native method stacks are created fresh per thread.
The heap holds all object/array instances and is the garbage collector's primary target; shared access across threads requires explicit synchronization.
The method area holds per-class data (constant pool, method/field metadata); HotSpot has realized it as PermGen historically, and as Metaspace since Java 8.
A JVM stack holds one frame per active method call, each with its own local variables array and operand stack; StackOverflowError comes from exhausting this per-thread stack.
Exact memory layout and area sizing (generational heap structure, Metaspace growth, stack sizing) are JVM-implementation and configuration details, not JLS/JVM-Specification-mandated guarantees.
Assuming every thread shares one JVM stack.
Understand that each thread gets its own private JVM stack, created when the thread starts and destroyed when it terminates.
Only the heap and method area are shared across threads; stacks, PC registers, and native method stacks are strictly per-thread.
Referring to 'PermGen' as if it still exists and is where the method area lives in any modern JVM.
PermGen was replaced by Metaspace starting in Java 8; describe PermGen as a historical HotSpot implementation detail, not current behavior.
Metaspace, unlike PermGen, grows by default into native memory rather than a fixed-size region, changing the OutOfMemoryError characteristics it produces.
Treating StackOverflowError and OutOfMemoryError as interchangeable symptoms of 'running out of memory.'
Distinguish them by area: StackOverflowError comes from exhausting a thread's private JVM stack (typically deep/unbounded recursion); OutOfMemoryError: Java heap space comes from exhausting the shared heap.
They come from different runtime data areas with different causes and different fixes — increasing heap size does nothing for a StackOverflowError caused by runaway recursion.
Which JVM runtime data areas are shared across all threads, and which are private to each thread?
Shared/per-JVM: the heap and the method area. Private/per-thread: the JVM stack, the program counter (PC) register, and the native method stack.
What does a single frame on a JVM stack contain?
A local variables array (for the currently executing method's parameters and local variables, including 'this' for instance methods) and an operand stack (working space for intermediate values during bytecode execution).
What replaced PermGen in HotSpot, starting with Java 8, and what's the key behavioral difference?
Metaspace replaced PermGen. Unlike PermGen's fixed-size region, Metaspace by default grows into native (off-heap) memory, largely eliminating the OutOfMemoryError: PermGen space failure mode that came from loading too many classes.
What typically causes a StackOverflowError, and which runtime data area does it come from?
Excessively deep or unbounded recursion, exhausting the current thread's private JVM stack — each recursive call pushes another frame until the stack's space limit is reached.
Why does the heap require explicit synchronization for concurrent access, while a thread's own JVM stack does not?
The heap is a per-JVM area shared and visible to every thread, so concurrent modification of the same object's fields is a genuine data race without synchronization. Each thread's JVM stack is private to that thread, so no other thread can ever access or race on its frames.
What is the runtime constant pool, and where does its data come from?
A per-class table (conceptually part of the method area) of literals and symbolic references that a class's bytecode refers to by index; it's derived from the constant_pool table stored in the class's compiled .class file.
Is the exact internal layout of the heap (e.g. generational young/old regions) something the JVM Specification mandates?
No — the specification only defines the heap as the shared area where objects are allocated and garbage-collected; generational subdivision is a common but JVM-implementation-specific optimization, not a specification requirement.
What happens when this program runs, and which runtime data area is responsible?
It throws java.lang.StackOverflowError. Each call to go pushes a new frame onto the main thread's JVM stack; with no base case, frames are pushed indefinitely until the thread's private JVM stack space is exhausted.
Write a short program whose main method creates many large arrays in a loop without releasing references to them, to illustrate exhausting the heap (do not actually run it without a bounded max heap in a real environment).
import java.util.ArrayList; import java.util.List; class HeapFiller { public static void main(String[] args) { List<int[]> keepAlive = new ArrayList<>(); while (true) { keepAlive.add(new int[1_000_000]); // referenced, so never GC-eligible } } }
Because every allocated array is kept reachable via keepAlive, none of them ever become eligible for garbage collection; the heap (a per-JVM shared area) eventually cannot satisfy a new allocation, and the JVM throws java.lang.OutOfMemoryError: Java heap space once its configured maximum heap size is exhausted.
Explain why two threads calling the same instance method on the same object concurrently do not interfere with each other's local variables, even though the object itself is shared.
The object's fields live on the shared heap, so concurrent field access can race and needs synchronization if it mutates shared state. But each thread's method invocation gets its own separate frame on that thread's own private JVM stack, with its own local variables array and operand stack — local variables and parameters are never shared between threads, only the object's heap-resident fields are.
class Recur {
static void go(int depth) {
go(depth + 1); // no base case
}
public static void main(String[] args) {
go(0);
}
}