The Garbage Collector Who Works When He Feels Like It

HarshavardhananHarshavardhanan
9 min read

In Chennai, the government-assigned garbage collectors show up every day — at least, they’re supposed to. Some days they don’t. And even when they do, there’s no fixed time.

One morning it's 6 AM, the next it's 11.

You can call the complaint number and lodge a request for punctual pickups, but it usually ends up as background noise. A gentle nudge, politely ignored.

Yet, as frustrating as they are, life without them would be a complete breakdown. Chennai would turn into a Cyberpunk 2077-style dystopia — minus the cyber and definitely minus the punk.

Just a rotting, chaotic Night City full of garbage.

While grumbling about this one day, it struck me: I know another silent worker who behaves the exact same way. Unreliable, opaque, sometimes sluggish — but absolutely vital.

🎬 Open theatre screen: Java Garbage Collector.


1 > What Is Garbage Collection, Really?

Most Java developers know what garbage collection does — it clears unused memory so we don’t have to. But very few think about how it decides what’s garbage and when to actually clean it up.

At its core, Java Garbage Collection (GC) is an automatic memory management service provided by the JVM. It tracks objects that are no longer reachable by any part of your code and reclaims that memory for future allocations.

Sounds efficient. But here’s the twist: you don’t control when it runs. You don’t choose how it runs. And unless you dig deep, you may not even realize it’s the reason your system is freezing during a traffic spike or latency-sensitive request.

So if GC is a janitor, it’s not a quiet, invisible one. It’s more like a moody worker who might suddenly decide to mop the floors during peak business hours — blocking the entrance while you’re trying to onboard a thousand new customers.

To understand why that happens (and how to prevent it), we need to look beneath the surface — at how the JVM actually organizes memory and what triggers GC events in the first place.


2 > How Java GC Works Under the Hood

Java’s memory isn’t one giant bucket. The JVM organizes the heap into generations based on object lifespan. Why? Because most objects in a typical Java application die young — so it’s wasteful to scan the entire heap every time.

JVM Heap Layout

  • Young Generation

    • Eden Space: This is where all new objects are born.

    • Survivor Spaces (S0, S1): If an object survives a Minor GC, it gets moved here. After a few rounds, it may be promoted to Old Gen.

  • Old Generation (Tenured)

    • For objects that have been around long enough to be considered “mature.” This is where long-lived references (like caches, session data) end up.
  • Metaspace (since Java 8)

    • Not technically part of the heap. Stores class metadata. Still capable of triggering OutOfMemoryErrors if class loading isn't managed well.

Minor vs Major GC

  • Minor GC

    • Focuses on cleaning the Young Gen. Fast and frequent. Only objects with active references survive and move to the next stage.
  • Major GC / Full GC

    • Sweeps the Old Gen. Can cause significant stop-the-world pauses. Sometimes includes Young Gen too, depending on the GC algorithm.

How Objects Die

The JVM uses reachability analysis starting from GC Roots (like static fields, thread stacks, JNI refs). If an object can't be traced from a root, it's considered garbage.

But here’s the catch — even unreachable memory isn’t freed immediately. The GC runs based on heuristics, not your schedule. Which means pauses can hit you when you least expect.


3 > GC Algorithms in Java

Not all garbage collectors are built equal. Over the years, Java has evolved multiple GC algorithms — each with different strategies for latency, throughput, and pause times.

Let’s walk through the key ones.


1. Serial GC

Best for: Small applications or single-threaded environments (e.g., embedded systems, test suites).

  • Uses a single thread for GC.

  • Performs full stop-the-world collections.

  • Simple but blocks everything during collection.

  • Enabled with: -XX:+UseSerialGC

💡 Predictable but outdated for most modern workloads.


2. Parallel GC (Throughput Collector)

Best for: CPU-rich batch systems focused on raw throughput.

  • Multi-threaded Minor and Major GCs.

  • Focuses on minimizing total GC time, not pause length.

  • Doesn’t care when your app freezes — only that it spends less overall time in GC.

  • Enabled with: -XX:+UseParallelGC

💡 Throughput wins, latency loses.


3. CMS (Concurrent Mark-Sweep)

Best for: Apps where long GC pauses are unacceptable (e.g., UI, API services).

  • Tries to do most of its GC work concurrently with application threads.

  • Reduced pause times but prone to fragmentation.

  • Deprecated in Java 9, removed in Java 14.

  • Enabled with: -XX:+UseConcMarkSweepGC

💡 First attempt at low-pause GC, but couldn’t scale well.


4. G1 GC (Garbage First)

Best for: General-purpose, low-pause workloads (modern default from Java 9+).

  • Heap is split into regions instead of fixed generations.

  • Prioritizes collecting regions with the most garbage first.

  • Concurrent marking + predictable pause goals via -XX:MaxGCPauseMillis.

  • Enabled with: -XX:+UseG1GC (default in Java 9+)

💡 Smart trade-off between throughput and latency. Go-to for most production systems.


5. ZGC & Shenandoah

Best for: Large heaps, ultra-low pause goals (<10ms).

  • ZGC (by Oracle):

    • Pause times < 10ms, even with 100+ GB heaps.

    • Requires recent Java (11+), experimental flags.

    • -XX:+UseZGC

  • Shenandoah (by RedHat):

    • Competes with ZGC for low-latency.

    • Works better in medium heaps (~8–16 GB).

    • -XX:+UseShenandoahGC

