Skip to Content
Course content

102: Garbage Collection Algorithms

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Wait, why are there so many different GC algorithms anyway?

If you've looked at the JVM flags, you've probably seen G1, ZGC, Parallel, and Shenandoah. It feels like overkill, right? But here's the thing: you can't have everything. In the world of memory management, we're always trading off between throughput (how much total work the CPU does) and latency (how long the application freezes).

I've seen teams use the Parallel GC for massive batch-processing jobs—think of a system that calculates payroll for 50,000 employees once a month. In that case, you don't care if the app freezes for two seconds; you just want the job done as fast as possible. But if you're building a high-frequency trading platform or a real-time multiplayer game, a two-second "Stop-the-World" pause is a catastrophe. That's where ZGC or Shenandoah come in. They do most of the heavy lifting concurrently while your code is still running, keeping pauses under a millisecond, even if it means the overall throughput takes a slight hit.

What is actually happening during a "Stop-the-World" event?

When you hear "Stop-the-World" (STW), it sounds like a crash, but it's actually a controlled pause. The JVM brings every single application thread to a "safe point." Why? Because if the GC is moving an object from one part of memory to another to defragment the heap, it can't have your application thread trying to read that object at the same time. That would be a recipe for a NullPointerException or, worse, memory corruption.

Think of it like repainting the lines on a highway. You can't just paint while cars are zooming over the lines at 70mph. You have to stop traffic, paint the line, and then let the cars go. Modern collectors like G1 (Garbage First) try to minimize this by breaking the heap into small regions and only cleaning the ones that are "most full" first, rather than trying to clean the entire heap in one giant, agonizing pause.

How does the "Generational" part actually work in my code?

Most GC algorithms rely on the Generational Hypothesis: the idea that most objects die young. If you create a StringBuilder just to format a single log message, that object is useless the millisecond the log is written. It shouldn't be treated the same as a UserSession object that needs to live for 30 minutes.

The heap is usually split into the Young Generation (Eden space and two Survivor spaces) and the Old Generation. Here is a mental model of how an object moves:

  • Eden: Where almost every object starts. It's cleaned frequently and very quickly.
  • Survivor Spaces: If an object survives an Eden cleanup, it gets kicked over to a survivor space. It's basically the JVM saying, "Okay, you're not dead yet, let's keep an eye on you."
  • Old Gen: After surviving enough cleanup cycles (the "tenuring threshold"), the object is promoted to the Old Generation.

// This object is born in Eden and will likely die there (Fast GC)
void logRequest(Request req) {
    String message = "Request received from " + req.getIp(); // Short-lived
    System.out.println(message);
}

// This object will likely migrate to the Old Gen (Slow GC)
public class AppCache {
    private final Map<String, User> userCache = new HashMap<>(); 
    
    public void addToCache(User user) {
        userCache.put(user.getId(), user); // Long-lived
    }
}

The real performance killer is when your "short-lived" objects accidentally get promoted to the Old Gen (called premature promotion). When the Old Gen fills up, you get a "Full GC," and that's when your application feels like it's hitting a brick wall.




📋 Practical Task

Experiment: Triggering and Analyzing a Full GC Pause

Your goal is to intentionally cause a "Full GC" event and observe the difference between a Minor GC and a Major GC using JVM telemetry.

  1. Create a program that maintains a List<byte[]>.
  2. Write a loop that adds 1MB byte arrays to the list every few milliseconds.
  3. Run the program with the following JVM flags to enable GC logging: -Xms128m -Xmx128m -Xlog:gc*
  4. Observe the console output. You should see several "GC (Allocation Failure)" events (these are the Young Gen/Minor GCs).
  5. Continue running the program until you see a "Full GC" or "Pause Full" event.
  6. The Challenge: Modify your code to introduce a "leak" (keep references to the arrays) versus a "temporary" load (clear the list every 100 iterations). Compare how the frequency of Full GC events changes in your logs.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.