Introduction
Most Java developers spend years writing business logic without understanding where their objects actually live in memory. When the application crashes with OutOfMemoryError at 3 AM, suddenly that knowledge becomes critical. This guide walks you through JVM memory explained: heap, stack and metaspace explained—the three core memory regions that determine whether your application scales or fails.
Before you adjust heap flags or blame the garbage collector, you need to know what each memory area holds, what its limits are, and what happens when it fills up. We'll cover the areas where your objects live, how to measure real usage, and why guessing at JVM flags is a losing game.
Memory Areas: Where Your Data Lives
The JVM divides memory into separate regions, each with its own purpose, limits, and failure modes. Understanding the difference is the foundation of effective memory tuning.
The Heap: Objects and Arrays
The heap is where most of your application's memory goes. Every object you create—every String, ArrayList, custom class instance—lives here. Arrays live on the heap too. The heap is shared across all threads, managed by the garbage collector, and it's where most memory problems occur.
You set heap limits with -Xms (initial size) and -Xmx (maximum size). If you create more live objects than the heap can hold, the garbage collector will run. If GC can't free enough space, you get OutOfMemoryError: Java heap space.
// Every object here lives on the heap
List<String> names = new ArrayList<>();
for (int i = 0; i < 1_000_000; i++) {
names.add("User_" + i); // Millions of String objects on heap
}
// If heap is too small and GC can't free them fast enough, crash.The heap is divided into generations (Young and Old in most collectors). New objects go to Young, and if they survive long enough, they move to Old. This split helps the GC optimize: younger objects die faster, so the GC scans Young frequently and Old less often.
The Stack: Thread-Local Frames and Primitives
Each thread gets its own stack. It holds method call frames and local primitive variables. When a method is called, a frame is pushed onto the stack. When the method returns, the frame is popped. This is fast and deterministic—no GC needed.
Stack size is set per-thread with -Xss (default is usually 1 MB on 64-bit JVMs). If a thread's stack fills up, you get StackOverflowError.
public class StackExample {
// Local primitives and object references live on the stack
public void doWork() {
int counter = 0; // primitive, on stack
String name = "Alice"; // reference on stack, String object on heap
byte[] buffer = new byte[1024]; // reference on stack, array on heap
recursiveCall(counter); // each call pushes a frame onto the stack
}
public void recursiveCall(int depth) {
if (depth > 10000) {
recursiveCall(depth + 1); // unbounded recursion = stack overflow
}
}
}In practice, StackOverflowError is almost always caused by unbounded recursion or a logic bug, not a too-small stack. Raising -Xss won't save you; you need to fix the recursion.
Metaspace: Class Metadata
Metaspace holds class metadata: bytecode, method signatures, constant pools, field data, and annotations. Every class you load consumes metaspace. If you dynamically generate classes (bytecode generation, proxies, reflection frameworks), you can fill metaspace.
Metaspace is allocated from native memory (not the Java heap), so its limit doesn't count against -Xmx. You control it with -XX:MetaspaceSize and -XX:MaxMetaspaceSize. The default max is "unlimited" (uses available OS memory), which is usually fine unless you have a classloader leak.
// Example: dynamic proxy generation creates new classes
for (int i = 0; i < 100_000; i++) {
Class<?> proxyClass = Proxy.getProxyClass(
Thread.currentThread().getContextClassLoader(),
SomeInterface.class
);
// Each proxy is a unique class loaded into metaspace
}Metaspace grows with your application's complexity. Most applications won't hit it. But if you generate classes dynamically or have a classloader leak (old classloader and its classes never unloaded), metaspace can grow unchecked and trigger OutOfMemoryError: Metaspace.
Direct Memory: Off-Heap Buffers
NIO buffers, some databases drivers, and some frameworks allocate memory outside the heap using ByteBuffer.allocateDirect(). This memory is native, not managed by the garbage collector, and doesn't count toward -Xmx. You limit it with -XX:MaxDirectMemorySize.
Direct memory is useful for I/O-heavy workloads because it avoids copying data between Java buffers and native buffers. But if you allocate too much, you can hit OutOfMemoryError: Direct buffer memory.
| Area | Holds | Per-Thread? | Managed by GC? | Common Limit Flag | Common Failure |
|---|---|---|---|---|---|
| Heap | Objects, arrays | No (shared) | Yes | -Xmx |
OutOfMemoryError: Java heap space |
| Stack | Method frames, local primitives | Yes (one per thread) | No (automatic) | -Xss |
StackOverflowError |
| Metaspace | Class metadata, bytecode | No (shared) | Yes (when class unloads) | -XX:MaxMetaspaceSize |
OutOfMemoryError: Metaspace |
| Direct Memory | NIO buffers, off-heap | No (shared) | No (explicit or finalizer) | -XX:MaxDirectMemorySize |
OutOfMemoryError: Direct buffer memory |
OutOfMemoryError: Java Heap Space—The Most Common Crash
This error means the heap is full, garbage collection can't free enough space, and the JVM can't allocate memory for a new object. It's tempting to just raise -Xmx, but that's often the wrong fix.
Before you increase heap size, you need to know: Are we leaking objects? Or are we legitimately storing that many objects?
A heap dump answers this question. Take one when the error occurs (or before it crashes), and inspect it with a tool like Eclipse MAT or Yourkit. Look for objects that should have been garbage collected but weren't—a sign of a leak.
// Example: accidental object leak via static collection
public class UserCache {
private static final Map<Integer, User> cache = new HashMap<>();
public static void cacheUser(User user) {
cache.put(user.id(), user); // objects never removed = leak
}
}
// Fix: use a bounded cache or weak references
private static final Map<Integer, User> cache = new WeakHashMap<>();If you dump the heap and see millions of legitimate user objects because your application legitimately needs to hold them, then yes, raise -Xmx. But if you see strings from six months ago or duplicate instances of something that should be a singleton, you have a leak.
Tuning steps: (1) take a heap dump, (2) inspect for leaks, (3) if no leaks, raise -Xmx in 25–50% increments until the error stops. (4) Monitor real usage with JFR or jcmd to ensure you're not wasting memory.
StackOverflowError—Usually Code, Not Configuration
This error means a thread's call stack ran out of space, almost always because of unbounded recursion or mutual recursion that wasn't caught by logic.
// Classic: forgot the base case
public int factorial(int n) {
return n * factorial(n - 1); // no base case, recurses forever
}
// Mutual recursion gone wrong
public void methodA() {
methodB();
}
public void methodB() {
methodA(); // infinite loop through the stack
}Raising -Xss is a band-aid. You need to fix the recursion. Add a base case, a depth limit, or use iteration instead.
In rare cases, a method that allocates huge local arrays or many local variables might genuinely need more stack space. Then -Xss can help. But test it—on some JVMs, raising -Xss reduces the number of threads you can create (because OS memory is finite), so you might trade one problem for another.
Measure First: jcmd and JFR Show Real Usage
Guessing at JVM flags is almost always wrong. Use tools to see what's actually happening.
jcmd: Quick Snapshots
Get a heap histogram without taking a full dump:
jcmd <pid> GC.class_histogram | head -20This shows the top object types by count and total memory. If you see millions of String or byte[], that's a clue.
Get memory usage:
jcmd <pid> VM.memory_usageThis gives you Heap (Young/Old), Metaspace, and Code Cache usage in real time.
JFR: Continuous Profiling
Java Flight Recorder (JFR) is a built-in low-overhead profiler. Enable it on startup:
java -XX:+UnlockCommercialFeatures -XX:+FlightRecorder \
-XX:StartFlightRecording=duration=60s,filename=recording.jfr \
MyAppThen analyze the recording with JDK Mission Control. You'll see: allocation rate, garbage collection frequency and duration, memory leaks (objects not freed), and hot allocations (where objects are created). This is how you actually know what's consuming memory.
Heap Dumps
When an OutOfMemoryError occurs, enable a dump:
java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/dumps MyAppOpen the dump in Eclipse MAT, sort by "Retained Heap", and look at the top objects. Retained heap is the memory that would be freed if that object were garbage collected—it shows which objects are actually holding onto memory.
Common Mistakes
- Raising -Xmx without investigating leaks. If you have a leak, a bigger heap just delays the crash.
- Raising -Xss to fix StackOverflowError. That's treating the symptom. Fix the recursion.
- Setting -Xms and -Xmx to different values. This forces the JVM to resize the heap, which pauses the application. Set them equal for predictable latency.
- Ignoring GC logs. Enable
-Xlog:gcand watch: if Full GC runs every few seconds, your application is memory-starved, not misconfigured. - Tuning without baseline measurements. Use jcmd and JFR first. Blindly changing flags is guesswork.
Key Takeaways
- The heap holds objects and arrays and is managed by GC. The stack holds method frames and local primitives per thread. Metaspace holds class metadata from native memory. Direct memory holds NIO buffers off-heap.
- OutOfMemoryError: Java heap space means either a leak or legitimate high memory use. Take a heap dump and inspect before raising -Xmx.
- StackOverflowError is almost always unbounded recursion, not a too-small stack. Fix the code, don't just raise -Xss.
- Use jcmd and JFR to measure real memory usage. Guessing at flags rarely works and often makes things worse.
- Set -Xms and -Xmx equal for predictable performance. Enable heap dumps on OOM and GC logs to see what's actually happening.






Comments
Loading comments…