Every Java developer knows objects live on the heap and local variables on the stack.
But when you create a simple Point object with two integers, do you know exactly
how much memory it consumes? If you guessed “8 bytes for the two ints,”
you’re off by 200%.
Your microservice crashes with “only” a million objects. GC pauses freeze traffic at the worst moment. Java 25 cuts heap use by ~22% and CPU time by ~8% in SPECjbb2015 with one flag, but only if you understand what it’s doing. JVM memory fundamentals connect all three.
This is part 1 of the JVM Memory series: the stack, the heap and the frame behind every method call. Part 2 measures what an object really costs, and part 3 shows how virtual threads change allocation. Garbage collection has its own series.
The Two Realms - Stack and Heap
The City Beneath Your Code
Imagine your Java application as a bustling metropolis. The Heap is the residential district-sprawling, shared by all citizens, constantly being cleaned by sanitation trucks (the Garbage Collector). The Stack is the business district-each building serves exactly one purpose, is demolished the moment its work is done, and is private to its owner.
Every thread in your application gets its own Stack-vertical, efficient, self-contained. The Heap is the shared commons where all threads store their objects, references crossing between threads like citizens visiting shared facilities.
The Stack: Where Methods Live and Die
The Stack is perhaps the most elegant data structure in computing. Last In, First Out. When you call a method, a stack frame is pushed. When the method returns, it’s popped. No garbage collection, no memory leaks - just automatic, deterministic cleanup.
Each thread gets its own stack, sized at JVM startup (-Xss flag, default 1MB).
When a thread exceeds this limit? StackOverflowError.
You’ve seen it - infinite recursion, overly deep call chains.
public class StackOverflowDemo {
public static void main(String[] args) {
recursiveMethod(1);
}
private static void recursiveMethod(int depth) {
System.out.println("Depth: " + depth);
recursiveMethod(depth + 1); // Eventually: StackOverflowError
}
}The stack stores primitives and references.
The heap stores everything those references point to.
Simple distinction, massive implications for memory and GC.
The Heap: The Object Repository
If the Stack is where methods execute, the Heap is where objects reside.
Every time you write new Object(), the JVM allocates space on the heap.
Unlike the stack, heap memory isn’t automatically reclaimed when a method
returns - that’s the Garbage Collector’s job.
The heap is shared across all threads. Your main thread creates an object;
your worker thread can reference it. This sharing is powerful but requires
synchronization when mutable objects are accessed concurrently.
Memory Error Types:
- StackOverflowError: Stack too deep (infinite recursion, deep call chains)
- OutOfMemoryError: Java heap space: Heap exhausted (memory leaks, insufficient size)
- OutOfMemoryError: Metaspace: Class metadata exhausted (too many classes loaded)
Stack Frames - The Method’s Workspace
Anatomy of a Frame
Every method invocation creates a stack frame - a self-contained execution environment. The frame contains everything the method needs to execute: its local variables, its computation workspace, and information about where to return when done.
The key insight: The compiler (javac) knows exactly how much memory each
frame needs before the program even runs.
When javac compiles your source code, it analyzes every method to determine:
max_locals: How many slots the local variable array needs (parameters + local variables, accounting for slot reuse)max_stack: The deepest the operand stack will grow during execution
These values are stored in the .class file’s Code attribute.
The JVM reads them when loading the class, so when a method is called,
it knows precisely how much memory to allocate - no guesswork, no waste.
The Local Variable Array
The local variable array is indexed storage for everything local to the method:
- Slot 0:
thisreference (for instance methods only) - Slots 1-N: Method parameters, in declaration order
- Subsequent slots: Local variables declared in the method body
Important: long and double occupy two consecutive slots. Everything else
(int, Object reference, byte, short, char, boolean, float) occupies one slot.
The compiler is smart about reusing slots. If you have:
public void example() {
int x = 1; // Uses slot 1
// ... code using x ...
int y = 2; // Can reuse slot 1 if x is no longer needed
}javac analyzes variable lifetimes and reuses slots when possible, minimizing max_locals.
The Operand Stack
Bytecode works indirectly. Want to add two numbers?
Load them onto the operand stack first. Execute iadd. Store the result back.
Local variables provide storage; the operand stack provides the workspace
where every instruction executes.
Consider this simple addition:
// Java source:
public int add(int a, int b) {
return a + b;
}
// Bytecode (simplified):
// 0: iload_1 // Push local variable 1 (a) onto stack
// 1: iload_2 // Push local variable 2 (b) onto stack
// 2: iadd // Pop two ints, add, push result
// 3: ireturn // Pop result, return to callerThe operand stack starts empty. iload_1 pushes a. iload_2 pushes b.
Now the stack is [a, b] (b on top). iadd pops both, adds them, pushes the result.
Stack is now [result]. ireturn pops and returns it.
The compiler simulates this execution during compilation to find the maximum
stack depth - the max_stack value stored in the .class file.
Dynamic Linking and Exception Handling
Every frame contains a reference to the runtime constant pool of the current class.
When your code calls another method (obj.doSomething()),
the bytecode contains a symbolic reference.
The JVM resolves this to the actual method address - dynamic linking.
Once resolved, the JVM caches the direct reference for subsequent calls.
The exception table maps ranges of bytecode (try blocks) to exception handlers (catch blocks). Each entry contains:
- Start PC, End PC: The protected code range
- Handler PC: Where to jump if exception occurs
- Catch type: The exception class to catch
When an exception is thrown, the JVM searches the current frame’s exception table. If no match, it pops the frame and checks the caller’s table - unwinding the stack until a handler is found or the thread terminates.
Key Takeaway: Every method call creates a frame with dedicated space for data and computation. The compiler determines exact memory requirements (
max_localsandmax_stack) ahead of time, so the JVM allocates precisely what’s needed.
Summary
- The Two Realms: Stack (per-thread, automatic, LIFO) vs Heap (shared, GC-managed)
- Stack Frames:
max_localsandmax_stackdetermined at compile time, frame components
References
-
Java Virtual Machine Specification
- Oracle. “Chapter 2: The Structure of the Java Virtual Machine.” Java SE Specifications, 2025.
- https://docs.oracle.com/javase/specs/jvms/se25/html/jvms-2.html
-
JVM Internal Architecture
- Zen, Nuwan. “Internal architecture of the Java Virtual Machine (JVM).” Medium, September 2021.
- https://nuwanzen.medium.com/internal-architecture-of-the-java-virtual-machine-jvm-2d3868ff528