Free lesson · Java Basic To Advance
JVM Memory Architecture
Two Memory Models, Same Name
Nikhil here, opening Module 19. Before we go any further, we need to clarify something that confuses thousands of Java engineers: Java has two unrelated "memory models," and they're both important.
Memory Model 1: The Java Memory Model (JMM). Covered in Module 18, Topic 18.3. Defines what threads can observe about each other's writes to shared memory. About happens-before, visibility, ordering. Specified in JLS Chapter 17.
Memory Model 2: The JVM Memory Architecture. Covered in this module. Defines how the JVM organizes memory at runtime — heap, stack, metaspace, native memory. About allocation, lifetimes, garbage collection.
The two are unrelated. JMM is the concurrency contract. JVM Memory Architecture is the runtime layout. When senior engineers say "memory model," they could mean either — context matters.
This Module 19 is about the second one. Where objects live. How they're allocated. When they're collected. How to find leaks. How to tune. Where the heap dump is when you crash with OutOfMemoryError.
The High-Level View
When a JVM process runs, its memory is organized into several regions:
┌─────────────────────────────────────────────────────┐
│ │
│ Native memory (OS-managed, outside JVM control) │
│ ┌───────────────────────────────────────────────┐ │
│ │ │ │
│ │ JVM-managed memory │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ Heap (objects) │ │ │
│ │ │ - Young generation │ │ │
│ │ │ - Old generation │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ Metaspace (class metadata) │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ Thread stacks (one per thread) │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ Code cache (compiled JIT code) │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ Direct buffers, native libraries, etc. │
└─────────────────────────────────────────────────────┘
Each region has a different purpose, lifetime, and management strategy. Let's walk through each.
The Heap
The heap is where Java objects live. Every new allocation, every Integer.valueOf(), every record instantiation — all go on the heap.
User user = new User("alice"); // User object on heap
List<String> list = new ArrayList<>(); // ArrayList on heap, internal array on heap
The heap is:
- Shared across threads — any thread can reference any heap object
- Garbage collected — the JVM reclaims unreachable objects automatically
- Configurable size —
-Xmxsets maximum,-Xmssets initial - Divided into generations — Young and Old, with different collection strategies
The heap is the largest memory region in most JVM processes — typically gigabytes. Topic 19.2 covers it in depth.
The Stack
Each thread has its own stack. The stack stores:
- Method invocation frames
- Local variables (primitives and object references)
- Parameter values
- Return addresses
public void method() {
int x = 5; // x lives on the stack
User u = new User(); // u (the reference) lives on stack; User object on heap
helper(x); // new stack frame pushed
} // frames popped on return
The stack is:
- Per-thread — no sharing
- Not garbage collected — frames are LIFO, automatically reclaimed
- Bounded size —
-Xsssets per-thread limit (default ~512KB-1MB) - Strict ordering — frames push and pop with method calls
The stack stores REFERENCES to heap objects, not the objects themselves. When you write User u = new User(), the reference u is on the stack; the User object is on the heap.
Topic 19.7 covers stack mechanics in detail.
Metaspace
Metaspace stores class metadata — the runtime representation of every class loaded by the JVM:
- Class structure (fields, methods, byte code)
- Method tables (vtables)
- Constant pool (literals)
- Annotations
- Reflection metadata
public class User { ... } // Class metadata in Metaspace
When the JVM loads User.class (the first time someone references it), the class definition goes into Metaspace.
Metaspace:
- Native memory — outside the heap, managed by the JVM but not garbage collected on the usual cycle
- Grows as classes are loaded —
-XX:MaxMetaspaceSizecan cap it - Replaced PermGen in Java 8 — older docs may reference PermGen instead
Before Java 8, this region was called PermGen (Permanent Generation) and was part of the heap. The size was hardcoded by -XX:MaxPermSize. Many production OutOfMemoryError: PermGen problems led to its replacement.
Topic 19.8 covers Metaspace, class loaders, and class memory.
Thread Stacks
Each thread allocates its own stack. The JVM default is typically 512KB-1MB per thread, configurable with -Xss.
For platform threads, this is OS-allocated memory. Creating 1000 threads consumes ~1GB just in stack space.
For virtual threads (Java 21+), stacks are much smaller and managed differently. See Topic 18.14.
Code Cache
The JIT compiler (Just-In-Time) compiles frequently-executed bytecode into native machine code for performance. This compiled code lives in the code cache.
- Stores method native code
- Sized by
-XX:ReservedCodeCacheSize(default 240MB typically) - Can fill up — when full, JIT compilation stops, performance degrades
- Independent of heap
You rarely think about the code cache, but it's a real region. If you see CodeCache is full. Compiler has been disabled. in your logs, the cache is exhausted.
Native Memory
Outside the JVM-managed regions:
- Direct ByteBuffers allocated via
ByteBuffer.allocateDirect()— used by NIO - Memory-mapped files via
FileChannel.map() - Native libraries loaded via JNI (their own allocations)
- OS-level structures (network buffers, file descriptors, etc.)
Native memory is outside the heap and the GC's control. Direct buffers are freed when their wrapper Java object is collected, but with timing the GC chose.
For services using NIO or native libraries, native memory can rival the heap in size.
Where Each Type of Data Lives
A concrete example. Trace through this code:
public class Order {
private final UUID id;
private final List<Item> items;
public Order(UUID id, List<Item> items) {
this.id = id;
this.items = items;
}
}
public void process(Order order) {
String name = order.getName(); // local variable
int count = order.getItems().size();
// ...
}
When the JVM executes this:
On the stack (for the current thread):
- The frame for
process()— return address, parameters namereference (points to a String on heap)countvalue (primitive, directly on stack)
On the heap:
- The
Orderobject (with field references to id, items) - The
UUIDobject - The
List<Item>object (typically ArrayList) - The internal array of the ArrayList
- Each
Itemobject - The
Stringforname
In Metaspace:
- The
Order.classmetadata - The
Item.classmetadata - The
UUID.classmetadata - The
ArrayList.classmetadata - The
String.classmetadata
The heap holds the data. The stack holds references and primitives. Metaspace holds the class definitions.
This separation is critical to understand. When we discuss garbage collection (Topic 19.3+), we're talking about reclaiming heap memory. When we discuss thread limits, we're talking about stack memory. When we discuss "too many classes," we're talking about Metaspace.
A Real Production Concern
A common bug at scale: each request creates "just a few" objects, but cumulatively the rate exceeds what the GC can handle.
In a Cloud Spanner client service:
- 10,000 requests/second
- Each request allocates ~50 small objects (request parsers, response builders, intermediate structures)
- 500,000 objects/second created
- GC must reclaim them at the same rate or the heap grows
If your code is producing garbage faster than the GC can collect, you're going to have a bad time. The fix is usually one of:
- Pool objects (where appropriate; rare in modern Java)
- Use primitive specializations (IntStream not Stream<Integer>)
- Avoid unnecessary allocation in hot paths
- Increase heap (delays the problem; doesn't solve it)
- Switch GCs (G1 → ZGC for low-latency)
This module gives you the tools to recognize, measure, and address these patterns.
A Summary Table
| Region | Stores | Per-thread? | Garbage collected? | Configurable |
|---|---|---|---|---|
| Heap | Objects | No (shared) | Yes | -Xmx, -Xms |
| Stack | Frames, locals, refs | Yes | No (LIFO) | -Xss |
| Metaspace | Class metadata | No | Limited | -XX:MaxMetaspaceSize |
| Code cache | JIT-compiled code | No | Limited | -XX:ReservedCodeCacheSize |
| Native | Direct buffers, JNI | Varies | Indirectly | OS-managed |
What This Module Will Cover
The remaining topics:
- 19.2 — The Heap and Object Allocation (where most action happens)
- 19.3 — Garbage Collection Fundamentals (how reclamation works)
- 19.4 — GC Algorithms (Serial, Parallel, G1, ZGC, Shenandoah)
- 19.5 — Memory Leaks in Production (Emeka's guest topic)
- 19.6 — Reference Types (Strong, Soft, Weak, Phantom)
- 19.7 — The Stack and Method Frames
- 19.8 — Metaspace and Class Loaders
- 19.9 — Tuning the JVM (heap sizing, GC selection)
- 19.10 — Profiling Memory (JFR, heap dumps, MAT)
- 19.11 — The Interview Lens
By the end, you should be able to read a heap dump, identify a memory leak, tune the GC for your workload, and understand what happens when you call new in a hot loop.
graph TD
A["JVM Process Memory"] --> B["JVM-managed"]
A --> C["Native (OS-managed)"]
B --> D["Heap<br/>(objects)"]
B --> E["Thread Stacks<br/>(frames, locals)"]
B --> F["Metaspace<br/>(class metadata)"]
B --> G["Code Cache<br/>(JIT-compiled)"]
D --> H["Young Generation"]
D --> I["Old Generation"]
C --> J["Direct ByteBuffers"]
C --> K["Memory-mapped files"]
C --> L["Native libraries (JNI)"]
M["Two 'Memory Models'"] --> N["JMM (Module 18)<br/>happens-before, visibility"]
M --> O["JVM Memory (Module 19)<br/>heap, stack, metaspace"]
M --> P["Unrelated despite shared name"]
style A fill:#7c3aed,color:#fff
style M fill:#dc2626,color:#fff
This diagram organizes JVM memory architecture. The JVM-managed regions: the heap holds objects allocated via new and is divided into Young and Old generations for generational garbage collection; thread stacks hold method frames, local variables, and references with one stack per thread; Metaspace holds class metadata — the runtime representation of every loaded class, replacing the older PermGen in Java 8; the code cache holds JIT-compiled native code. Native memory is OS-managed, outside JVM control: direct ByteBuffers for NIO, memory-mapped files via FileChannel, native libraries loaded via JNI. Critical distinction: Java has two memory models with the same name. The Java Memory Model (JMM) covered in Module 18 defines happens-before and visibility for concurrency. The JVM memory architecture covered in this Module 19 defines runtime memory layout. They are unrelated despite the shared term — context determines which is meant.
Common Pitfalls
Warning: Confusing the JMM with JVM memory architecture. Different concepts, same name.
Thinking "more heap = better performance." Larger heaps mean longer GC pauses.
Ignoring stack size for high-thread services. 1MB × 1000 threads = 1GB just in stacks.
Forgetting Metaspace exists. Heavy reflection or dynamic class loading can fill it.
Treating direct buffers as heap. They're native memory, freed differently.
Why This Matters in Interviews
Interview: "Describe JVM memory architecture" is a standard senior question. "Where does a String literal live?" "What happens to a local variable when a method returns?" "What's the difference between heap and stack?" are all routine.
Topic 19.11 covers the Interview Lens.
Try It
Exercise: Get oriented:
- Run a simple Java program with
-XX:+PrintGCDetailsand observe the memory regions reported.- Use
jcmd <pid> VM.native_memory(with-XX:NativeMemoryTracking=summaryenabled) to see native memory breakdown.- Identify in your codebase: which classes are loaded? Use
-verbose:classto see.- Allocate a 1GB direct buffer. Verify it doesn't affect heap.
- Configure
-Xss512kand run a deeply recursive program. Observe StackOverflowError.- Run with explicit
-Xms/-Xmx/-XX:MaxMetaspaceSizeand verify the regions cap at those values.Each exercise builds memory architecture intuition.