Modern Garbage Collection #Java
Garbage collectors actually mark live objects, not dead objects.
This is a common point of confusion. In Java, collectors use an approach called “Mark and Sweep” (or Mark and Relocate).
Here is exactly why collectors mark the living instead of the dead:
1. The Logic: Tracking Life is Easier Than Tracking Death
A JVM heap can contain millions of objects.
- To find dead objects directly, the GC would have to scan every single object in the entire memory space to check if anything is still using it. This is incredibly slow. [1]
- To find live objects, the GC only needs to start from a few guaranteed active points called GC Roots (like local variables currently in use by a thread, or static variables). It follows the references from those roots like a chain.
Anything the GC can reach from a root is marked as live. Anything it cannot reach is automatically considered garbage.
2. How G1 GC and ZGC Handle This
Both G1 and ZGC use Concurrent Marking to find live objects, but they handle the unmarked (dead) space differently:
In G1 GC:
- Concurrent Mark: G1 walks the object graphs while your application runs and marks all live objects.
- Determine Garbage: If G1 finds an entire region where zero live objects were marked, it knows that 100% of the objects in that region are dead.
- Evacuation Pause: G1 pauses the application, targets the regions with the lowest percentage of live objects (the “Garbage-First” rule), copies those few live objects to a completely new region, and instantly wipes the old region clean.
In ZGC:
- Concurrent Mark: ZGC walks the object graph while your application runs and changes the “color” bit on the pointer of every live object to indicate it is active.
- Concurrent Relocate: Instead of waiting for a pause, ZGC immediately begins copying those marked live objects to new, clean memory pages while your application keeps running.
- Reclaim: Once all live objects are safely moved out of a page, that entire page is reclaimed and made available for new allocations.
1. When Do G1 and ZGC Pause?
While both collectors mark live objects and relocate them, they handle their Stop-The-World (STW) pauses very differently across the phases.
G1 GC Pauses
Get Prateek Das’s stories in your inbox
Join Medium for free to get updates from this writer.
G1 GC must pause your application twice during marking, and once majorly during relocation:
- Initial Mark (Pause 1): A very brief pause to mark objects directly connected to GC Roots (like active threads).
- [Concurrent Phase: G1 walks the rest of the object graph while your app runs].
- Remark (Pause 2): A brief pause to finalize the marking and catch any object references that changed while the app was running.
- Evacuation / Relocation (Pause 3 — The Longest): G1 stops the application entirely to physically copy the live objects to a new memory region. This pause scales based on how much live data needs to be moved.
ZGC Pauses
ZGC still has pauses, but they are fixed, ultra-brief checkpoints (usually under 1 millisecond) regardless of how large your heap is:
- Pause 1 (Initial Mark): A tiny pause to mark the initial GC Roots.
- [Concurrent Phase: ZGC marks the rest of the live objects while your app runs].
- Pause 2 (Remark): A tiny pause to synchronize and finish the marking phase.
- Pause 3 (Start Relocation): A tiny pause to prepare the relocation sets.
- [Concurrent Phase: ZGC physically moves the objects while your app runs]. This is where ZGC saves all your pause time. It does not stop the app to move data.
2. What is a Load Barrier?
A Load Barrier is the secret weapon that allows ZGC to move live objects while your application threads are actively running, without causing data corruption.
It is not a synchronization lock or a traditional blocking barrier. Instead, it is a tiny, highly-optimized piece of interceptor code injected by the Just-In-Time (JIT) compiler whenever your application code tries to read (load) an object reference from the heap.
How it works conceptually:
Think of a load barrier like a smart postal redirect system.
If your Java code executes a statement like:
Order myOrder = customer.getOrder(); // This is a "Load" operationThe Load Barrier instantly intercepts this read and checks the Colored Pointer (the metadata bits hidden inside the myOrder reference pointer).
- Scenario A (Normal State): The pointer bits show the object is stable. The load barrier does nothing. Your app gets the object instantly. This happens 99% of the time and takes less than a nanosecond.
- Scenario B (Object is Being Moved): ZGC is currently relocating that specific
Orderobject in the background. The load barrier detects this "color" flag on the pointer and takes immediate action:
- It checks ZGC’s central tracking table to see if the object has already been copied to its new address.
- If it hasn’t been copied yet, the application thread manually copies the object itself right then and there to the new memory page.
- It updates (re-colors) the
customer.getOrder()pointer so it points directly to the new, correct address. - It returns the updated, safe object reference to your code.
Why this matters:
Because the load barrier fixes “bad” pointers on the fly the exact moment your code tries to touch them, ZGC never has to pause the application to update references. The application threads and the garbage collector actively work together to clean up memory seamlessly.







