Memory Management
By the end you'll be able to picture where every value lives (stack vs heap), explain how Java's generational garbage collector reclaims objects for you, pick the right GC and heap flags, and hunt down the leaks the GC can't fix.
Learn Memory Management in our free Java course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick reference.
Part of the free Java course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
Before You Start
This lesson assumes you're comfortable creating objects with new from Object-Oriented Programming , using lists and maps from Collections , and the idea of threads from Multithreading . Memory management touches all three — your data-structure choices, your thread count, and your object lifetimes all decide how much memory your program uses.
Real-World Analogy: A Desk and a Warehouse
💡 Analogy: Think of the stack as your desk . It's small and tidy. When you start a task you lay out exactly what you need on top; when the task is done you sweep it off — instantly, no thought required. Every worker (thread) has their own desk.
The heap is the warehouse out back. It's huge, shared by everyone, and holds the bulky items (your objects). Nobody clears their own warehouse junk, so a janitor — the garbage collector — walks the aisles, and anything no one is still holding a tag for gets hauled away. You never throw items out yourself; you just stop holding their tags, and the janitor does the rest.
A "tag" here is a reference — a variable pointing at an object. An object survives as long as something can still reach it through a chain of tags. Once the last tag is gone, the object is garbage and the janitor is free to reclaim its space.
1️⃣ Stack vs Heap — Where Values Live
When a method runs, Java gives it a stack frame — a slot that holds its primitive values (like an int ) and its references (the tags that point at objects). When the method returns, that frame is popped off and its memory is reclaimed instantly. The actual objects those references point at live on the heap , which is shared by every thread and managed by the garbage collector.
So int count = 2; stores the 2 right on the stack, but User u = new User("Alice"); stores the object on the heap and only keeps the small reference u on the stack.
Feature
Stack
Heap
Stores
Primitives, references, call frames
Objects, arrays
Lifetime
Until the method returns
Until no reference remains (then GC)
Scope
One per thread (private)
One, shared by all threads
Speed
Very fast (push/pop, LIFO)
Slower (allocation + GC managed)
Error when full
StackOverflowError
OutOfMemoryError
2️⃣ The Object Lifecycle & Generational GC
Every object goes through the same arc: created with new → in use while something references it → unreachable once the last reference is dropped → collected when the GC reclaims its space. You only control the first three; the JVM owns the last.
The crucial observation that makes GC fast is the weak generational hypothesis : most objects die young . So the heap is split into generations:
- Young generation — where new objects are born. It has an Eden space (allocation happens here) plus two small survivor spaces. A minor GC cleans it: cheap and frequent, because nearly everything in it is already dead.
- Old generation (tenured) — objects that survive enough minor GCs get promoted here. A major / full GC cleans it: slower and rarer, because long-lived objects accumulate slowly.
Collecting the small young space often, and the big old space seldom, is far cheaper than scanning the whole heap every time. That single trick is why automatic memory management can keep up with millions of allocations per second.
You can watch this happen by adding -Xlog:gc when you launch. The worked example below floods the young generation with short-lived arrays so minor "Pause Young" collections fire repeatedly.
3️⃣ GC Algorithms — G1, ZGC, Parallel & Serial
The JVM ships several collectors, and you choose one with a launch flag. They trade off the same two things: throughput (how much real work gets done) versus pause time (how long the app freezes during a collection). You rarely need to switch from the default — but knowing the menu helps you tune later.
Collector
Flag
Best for
Serial GC
-XX:+UseSerialGC
Single core, small heaps, embedded
Parallel GC
-XX:+UseParallelGC
Batch jobs — max throughput, pauses OK
G1 GC
-XX:+UseG1GC
Default since Java 9 — balanced, general server apps
ZGC / Shenandoah
-XX:+UseZGC
Latency-sensitive — sub-10ms pauses, big heaps
G1 ("Garbage First") divides the heap into many small regions and collects the ones with the most garbage first, aiming for a pause-time goal you set with -XX:MaxGCPauseMillis . ZGC does almost all its work concurrently with your app, so pauses stay tiny even on multi-gigabyte heaps — at a small throughput cost. Parallel uses every core to collect as fast as possible but stops the world while it does, which is fine for batch work.
4️⃣ Reference Strengths — Strong, Soft, Weak, Phantom
Not all references are equal. Java lets you choose how tightly a variable holds an object, which tells the GC how eager it can be to collect it. Wrappers in java.lang.ref give you the weaker grips, which are the building blocks for leak-free caches.
Reference
When the GC collects it
Typical use
Strong
Never, while it stays reachable
Ordinary variables (the default)
Soft
Only when memory is running low
SoftReference — memory-sensitive caches
Weak
At the next GC, once no strong ref remains
WeakReference , WeakHashMap keys
Phantom
After the object is finalized, for cleanup signalling
PhantomReference + a ReferenceQueue
A soft reference is a great cache: the JVM keeps your cached values around until it actually needs the memory, then frees them rather than throwing OutOfMemoryError . A weak reference is more aggressive — it survives only until the next collection — which is exactly what WeakHashMap uses so entries vanish once their keys are gone. Phantom references never let you retrieve the object ( get() always returns null ); they exist only to tell you "this object has been collected, run your cleanup now".
5️⃣ Memory Leaks in Java — Yes, They Still Happen
A garbage collector frees unreachable objects. A leak is when you keep objects reachable by accident, so the GC is forbidden from collecting them and the heap slowly fills until you hit OutOfMemoryError . Three patterns cause the vast majority of real-world Java leaks:
- Static collections that only grow. A static List or static Map lives for the whole program. If you keep adding and never remove, every element stays reachable forever. Fix: bound the size, evict old entries, or hold the values as soft/weak references.
- Listeners and callbacks you never unregister. Registering a listener stores a reference to your object inside the event source. If you forget to unregister it when you're done, the source keeps your object alive. Fix: always pair addListener with removeListener (often in a finally or a close() method).
- ThreadLocal values on pooled threads. A ThreadLocal attaches data to a thread. In a thread pool the threads never die, so that data lingers across tasks and leaks. Fix: always call threadLocal.remove() in a finally block when the task ends.
The tell-tale sign of a leak is a heap that keeps climbing across full GCs and never comes back down. To confirm it, capture a heap dump ( -XX:+HeapDumpOnOutOfMemoryError ) and open it in a tool like VisualVM or Eclipse MAT to see which objects are pinning memory.
🎯 Your Turn #1 — Make an Object GC-Eligible
Fill in the four blanks so the object becomes eligible for garbage collection only after the last reference is dropped. The // 👉 hints tell you exactly what to write. Check your run against the expected output in the comments.
🎯 Your Turn #2 — Spot the Static-Collection Leak
This program demonstrates the #1 Java leak: a static list that only ever grows. Fill in the blanks to add blocks and report how many the list is still pinning in memory. The fix is in the final line — read it.
🏆 Mini-Challenge — Build a Self-Cleaning Cache
Time to fade the scaffolding. You get only a comment outline — no filled-in logic. Build a cache whose entries disappear automatically when their key is no longer referenced, using what you learned about WeakHashMap and weak references. The expected output is in the comments so you can self-check.
6️⃣ Tuning the Heap — -Xmx, -Xms & Friends
You size the heap and pick the collector at launch time , with JVM flags — not in Java code. The two you'll set most often are -Xms (the initial heap size) and -Xmx (the maximum heap size). If the heap needs more than -Xmx after a full GC, you get OutOfMemoryError: Java heap space .
Two rules of thumb cover most cases: keep -Xmx at roughly 75% of the machine's RAM (leave room for the OS, thread stacks, and off-heap memory), and in production set -Xms = -Xmx so the JVM grabs the full heap up front and never pauses to resize it.
Common Errors (and How to Fix Them)
- ❌ java.lang.OutOfMemoryError: Java heap space — the heap filled up. Either you genuinely need more memory (raise -Xmx ) or, more often, you have a leak. Add -XX:+HeapDumpOnOutOfMemoryError and inspect the dump to find what's pinning objects.
- ❌ java.lang.StackOverflowError — the stack , not the heap, overflowed. Almost always unbounded recursion (a method calling itself with no base case). Fix the recursion; only raise -Xss if the deep recursion is genuinely intentional.
- ❌ OutOfMemoryError: GC overhead limit exceeded — the JVM is spending > 98% of its time in GC and reclaiming almost nothing. A near-full heap with a slow leak. Profile and fix the leak; bumping -Xmx only delays it.
- ❌ Heap keeps growing across full GCs — the classic leak signature. Check your static collections, unregistered listeners, and ThreadLocal s that never call remove() .
- ❌ OutOfMemoryError: unable to create new native thread — not a heap problem at all. Each thread reserves stack space outside the heap; you've created too many. Use a bounded thread pool and lower -Xss if needed.
Pro Tips
💡 Set -Xms = -Xmx in production to avoid heap-resize pauses during traffic spikes.
💡 Always enable -XX:+HeapDumpOnOutOfMemoryError — when OOM hits at 3 AM you'll have the evidence to diagnose it.
💡 Don't call System.gc() in real code — it's only a hint and usually triggers a costly full GC at the worst moment.
💡 ZGC (Java 15+) holds pauses under ~10 ms even on multi-gigabyte heaps — reach for it when latency matters more than raw throughput.
📋 Quick Reference
Concept
API / Flag
Use case
Initial / max heap
-Xms / -Xmx
Size the heap (set equal in prod)
Per-thread stack
-Xss
Shrink when running many threads
Pick a collector
-XX:+UseG1GC / +UseZGC
Balanced vs low-latency
See GC happen
-Xlog:gc
Log every collection
Dump on OOM
-XX:+HeapDumpOnOutOfMemoryError
Post-mortem leak analysis
Cache-friendly ref
WeakReference<T> / SoftReference<T>
Let the GC reclaim cached values
Self-evicting map
WeakHashMap
Entries drop when keys are gone
Inspect a live JVM
jcmd / jmap / VisualVM
Heap usage & object histograms
Frequently Asked Questions
🎉 Lesson Complete!
Nicely done. You can now place any value on the stack or the heap, trace an object from new to GC-eligible, explain why generational GC (young/old, minor/major) makes automatic memory management fast, pick between G1, ZGC, and Parallel, reach for soft/weak references to build leak-free caches, recognise the three classic leak patterns, and size the heap with -Xms / -Xmx .
Next up: JVM Internals — classloading, JIT compilation, and how your bytecode actually executes.
Practice quiz
After User alias = alice; what does (alias == alice) print?
- false
- Compile error
- true
- null
Answer: true. Assigning a reference copies the tag, not the object, so both names point at the same heap object — true.
Where do objects created with 'new' live?
- The heap
- The stack
- The PC register
- The metaspace
Answer: The heap. Objects and arrays live on the heap, shared by all threads. The stack holds references and primitive locals.
When does an object become eligible for garbage collection?
- When you call free()
- When the method that created it starts
- Immediately after 'new'
- When the last reference to it is dropped
Answer: When the last reference to it is dropped. An object is eligible for GC once no live reference can reach it. Java has no free() or delete().
Why is Java's garbage collector called 'generational'?
- It runs once per generation of the JVM
- It splits the heap into young and old because most objects die young
- It collects in alphabetical order
- It only collects static fields
Answer: It splits the heap into young and old because most objects die young. The weak generational hypothesis says most objects die young, so young is collected cheaply and often, old rarely.
What does running out of stack space throw?
- StackOverflowError
- OutOfMemoryError
- NullPointerException
- GC overhead limit exceeded
Answer: StackOverflowError. Too-deep recursion overflows a thread's stack and throws StackOverflowError; the heap filling throws OutOfMemoryError.
Which collector has been the default since Java 9?
- Serial GC
- Parallel GC
- G1 GC
- ZGC
Answer: G1 GC. G1 (Garbage First) is the balanced default since Java 9; ZGC targets very low pauses, Parallel targets throughput.
When does a WeakReference's referent get collected?
- Never while the JVM runs
- At the next GC once no strong reference remains
- Only on System.exit
- Only when memory is critically low
Answer: At the next GC once no strong reference remains. A weak reference does not prevent collection — once no strong ref remains, the next GC can reclaim the object.
Which is a classic cause of a memory leak despite having a garbage collector?
- Using local variables
- Calling new too often
- Using try-with-resources
- A static collection that only ever grows
Answer: A static collection that only ever grows. A static collection that only grows keeps every element reachable forever, so the GC can never collect them.
What does System.gc() actually do?
- Immediately frees all garbage
- Is only a hint the JVM may ignore
- Throws OutOfMemoryError
- Clears the stack
Answer: Is only a hint the JVM may ignore. System.gc() is just a hint; the JVM decides when to collect. In practice it often triggers a costly full GC.
In production, why set -Xms equal to -Xmx?
- To use less RAM
- To disable the GC
- So the JVM grabs the full heap up front and avoids resize pauses
- To allow unlimited heap growth
Answer: So the JVM grabs the full heap up front and avoids resize pauses. Setting initial heap equal to max means the JVM allocates the whole heap immediately and never pauses to resize it.
Continue this course
- Previous: Previous Lesson
- Next: Next Lesson