JVM memory & garbage collection
Stack vs heap, class loading, generations, G1/ZGC, memory leaks, heap flags and diagnostic tools.
Java manages memory for you: you create objects with new, and the garbage collector (GC) frees them when they are no longer needed. You never call free(). But "automatic" doesn't mean "magic". Understanding how the JVM organises memory helps you explain StackOverflowError and OutOfMemoryError, avoid memory leaks, tune applications, and answer a classic interview topic.
The big picture#
Stack vs heap in action#
- Each method call pushes a frame; returning pops it. Stack memory is reclaimed instantly and automatically.
- Primitives and references that are local variables live in the frame; the objects they point to live on the heap. (Fields of an object, even primitive ones, live inside the object on the heap.)
- Each thread has its own stack, which is why local variables are inherently thread-safe, while heap objects are shared.
StackOverflowError#
Each thread's stack has a fixed size (often 512 KB to 1 MB by default; set with -Xss). Infinite or very deep recursion exhausts it:
The exact depth depends on frame size and JVM settings. The fix is almost always a missing base case, or converting deep recursion into a loop.
How garbage collection works#
An object is garbage when no chain of references leads to it from a GC root. GC roots include:
- local variables and parameters on active threads' stacks;
staticfields of loaded classes;- active threads themselves, and JNI references.
Cycles are not a problem: if objects A and B reference each other but nothing reachable references either, both are collected. Java uses tracing (mark reachable objects, reclaim the rest), not reference counting.
Generational collection#
Most objects die young: temporary strings, iterators, lambdas, request objects. The JVM exploits this weak generational hypothesis:
- New objects are allocated in Eden (part of the young generation). Allocation is extremely fast, basically bumping a pointer.
- When Eden fills, a minor GC copies the few surviving objects into a survivor space and wipes Eden in one go.
- Objects that survive several minor GCs are promoted to the old generation.
- The old generation is collected less often, by a major/mixed collection, which is more expensive.
Minor GCs are quick because they touch only live objects, and in the young generation there are few of those.
Garbage collectors in modern Java#
GCs pause application threads ("stop-the-world") for some phases; modern collectors do most work concurrently to keep pauses short. For most applications: stick with G1, set a sensible -Xmx, and only switch collectors when measurements show a need (e.g. ZGC for latency-sensitive services).
Key JVM memory flags#
Seeing memory from code#
Run it with different limits to see the effect:
(Your numbers will differ.) System.gc() only suggests a collection. Production code should almost never call it.
OutOfMemoryError and memory leaks#
Java can't leak memory the C way, but it can retain objects you no longer need: if something reachable keeps referencing them, the GC must keep them. Classic leak sources:
- static collections that only grow (caches without eviction);
- listeners or callbacks registered and never removed;
ThreadLocalvalues in pooled threads that are never cleared;- unclosed resources (streams, connections) holding buffers;
- huge
HashMaps keyed by objects with brokenequals/hashCode, so "duplicates" pile up.
The exact number depends on the JVM version and collector. With G1, each 1 MB array counts as a "humongous" object and uses more than its own size in heap regions, so 64 MB fills up after roughly 30 buffers.
Fixes: bound caches (an LRU LinkedHashMap, or a library like Caffeine), remove listeners, clear ThreadLocals, and use try-with-resources.
Reference types for caches
java.lang.ref offers references the GC treats specially:
WeakReference<T>: doesn't keep the object alive; cleared at the next GC once only weakly reachable.WeakHashMapuses weak keys.SoftReference<T>: cleared only when memory is tight; sometimes used for memory-sensitive caches.
Diagnostic tools that come with the JDK#
Open heap dumps with Eclipse MAT or VisualVM, and flight recordings with JDK Mission Control, to find which objects dominate memory and who holds onto them.
The JIT compiler, briefly#
The JVM starts by interpreting bytecode, profiles which methods are hot, and compiles them to optimised machine code (the C1 and C2 compilers, together called tiered compilation). Optimisations include inlining, loop unrolling and escape analysis, which can avoid heap allocation entirely for objects that never escape a method. That's why Java programs get faster after warming up, and why micro-benchmarks need a tool like JMH.
Practical guidelines#
- Set
-Xmx(orMaxRAMPercentagein containers) explicitly in production. - Prefer the default G1 unless measurements say otherwise.
- Don't prematurely optimise allocation; short-lived objects are cheap. Avoid retaining objects, not creating them.
- Enable
-XX:+HeapDumpOnOutOfMemoryErrorso failures leave evidence. - Monitor GC pause times and heap usage (JFR, Micrometer/Prometheus in Spring Boot).
Common mistakes#
- Believing
obj = nullimmediately frees memory. - Calling
System.gc()to "fix" memory problems. - Unbounded static caches and collections.
- Giving a container 1 GB of RAM and the JVM
-Xmx1g. Leave room for Metaspace, thread stacks and native memory. - Confusing
StackOverflowError(recursion depth) withOutOfMemoryError(heap).
What's next#
Next, we connect Java to a real database: JDBC with MySQL.
Check your understanding
Quick quiz
1.In
void m() { Order o = new Order(); }, where do the variableoand the Order object live?2.When is an object eligible for garbage collection?
3.Which flag sets the maximum heap size to 2 GB?
Finished reading?
Mark this lesson complete to track your progress.