skip to content

In the JVM, the heap and class-metadata area are shared by all threads while each thread gets its own stack and program-counter register. Why is the split drawn there, and what practical consequences follow from it?

level: seniorimportance: should knowfreq 40%

answer

  1. Private = frames + PC; shared = heap, metadata, code cache
  2. Privacy → no locks, free reclamation on pop
  3. Sharing → GC, safepoints, memory model
  4. Stacks are native memory, ~1 MB each, outside -Xmx
  5. TLABs and escape analysis re-privatize the heap

basics

~20 s

Method frames and the instruction pointer are private to one execution, so they need no coordination — locals are unsynchronized and cheap. Objects outlive the frame that created them and must be reachable from anywhere, so the heap is shared, which forces garbage collection, safepoints and a memory model. Static state is per-type, hence global.

solid answer

~60 s

The split follows lifetime and visibility. A frame — local-variable array, operand stack, return info — exists for exactly one invocation on one thread, and the program counter names that thread's current instruction. Nothing else can observe them, so the JVM needs no locking for locals, stack memory is reclaimed by popping the frame, and analyses like escape analysis can exploit the privacy. Objects, by contrast, outlive the frame that created them and can be handed to any thread, so instances live on a shared heap. That is precisely what forces the expensive machinery: garbage collection with reachability tracing, safepoints where all threads can be stopped consistently, and a written memory model to define cross-thread visibility. Class metadata and statics are shared for the same reason — one loaded type serves all threads, which is why static mutable state is global state. Operationally: each thread costs its own stack (default around 1 MB on 64-bit HotSpot), so thread count multiplies native memory; heap pressure and GC are a whole-process concern; and per-thread allocation buffers exist precisely to make shared-heap allocation behave like private allocation.

go deeper

for a junior

State the split correctly and give the reason in one line: locals belong to one call on one thread, objects can be shared and outlive the call.

for a middle

Explain what privacy buys — no synchronization, reclamation by popping a frame — and what sharing forces: garbage collection and a memory model.

for a senior

Reason about consequences in production: stacks are native memory outside -Xmx, thread count versus -Xss tradeoffs, TLABs making shared allocation cheap.

for a principal

Frame it as the core design tension — sharing is what makes the platform useful and also what costs a collector, safepoints and a memory model — and connect it to sizing and concurrency-model choices.

