The most surprising thing about modern garbage collectors like ZGC and Shenandoah is that they achieve near-zero pause times not by being "faster" at collecting garbage, but by doing most of their work concurrently with the application threads.

Let’s see what that looks like in practice. Imagine an application running, allocating objects, and keeping references to them. The garbage collector’s job is to reclaim memory occupied by objects no longer reachable.

Here’s a simplified view of a ZGC-enabled application startup and some activity. We’ll assume a heap size of 16GB and target pause times under 10ms.

java \
  -XX:+UnlockExperimentalVMOptions \
  -XX:+UseZGC \
  -Xmx16g \
  -Xms16g \
  -XX:MaxNewSize=8g \
  -jar my-application.jar

As my-application.jar starts and begins allocating, ZGC is already doing its thing in the background. It doesn’t stop your application to scan the heap. Instead, it uses techniques like concurrent marking, concurrent region marking, and concurrent reference processing. When a GC cycle is needed, it might briefly pause the application for a very short "synchronization" phase, but the heavy lifting of identifying garbage happens while your application is merrily running.

Consider a scenario where your application has a tight loop creating many short-lived objects.

public class ObjectFactory {
    public static void main(String[] args) {
        // Simulate a hot loop allocating objects
        for (long i = 0; i < Long.MAX_VALUE; i++) {
            if (i % 1_000_000 == 0) {
                System.out.println("Allocated " + i + " objects...");
            }
            new byte[1024]; // Allocate 1KB object
            if (i > 10_000_000_000L) break; // Prevent infinite loop
        }
    }
}

With ZGC, your application threads keep allocating. ZGC, running on separate threads, concurrently traverses the object graph. It uses a technique called "load barriers" to track object modifications and "colored pointers" to mark objects’ states (e.g., live, to-be-collected) without needing a full stop-the-world pause. When it’s time to reclaim memory, ZGC might perform a very brief pause to finalize its work, but the bulk of the identification and deallocation happens alongside your application.

The core problem these collectors solve is the "stop-the-world" pause. Traditional GCs (like ParallelGC or even G1 in certain situations) would halt all application threads to perform heap scanning and compaction. For latency-sensitive applications (e.g., high-frequency trading, real-time gaming, responsive UIs), these pauses, even if infrequent, can be devastating. ZGC and Shenandoah aim to make these pauses so short and infrequent that they are effectively invisible to the end-user or the application’s responsiveness requirements.

Internally, ZGC uses a generational approach but with concurrent collection across generations. It divides the heap into regions. During marking, it identifies reachable objects. Then, during a brief pause, it performs a "relocation" phase, moving live objects to new regions, effectively compacting the heap concurrently. Shenandoah takes a similar approach, focusing on concurrent marking and compacting live objects into a contiguous space. Both rely heavily on advanced JVM features like load barriers and specialized memory management techniques.

The critical levers you control are primarily the heap size (-Xmx, -Xms) and the GC choice itself (-XX:+UseZGC, -XX:+UseShenandoahGC). For ZGC, you can also tune -XX:ConcGCThreads to control the number of concurrent GC threads, but the JVM often does a good job of autotuning this. The key is to provide enough heap memory for your application to run comfortably, allowing the GC to perform its concurrent work without being constantly triggered by a full heap.

The one thing most people don’t realize is how much metadata these collectors manage concurrently. It’s not just about marking objects. They are constantly updating internal "coloring" schemes on pointers and managing region states. This is why the JVM needs to be built with specific support for these collectors; it’s a deep integration with the JVM’s memory manager and object model, not just a tunable parameter.

The next challenge you’ll likely encounter is understanding how application-specific object allocation patterns and reference lifecycles interact with the concurrent GC’s phases.

Want structured learning?

Take the full Jvm course →