💡 Pause time reduction is their superpower. Still evolving.


4 > Tuning Garbage Collection

Tuning GC is like walking in a minefield — too many tweaks, you might lose a leg and sometimes if the GC feels naughty, the application itself. But used wisely, GC tuning can reduce pause times, improve throughput, and stabilize memory pressure.

When You Should Tune

  • Your application has unpredictable latency spikes

  • You're seeing Full GCs during peak traffic

  • GC logs show frequent promotions or Old Gen churn

  • You're scaling up heap size > 8–16 GB

If you’re not hitting performance issues, tuning might do more harm than good. GC has gotten smarter — especially with G1, ZGC, and Shenandoah.


Useful JVM GC Flags (G1-focused)

FlagWhat It Does
-Xms, -XmxSet initial and max heap size
-XX:MaxGCPauseMillis=200Sets soft goal for pause time (in ms)
-XX:+PrintGCDetails, -Xlog:gc*Enables GC logging (format varies by Java version)
-XX:+UseStringDeduplicationReduces duplicate string memory (G1 only)
-XX:NewRatio=3Old:Young heap ratio (for non-G1 collectors)
-XX:InitiatingHeapOccupancyPercent=45Triggers concurrent GC earlier (lowers STW risk)

Anti-Patterns

  • Blindly increasing heap size → Longer GC cycles.

  • Overusing System.gc() → Forces Full GC and blocks threads (if the GC chooses to honor your call).

  • Over-customizing all GC flags → Might fight against default heuristics.

  • Choosing low-pause GCs (ZGC, Shenandoah) on small heaps → Wastes CPU.


5 > Real-World GC Footguns

In theory, Java GC is your invisible assistant. In production, it’s often the cause of mysterious lags, memory spikes, and 2 AM war room calls. Here are the GC landmines no one warns you about — until they blow up.


1. Memory Leaks in a Garbage-Collected World

Just because Java has GC doesn’t mean you're safe from leaks. If your code holds on to references unnecessarily (e.g., long-lived maps, static caches, thread locals), GC won’t collect anything.

➡️ Classic trap: Map<SessionId, Data> that never gets cleaned up.
➡️ GC sees a reference, assumes it’s still needed. No questions asked.


2. Long GC Pauses = User Rage

Major GCs (especially in Old Gen) can cause stop-the-world (STW) pauses — where your app threads freeze until GC finishes.

  • Users experience frozen UIs or timeout errors

  • GC logs may show "Full GC (System.gc())" → red flag

  • High pause time + high allocation rate = meltdown


3. Allocation Rate vs GC Throughput

If your app creates objects faster than GC can reclaim memory, it’s game over. You’ll see:

  • GC running more frequently

  • Survivor spaces overflowing

  • Full GCs getting triggered under pressure

➡️ The app doesn’t crash — it just dies slowly under the weight of its own object churn.


4. GC Choosing the Wrong Time to Run

GC has heuristics. They don't always align with your traffic.

  • Peak traffic? GC thinks now’s a great time to clean.

  • Low traffic? GC might idle and let memory bloat.

This is why low-pause collectors like G1, ZGC, and Shenandoah matter — they’re built to mitigate mistimed sweeps.


6 > Debugging GC in Production

When latency spikes, memory usage climbs, or users start complaining, GC is a usual suspect. But most logs don’t scream “GC problem” — they whisper it. You need to know where to listen.


1. GC Logs: Your First Signal

Enable detailed GC logging to monitor behavior:

🔹 Java 8 and below:

-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/path/to/gc.log

🔹 Java 9+ (Unified Logging):

-Xlog:gc*:file=gc.log:time,level,tags

Look for:

  • GC frequency: Too frequent → high allocation or small heap

  • Pause time: Anything > 200ms (or lower in latency-critical apps)

  • Promotion failures: Means Old Gen is full or fragmented


2. GC Visualization Tools

  • JVisualVM: Lightweight, comes with JDK. Good for heap snapshots and live GC observation.

  • Java Mission Control (JMC): Oracle’s profiler for deep GC + thread behavior analysis.

  • GCViewer: Open-source tool to parse GC logs visually.

  • GCEasy.io: Paste your logs, get a visual report — great for quick triage.


What to Watch

SymptomGC Clue
Sudden spike in latencyFull GC or STW GC
High Old Gen occupancyPoor promotion policy or memory leak
Constant Minor GCsEden space too small or high allocation rate
Full GC with low memory reclaimedFragmentation or retained objects
High GC CPU usageOver-tuning, low pause goals, or wrong GC type

7 > Wrap-Up: Know Thy Collector

Garbage Collection in Java isn’t just a background process — it’s a silent system-level actor with direct influence over latency, memory footprint, and overall app resilience.

You don’t need to memorize every flag or dissect every algorithm. But you do need to understand what collector you're using, how it behaves under pressure, and what signals to watch in production.

So if you’re serious about writing high-performance Java systems, along with coding proper, also learn how your runtime cleans up after you.

That’s what makes you grow from a Java Developer to a Java Engineer.

0
Subscribe to my newsletter

Read articles from Harshavardhanan directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Harshavardhanan
Harshavardhanan