## The organising principle: lifetime and reachability The JVM does not divide memory by convenience. It divides it by whether a piece of state can, in principle, be observed by more than one thread and whether it outlives the invocation that created it. Everything that cannot be observed elsewhere and dies with its invocation is per thread; everything that can be shared or outlives its creator is shared. ## What is per thread and why **The JVM stack.** Each thread has its own stack of frames. A frame is pushed on method entry and holds a local-variable array (parameters and locals, addressed by slot index), an operand stack (the working area bytecode instructions push and pop), and frame data such as the return address and a reference to the runtime constant pool. Because a frame belongs to exactly one invocation on exactly one thread, no other thread can reach it. That has direct payoffs: - Reads and writes of locals need no synchronization, no memory barriers and no atomicity concerns — the memory model has nothing to say about them. - Reclamation is free: returning pops the frame. Nothing traces stack memory for liveness. - The compiler can keep locals in CPU registers and reorder freely within a thread, because single-thread semantics only have to be preserved as observed by that thread. - Escape analysis becomes valuable: if the JIT proves an object never escapes the frame, it can replace it with scalar values in registers, effectively moving heap-shaped work back into the private region. **The program-counter register.** One per thread, holding the address of the instruction currently executing (undefined while inside a native method). Threads are independently schedulable precisely because each carries its own instruction pointer alongside its own stack. **The native method stack**, similarly per thread, for frames of code entered via JNI. ## What is shared and why **The heap.** Every object instance and every array lives here, because references to them are values that can be stored in fields, arrays or other threads' locals and can outlive the method that allocated them. Sharing is the whole point — and it is expensive. It is exactly why the JVM needs a garbage collector, because no single frame's exit can decide an object is dead; liveness is a global reachability property. It is why the JVM needs safepoints, points where every thread can be brought to a consistent state so the collector can walk stacks for roots. And it is why the JVM needs a written memory model at all: once two threads can touch the same field, the specification must say when a write by one becomes visible to the other. **Class metadata.** Per-type structures — the runtime constant pool, method and field metadata, bytecode — live in a shared area (Metaspace in HotSpot, allocated from native memory since Java 8). A type is loaded once per defining loader and serves every thread. The corollary is the one that bites in practice: static fields are per-type, therefore process-global within their loader, therefore shared mutable state by default with no scoping to save you. **The code cache.** Compiled native code is installed once and executed by any thread, so it too is shared. ## Consequences that matter in production **Thread count buys native memory, not heap.** Each thread's stack is committed native memory outside the Java heap — on 64-bit HotSpot the default reserve is around 1 MB, tunable with `-Xss`. Thousands of threads is therefore a gigabyte-scale native cost independent of `-Xmx`, and it is a common surprise when a container is sized on heap alone. **Deep recursion is a per-thread failure.** Exhausting one thread's stack fails that thread, without touching the heap or other threads. Larger stacks per thread trade directly against how many threads fit. **Shared-heap allocation is made to feel private.** Naive shared allocation would require a lock or an atomic bump on every `new`. HotSpot instead hands each thread a thread-local allocation buffer — a private slice of the heap it bumps a pointer within — and only takes the slow path when the slice is exhausted. This is a deliberate reintroduction of privacy on top of a shared region, purely for speed. **Escape analysis is the same trick from the compiler side.** If the JIT can prove an allocation never becomes reachable outside the frame, it can eliminate it and keep the fields in registers, and can elide locks that are provably unshared. **Static state is global state.** Because metadata is shared, a `static` mutable collection is visible to every thread with no natural lifecycle, which is the structural reason static caches are a recurring source of unbounded growth and of accidental cross-request coupling. ## The crisp answer Private state needs no coordination and can be reclaimed by popping a frame; shared state needs a collector, safepoints and a memory model. The JVM draws the line exactly where sharing becomes possible, and much of its performance engineering — allocation buffers, escape analysis — is about clawing back the privacy that sharing gave away.

  • If the heap is shared, how does HotSpot avoid taking a lock on every object allocation?
    Each thread is given a thread-local allocation buffer, a private chunk carved out of the shared young region. Allocation inside it is a pointer bump with no synchronization, and only exhausting the buffer takes the slow path to request another chunk. It reintroduces per-thread privacy on top of shared memory purely to make allocation nearly as cheap as stack allocation.
  • A service running 4,000 threads with -Xmx2g gets killed by the container for exceeding its memory limit while heap usage looks fine. How does the shared/per-thread split explain that?
    Thread stacks are per-thread native memory allocated outside the Java heap, so they are invisible to heap metrics and to `-Xmx`. At roughly 1 MB reserved per thread, 4,000 threads is on the order of gigabytes of additional footprint. Sizing a container on heap alone ignores stacks, Metaspace, code cache and GC structures, all of which live outside `-Xmx`.
  • Does the garbage collector ever need to look at thread stacks even though they are private?
    Yes — stacks are a primary source of GC roots. References held in local variables and operand stacks keep heap objects alive, so the collector must scan them. That is exactly why safepoints exist: threads must be paused at points where their frames can be interpreted precisely enough to identify which slots hold references.

saying these in an interview costs you the question

  • Saying locals are stored on the heap, or that objects are stored on the stack — the reference may be a local, but the instance is on the heap.
  • Claiming thread stacks come out of the Java heap and are covered by -Xmx.
  • Assuming a StackOverflowError in one thread affects the whole process's memory the way heap exhaustion does.
  • Believing escape analysis 'moves objects to the stack' as a guaranteed language feature rather than an opportunistic JIT optimization that usually scalarizes them.
  • Treating static fields as scoped to a request or a thread; they are per-type and shared.

context