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.

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.

💡 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.

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.

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:

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.

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.

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.

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.

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".

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:

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.

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.

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.

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.

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.

💡 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.

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.