Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
102: Garbage Collection Algorithms
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.
- Create a program that maintains a
List<byte[]>. - Write a loop that adds 1MB byte arrays to the list every few milliseconds.
- Run the program with the following JVM flags to enable GC logging:
-Xms128m -Xmx128m -Xlog:gc* - Observe the console output. You should see several "GC (Allocation Failure)" events (these are the Young Gen/Minor GCs).
- Continue running the program until you see a "Full GC" or "Pause Full" event.
- 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.
There are no comments for now.