Java Memory and Garbage Collection: Stack, Heap and Common Errors
Codevity Admin · 10/25/2026
Step 27 of 38 · Java Interview Preparation for Beginners · Stage 5: Concurrency and the JVM
Codevity Admin · 10/25/2026
Java frees memory for you, so interviewers ask a different question: do you know where your objects live and when they die? This article covers the memory areas, how garbage collection (GC) decides what to free, and the two errors every developer eventually sees.
| Area | Holds | Shared? | Typical failure |
|---|---|---|---|
| Heap | All objects and arrays | Shared by all threads | OutOfMemoryError: Java heap space |
| Stack (one per thread) | Frames with local variables, parameters, references | Per thread | StackOverflowError |
| Metaspace | Class metadata (it replaced PermGen in Java 8) | Shared | OutOfMemoryError: Metaspace |
| Program counter | The current instruction of each thread | Per thread | none |
| Native method stack | Frames for native (non-Java) code | Per thread | rare |
In Person p = new Person("Asha"); the reference p lives on the stack (as a local variable) and the Person object lives on the heap. Primitive locals live in the stack frame itself. A reference stored in a field lives inside its object on the heap. Strings in the string pool also live on the heap.
An object can be collected when it is unreachable from every GC root. The roots are local variables on live thread stacks, static fields, active threads and JNI references. Reference counting is not used, so two objects that point at each other but are otherwise unreachable are still collected.
// Illustrative: Person stands for any class
Person a = new Person("Asha"); // reachable through a
Person b = a; // still one object, two references
a = null; // the object is still reachable through b
b = null; // now it is eligible for collection"Eligible" does not mean "collected right now". System.gc() is only a hint, and you should never depend on it.
Most objects die young, so the heap is divided by age:
| Collector | Notes |
|---|---|
| G1 (default since Java 9) | Splits the heap into regions, collects the most garbage first, aims for a pause-time goal. A good default |
| ZGC | Very low pauses, even on multi-terabyte heaps. Generational in Java 21 () |
| Parallel | Maximizes throughput, longer pauses. Batch jobs |
| Serial | Single-threaded, tiny heaps and containers with one CPU |
A stop-the-world pause is when the application threads are halted so the collector can work. Modern collectors keep these pauses short.
Each method call pushes a frame onto the thread's stack, and the stack has a fixed size (about 512 KB to 1 MB by default, set with ). Endless or very deep recursion fills it:
The fix is a correct base case, or converting the recursion into a loop. Catching is for demonstrations only.
This means the heap cannot supply enough room even after a full collection. The usual causes are loading too much data at once (for example into a list), a cache without a size limit, or a genuine leak.
With this prints .
A leak in Java is an object that is still reachable but no longer needed. The collector cannot free what you still point at. The usual suspects:
Use a bounded cache (an LRU cache built on LinkedHashMap, or a library such as Caffeine) or / when entries should vanish if nothing else needs them.
| Type | Collected when | Typical use |
|---|---|---|
| Strong () | Never while reachable | Everything normal |
| The JVM is short of memory | Memory-sensitive caches | |
| At the next GC if only weakly reachable | , canonicalizing maps | |
| After finalization, for cleanup bookkeeping | Advanced; see |
is deprecated (for removal since Java 18). For cleanup, use and , or .
| Flag | Meaning |
|---|---|
| / | Initial and maximum heap size |
| Stack size per thread | |
| , | Choose the collector |
| Write a heap dump file when the heap runs out | |
| Print GC activity (Java 9 and later) |
To diagnose a problem: look at output (are collections frequent, is the old generation always full?), take a heap dump () and open it in Eclipse MAT or VisualVM, and take thread dumps with .
-XX:+UseZGC -XX:+ZGenerational-XssStackOverflowErrorSELECT *-Xmx32mcaught Java heap spaceThreadLocalhashCodeequalsWeakHashMapWeakReferenceObject o = new Object()SoftReferenceWeakReferenceWeakHashMapPhantomReferenceCleanerfinalize()try-with-resourcesAutoCloseablejava.lang.ref.Cleaner-Xms512m-Xmx2g-Xss1m-XX:+UseG1GC-XX:+UseZGC-XX:+HeapDumpOnOutOfMemoryError-Xlog:gc-Xlog:gcjcmd <pid> GC.heap_dump file.hprofjstackSystem.gc()StackDemojava -Xss256k StackDemo.java-Xss4mHeapDemo-Xmx32m-XX:+HeapDumpOnOutOfMemoryErrorpublic class StackDemo {
static int depth = 0;
static void recurse() {
depth++;
recurse(); // no base case: it never stops
}
public static void main(String[] args) {
try {
recurse();
} catch (StackOverflowError e) {
System.out.println("recursion too deep"); // recursion too deep
System.out.println(depth > 1000); // true
}
}
}// Illustrative: run it with a small heap, for example java -Xmx32m HeapDemo.java
import java.util.*;
public class HeapDemo {
public static void main(String[] args) {
List<byte[]> hoard = new ArrayList<>();
try {
while (true) {
hoard.add(new byte[1024 * 1024]); // keep 1 MB blocks reachable forever
}
} catch (OutOfMemoryError e) {
hoard = null; // free the blocks first, then report
System.out.println("caught " + e.getMessage());
}
}
}// Illustrative: a leak, because nothing ever removes entries from the static map.
public class LeakyCache {
private static final java.util.Map<String, byte[]> CACHE = new java.util.HashMap<>();
static byte[] load(String key) {
return CACHE.computeIfAbsent(key, k -> new byte[1_000_000]); // grows with every distinct key
}
}