GC in Java
Demystifying Java Garbage Collection & Memory Leaks
Everything you need to know to ace a Senior Developer interview (and keep your production servers from crashing).
If you are a junior developer, you can usually get away with just writing new Object() and letting the Java Virtual Machine (JVM) handle the rest. But as you transition into a senior backend or full-stack role, understanding how that object affects the server's CPU and memory is no longer optional.
In technical interviews, explaining Garbage Collection (GC) is the ultimate separator. It proves you understand howsystems work under the hood, not just how to write syntax.
Here is a complete breakdown of how Java manages memory, why it pauses your application, and how to hunt down the dreaded Java memory leak.
1. The Core Philosophy: The Weak Generational Hypothesis
Before diving into how the Garbage Collector works, you have to understand why it is designed the way it is. Java’s entire memory architecture is built on a single, proven statistical fact called the Weak Generational Hypothesis:
- Most objects die young. (Think of temporary variables inside a
forloop or data tied to a single HTTP request). - Objects that survive for a while tend to live forever. (Think of database connection pools, cached configurations, or Spring Beans).
Because of this, it would be a massive waste of CPU to scan the entire memory heap every time a cleanup is needed. Instead, Java splits the Heap into Generations (levels) so it can aggressively clean the “young” area without touching the massive “old” area.
2. The Memory Architecture
The Java Heap is divided into two main zones:
- Young Generation: This is where all new objects are born. It is divided into three sub-spaces:
- Eden Space: Every time you use the
newkeyword, the object is placed here. - Survivor 0 (S0) & Survivor 1 (S1): Two smaller buffer zones used for objects that survive their first cleanup.
- Old Generation (Tenured): This is the retirement home for objects that have lived a long time. It is much larger than the Young Generation.
(Note: There is also Metaspace, formerly PermGen. Since Java 8, this is stored in native OS memory outside the heap and holds class definitions and static variables).
3. The Lifecycle of an Object
So, what exactly happens when you create an object? Here is the step-by-step journey:
- Birth: You create a new object. It goes straight into Eden.
- First Cleanup (Minor GC): Eventually, Eden fills up. This triggers a Minor GC. The Garbage Collector sweeps through Eden, deleting all dead (unreachable) objects. The surviving objects are moved into S0, and they are assigned an “age” of 1. Eden is now completely empty.
- The Ping-Pong Effect: The next time Eden fills up, another Minor GC happens. This time, it sweeps both Eden and S0. All surviving objects are moved to S1, and their ages are incremented to 2. Now Eden and S0 are empty.
- Promotion: The GC constantly ping-pongs survivors between S0 and S1, incrementing their age each time. Once an object reaches the Max Tenuring Threshold (usually age 15), the GC assumes this object is going to stick around permanently and promotes it to the Old Generation.
4. The “Stop-The-World” Problem
This is the most critical concept for a senior developer to grasp.
Get Aditya Narayan Singh’s stories in your inbox
Join Medium for free to get updates from this writer.
When the GC is moving memory addresses around (like promoting an object to the Old Generation), it cannot let your application code keep running. If it did, your program might try to read an object that is currently being moved. To prevent this, the GC triggers a Stop-The-World (STW) event. It literally pauses all your application threads.
- Minor GC Pauses: These happen frequently in the Young Generation but are so incredibly fast (fractions of a millisecond) that you never notice them.
- Major / Full GC Pauses: When the massive Old Generation finally fills up, Java has to do a Full GC. This is a heavy operation. If your Old Generation is filled with gigabytes of data, a Full GC can freeze your application for several seconds.
If you are building a real-time trading app or a high-traffic API, a 3-second Full GC pause means dropping thousands of user requests. Most memory tuning in Java is about preventing objects from prematurely entering the Old Generation to avoid these massive pauses.
5. Choosing Your GC Algorithm
Modern Java doesn’t just have one Garbage Collector. You can choose your algorithm via JVM flags depending on your application’s needs:
- Parallel GC: The old default (Java 8). Good for raw throughput and batch processing, but has terrible Stop-The-World pauses.
- G1 GC (Garbage-First): The default since Java 9. Instead of massive chunks of memory, it slices the heap into hundreds of tiny regions. It cleans the regions with the most garbage first, keeping pause times very predictable.
- ZGC (Z Garbage Collector): The modern beast (production-ready in Java 15+). It handles terabytes of heap memory with pause times strictly under 1 millisecond. For modern microservices, this is the gold standard.
6. The Ultimate Gotcha: Memory Leaks in Java
If the Garbage Collector cleans up dead objects automatically, how do memory leaks still happen?
In languages like C or C++, a memory leak happens when you forget to free memory, and it becomes “lost” in the system. In Java, the GC cannot lose memory. A Java memory leak is actually Unintentional Object Retention.
The GC only deletes objects that are unreachable (meaning no active part of your code points to them anymore). A leak happens when you accidentally keep a reference to an object that you no longer need, tricking the GC into thinking it is still active.
The Top 4 Causes of Java Memory Leaks
- Static Collections (The #1 Culprit): The lifecycle of a
staticvariable is tied to the Class itself, meaning it lives for the entire duration of the application. If you have astatic Listorstatic HashMapthat you constantly add items to but never remove them from, the GC will never touch them. The collection will grow until it consumes the entire heap. - Unclosed Resources: When you open a database connection, a network socket, or a file stream, the JVM allocates memory to handle that buffer. If you do not explicitly close it (preferably using a
try-with-resourcesblock), that memory remains locked up. - Improper
equals()andhashCode()in Map Keys: If you use a custom object as a Key in aHashMap, but you forget to overrideequals()andhashCode(), the Map cannot reliably tell if two keys are the same. If you try to update an existing entry, the Map will think it is a brand new key and create a duplicate. Over time, the Map fills up with unreachable duplicate garbage. - Caching Without Eviction: Developers often try to build their own cache using a standard
HashMap. The problem is that a standard map never forgets. A proper cache requires an "eviction policy" (e.g., deleting the oldest items). If you need an in-memory cache, always use a library like Caffeine or Java's built-inWeakHashMap.
7. How to Hunt Down a Memory Leak
You cannot find a memory leak just by reading code; you have to look at the JVM memory footprint while the application is running or right after it crashes.
Step 1: The Safety Net (Heap Dumps) Run your production Java applications with this JVM flag: -XX:+HeapDumpOnOutOfMemoryError When your app eventually crashes from a memory leak, this flag forces the JVM to take a "snapshot" (a Heap Dump file) of the exact state of your memory the millisecond before it died.
Step 2: Memory Profilers Once you have a Heap Dump, you need a profiler to analyze the data.
- IntelliJ IDEA Ultimate Profiler: Great for local debugging. It allows you to capture memory snapshots and highlights objects dominating the heap, showing you the exact line of code holding the reference.
- Eclipse Memory Analyzer Tool (MAT): The industry-standard tool for reading Heap Dumps. It automatically generates a “Leak Suspects” report and analyzes the “GC Roots” to tell you exactly which object is hoarding your memory.
Step 3: The Diagnosis When looking at the profiler, do not look for the largest raw objects (like byte arrays). Look for the Retained Size of your classes. If you see an array that holds 500,000 instances of a UserSession object, you simply follow the reference chain backward until you find the exact static field or unclosed thread holding onto them.
Conclusion
Understanding Garbage Collection and memory management is what separates a good Java developer from a great one. By understanding how the JVM handles memory under the hood, you can write cleaner, faster, and much more resilient applications.
Next time your application crashes with an OutOfMemoryError, you will know exactly where to look.
🔥 Pro Tip: The String Concatenation Trap
Interviewers love asking about String vs. StringBuilder in the context of memory. Because Java Strings are immutable, their values can never change once created.
If you write a for loop that adds a character to a String 10,000 times (text = text + "a";), Java doesn't just expand the original String. It creates 10,000 brand-new, separate String objects in the Heap, abandoning the previous one each time. This creates a massive pile of dead objects that immediately triggers a Minor GC pause.
The Fix: Always use StringBuilder (or StringBuffer if you need thread safety) when modifying text in a loop. StringBuilderholds a single, mutable array of characters under the hood. It updates that single array in place, meaning it only creates one object in the Heap, saving massive amounts of memory and CPU cycles.






