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