Java Memory Management
How the JVM manages memory: heap vs stack, object lifecycle, generational garbage collection, and common GC algorithms.
Heap vs Stack
The JVM divides memory into two main regions:
- Stack: stores method frames, local variables, and references. Each thread has its own stack. Memory is allocated/freed automatically as methods are called and return.
- Heap: stores all objects and class instances. Shared across threads. Managed by the Garbage Collector (GC).
Java
public void example() {
int x = 10; // stored on stack
String s = new String("hello"); // reference 's' on stack, object on heap
}
// After method returns: x and s are popped off stack.
// The String object on heap is eligible for GC when no references remain.
Heap Generations
The JVM heap is divided into generations to optimize GC performance, based on the observation that most objects die young (the generational hypothesis).
- Young Generation: newly created objects go here. Divided into Eden space and two Survivor spaces (S0, S1). Minor GC runs frequently and is fast.
- Old Generation (Tenured): objects that survive multiple Minor GCs are promoted here. Major GC (Full GC) runs less often but takes longer.
- Metaspace (Java 8+, replaces PermGen): stores class metadata. Grows dynamically, limited by native memory.
Java
// JVM flags to view GC activity
// -verbose:gc
// -XX:+PrintGCDetails
// -Xms256m -Xmx1g (initial and max heap size)
// -XX:NewRatio=2 (Old:Young ratio, default 2 means 1/3 Young, 2/3 Old)
// -XX:SurvivorRatio=8 (Eden:Survivor ratio within Young Gen)
Garbage Collection Algorithms
Java provides several GC implementations, selectable via JVM flags:
- Serial GC (
-XX:+UseSerialGC): single-threaded. Best for small apps or single-core environments. - Parallel GC (
-XX:+UseParallelGC): multi-threaded Minor and Major GC. Default in Java 8. Throughput-focused. - G1 GC (
-XX:+UseG1GC): default since Java 9. Divides heap into equal-sized regions. Balances throughput and latency. Targets predictable pause times. - ZGC (
-XX:+UseZGC): low-latency GC with sub-millisecond pauses. Available since Java 11, production-ready in Java 15+. - Shenandoah (
-XX:+UseShenandoahGC): concurrent GC, similar goals to ZGC.
Java
// Explicitly requesting GC (not guaranteed to run immediately)
System.gc();
// Checking heap usage at runtime
Runtime rt = Runtime.getRuntime();
long usedMB = (rt.totalMemory() - rt.freeMemory()) / (1024 * 1024);
long maxMB = rt.maxMemory() / (1024 * 1024);
System.out.printf("Used: %dMB / Max: %dMB%n", usedMB, maxMB);
Reference Types and GC Eligibility
Java provides four reference strengths that affect when objects are garbage collected:
- Strong reference: normal reference (
Object o = new Object()). Never collected while reachable. - SoftReference: collected only when JVM needs memory. Useful for memory-sensitive caches.
- WeakReference: collected at next GC cycle regardless. Used in
WeakHashMap. - PhantomReference: enqueued after object is finalized. Used for cleanup actions.
Java
import java.lang.ref.*;
// Soft reference — kept alive while memory is available
SoftReference<byte[]> softRef = new SoftReference<>(new byte[1024 * 1024]);
byte[] data = softRef.get(); // null if GC collected it
// Weak reference — collected eagerly
WeakReference<MyObject> weakRef = new WeakReference<>(new MyObject());
MyObject obj = weakRef.get(); // may return null after GC
// WeakHashMap — entries removed when key is no longer strongly reachable
Map<Object, String> cache = new WeakHashMap<>();
Common Memory Leak Patterns
Java can still have memory leaks: situations where objects are reachable but no longer needed:
- Storing objects in static collections without ever removing them
- Listeners/callbacks registered but never deregistered
- Unclosed resources (streams, connections): use try-with-resources
- ThreadLocal variables not cleaned up in thread pools
Java
// BAD: potential memory leak — stream not closed on exception
BufferedReader reader = new BufferedReader(new FileReader("file.txt"));
String line = reader.readLine();
reader.close(); // not reached if readLine() throws
// GOOD: try-with-resources — always closes the stream
try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
String line = reader.readLine();
}
// BAD: ThreadLocal not removed — leaks in thread pools
private static ThreadLocal<Connection> conn = new ThreadLocal<>();
// GOOD: always remove when done
try {
conn.set(getConnection());
// use connection...
} finally {
conn.remove();
}