Garbage Collection
GC Fundamentals
Section titled “GC Fundamentals”What Is Garbage?
Section titled “What Is Garbage?”An object is garbage when it is no longer reachable from any live thread through any chain of References. The JVM determines this through reachability analysis, starting from a set of root Objects and tracing all objects transitively reachable from those roots.
GC Roots
Section titled “GC Roots”GC roots are the starting points for reachability analysis. An object is reachable if there is a Path from at least one GC root. The primary categories of GC roots:
- Stack-local variables and method parameters. References on the call stack of each thread.
- Static fields. References stored in class metadata (loaded by any class loader).
- JNI references. References held by native code through the Java Native Interface.
- JNI global references. Explicitly created global references in native code.
- Thread objects. Every live thread is a GC root (its stack contains references).
finalfields in objects reachable from roots (finalizers also contribute).
public class GCDemo { private static Object staticRoot; // GC root (static field) private Object instanceField; // reachable if "this' is reachable
public void method() { Object localVar = new Object(); // GC root (stack-local) while in scope // After method returns, localVar is no longer a root — eligible for GC // UNLESS a reference was stored somewhere reachable (e.g., in instanceField) }}Reachability Phases
Section titled “Reachability Phases”The JVM does not immediately collect objects when they become unreachable. The GC process involves Three conceptual phases:
- Mark. Identify all reachable objects starting from roots.
- Sweep. Identify all unreachable objects (garbage).
- Compact (optional). Move surviving objects to eliminate fragmentation.
JVM Memory Regions
Section titled “JVM Memory Regions”Heap Structure
Section titled “Heap Structure”The heap is divided into generations based on the generational hypothesis: most objects are Short-lived (“die young”). Separating the heap into generations allows the GC to focus collection Effort on the young generation, where most garbage accumulates.
Heap├── Young Generation│ ├── Eden Space — where new objects are allocated│ ├── Survivor Space 0 — objects that survived one GC│ └── Survivor Space 1 — objects that survived one GC└── Old Generation (Tenured) └── Long-lived objects — objects that survived multiple young GCsYoung Generation
Section titled “Young Generation”Eden: All new objects (except very large ones) are allocated in Eden. When Eden fills up, a minor GC (young GC) is triggered.
Survivor spaces: Two equally-sized spaces (S0 and S1) that act as a holding area. After a minor GC, surviving objects from Eden and one survivor space are copied to the other survivor space. Objects accumulate an “age” counter; each time they survive a GC, the age increments.
Promotion: When an object’s age exceeds a threshold (
-XX:MaxTenuringThresholdDefault 15 for G1, 6 for Parallel), it is promoted to the old generation.
## Young generation sizing-Xmn512m # Fixed young generation size: 512 MB-XX:NewRatio=2 # Old:Young = 2:1, so young = 1/3 of heap-XX:SurvivorRatio=8 # Eden:Survivor = 8:1, so each survivor = 1/10 of youngOld Generation
Section titled “Old Generation”Objects that survive multiple young GCs are promoted to the old generation. A major GC (or full GC) collects the entire heap, including the old generation. Major GCs are significantly more Expensive than minor GCs because they must scan the entire heap.
Metaspace
Section titled “Metaspace”Stores class metadata (method bytecodes, constant pool, field/method definitions), static fields, And interned strings (moved from PermGen in JDK 8). Metaspace uses native memory and grows until MaxMetaspaceSize is reached.
-XX:MaxMetaspaceSize=256m # Limit metaspace (prevents native memory exhaustion)-XX:CompressedClassSpaceSize=1g # Compressed class pointer spaceCode Cache
Section titled “Code Cache”Stores JIT-compiled native code. When the code cache fills up, the JVM stops compiling (and may Deoptimize). Monitor with:
-XX:ReservedCodeCacheSize=256mGenerational Hypothesis
Section titled “Generational Hypothesis”The generational hypothesis states that:
- Most objects are short-lived. They become garbage soon after allocation.
- Few objects survive many GC cycles. The old generation is relatively stable.
This hypothesis has been validated empirically across virtually all Java workloads. It justifies the Generational heap layout: by collecting the young generation frequently and cheaply, the JVM avoids Costly full heap collections.
Exceptions to the hypothesis: long-lived caches, large object graphs loaded at startup, and Memory-mapped structures. These can cause premature promotion and increase old generation pressure.
GC Algorithms
Section titled “GC Algorithms”Serial GC
Section titled “Serial GC”The simplest collector. Uses a single thread for both minor and full GC. Stop-the-world (STW) for The entire duration of collection.
-XX:+UseSerialGCWhen to use: Small heap sizes (under ~1 GB), single-processor systems, or applications with low Pause-time requirements where simplicity is preferred. Rarely used in production servers.
Algorithm:
- Young GC: mark-sweep-compact on the young generation using a single thread.
- Full GC: mark-sweep-compact on the entire heap using a single thread.
Parallel GC (Throughput Collector)
Section titled “Parallel GC (Throughput Collector)”The default collector in JDK 8 and earlier for server-class machines. Uses multiple threads for Young GC and full GC but is still stop-the-world. Optimizes for throughput (maximize application Time between GC pauses).
-XX:+UseParallelGC # Enable Parallel GC (default in JDK 8)-XX:+UseParallelOldGC # Enable parallel compaction for full GC (default since JDK 7u4)-XX:ParallelGCThreads=8 # Number of GC threads-XX:MaxGCPauseMillis=200 # Target maximum pause time (hint, not guarantee)Algorithm:
- Young GC: parallel mark-copy (Eden + one survivor -> other survivor).
- Full GC: parallel mark-sweep-compact.
Trade-off: Higher throughput (more CPU time for application) but potentially long STW pauses, Especially during full GC on large heaps.
The default collector since JDK 9. Divides the heap into fixed-size regions ( 2048 regions, Each 1-32 MB). The collector can selectively collect regions, mixing young and old generation Collection in a single pass (“mixed collection”).
-XX:+UseG1GC # Enable G1 GC (default since JDK 9)-XX:MaxGCPauseMillis=200 # Target pause time-XX:G1HeapRegionSize=8m # Region size (1, 2, 4, 8, 16, 32 MB)-XX:G1MixedGCCountTarget=8 # Number of mixed collections after a marking cycle-XX:G1ReservePercent=10 # Reserve 10% of heap for unexpected promotions-XX:InitiatingHeapOccupancyPercent=45 # Start marking when heap is 45% fullG1 phases:
- Young-only collection. Collects Eden and survivor regions (same as minor GC in other collectors).
- Concurrent marking. While the application runs, G1 marks live objects across all regions.
- Mixed collection. Collects some young regions AND some old regions with the most garbage. Balances pause time against overall memory release.
G1 advantages:
- Predictable pause times (configurable target).
- No full GC in normal operation (mixed collections replace it).
- Handles large heaps (multi-GB to TB-scale) better than Parallel GC.
- Compaction is incremental (done region-by-region during mixed collections).
G1 disadvantages:
- Higher memory overhead (region tables, remembered sets).
- More complex tuning.
- Can fall back to full GC under memory pressure (sign of a tuning problem).
A scalable, low-latency collector designed for multi-TB heaps with pause times under 10 milliseconds (target: sub-millisecond). Became production-ready in JDK 15.
-XX:+UseZGC # Enable ZGC-XX:+ZGenerational # Enable generational ZGC (JDK 21+)-XX:SoftMaxHeapSize=16g # Soft limit — GC will try to stay under thisZGC design:
- Concurrent: Almost all GC work (marking, relocation, compaction) happens concurrently with the application. STW pauses are limited to root scanning and a few other operations, under 1 ms regardless of heap size.
- Colored pointers: ZGC uses 64-bit pointers with metadata bits (color bits) to track object state (marked, relocated, finalized) without requiring per-object headers.
- Load barriers: ZGC uses load barriers (triggered on every reference read) to detect and fix stale references. This is the main concurrency mechanism.
- Region-based: Like G1, the heap is divided into regions, but ZGC regions are more dynamic (2 MB, 32 MB, or N MB for large objects).
ZGC limitations:
- Requires 64-bit platform (uses high bits of 64-bit pointers).
- Load barriers add a small overhead to every reference read (~2-5% throughput reduction).
- May use more native memory than G1 for large heaps.
Shenandoah
Section titled “Shenandoah”An open-source low-pause collector (backed by Red Hat). Similar goals to ZGC but uses a different Implementation (Brooks pointers instead of colored pointers).
-XX:+UseShenandoahGCGC Pauses
Section titled “GC Pauses”Stop-the-World (STW)
Section titled “Stop-the-World (STW)”During an STW pause, all application threads are suspended. The JVM cannot execute application code While the GC is running. STW pauses directly impact application latency — if a GC pause takes 500 Ms, no requests can be served during that time.
Concurrent Phases
Section titled “Concurrent Phases”Modern collectors (G1, ZGC, Shenandoah) perform significant work concurrently with the application. Only specific phases require STW pauses:
| Collector | STW Phases | Concurrent Phases |
|---|---|---|
| Serial | All | None |
| Parallel | All | None |
| G1 | Root scanning, evacuation (partial) | Marking, remembered set processing |
| ZGC | Root scanning (sub-ms) | Marking, relocation, compaction |
| Shenandoah | Root scanning (sub-ms) | Marking, evacuation, compaction |
GC Tuning Flags
Section titled “GC Tuning Flags”Heap Sizing
Section titled “Heap Sizing”-Xms4g # Initial heap size (minimum)-Xmx4g # Maximum heap size## Set Xms == Xmx to avoid dynamic resizing overheadCollector Selection
Section titled “Collector Selection”-XX:+UseSerialGC # Serial-XX:+UseParallelGC # Parallel (throughput)-XX:+UseG1GC # G1 (balanced)-XX:+UseZGC # ZGC (low latency)-XX:+UseShenandoahGC # Shenandoah (low latency)G1-Specific Tuning
Section titled “G1-Specific Tuning”-XX:MaxGCPauseMillis=200 # Target max pause (default 200ms)-XX:G1HeapRegionSize=8m # Region size-XX:G1MixedGCCountTarget=8 # Mixed collections per cycle-XX:InitiatingHeapOccupancyPercent=45 # Marking trigger threshold-XX:G1NewSizePercent=5 # Min young generation (% of heap)-XX:G1MaxNewSizePercent=60 # Max young generation (% of heap)Parallel GC Tuning
Section titled “Parallel GC Tuning”-XX:ParallelGCThreads=8 # GC worker threads-XX:MaxGCPauseMillis=200 # Throughput hint (not guaranteed)-XX:GCTimeRatio=99 # GC time : application time = 1:99String Deduplication (G1 only)
Section titled “String Deduplication (G1 only)”-XX:+UseG1GC-XX:+StringDeduplication # Deduplicate string backing arraysGC Logging
Section titled “GC Logging”JDK 9+ uses the unified logging framework (-Xlog):
# Basic GC logging-Xlog:gc*
# GC logging with details-Xlog:gc*=info:file=gc.log:time,uptime,level,tags:filecount=5,filesize=100M
# Specific GC phases-Xlog:gc+heap=debug # Heap changes-Xlog:gc+phases=debug # GC phase details-Xlog:gc+pause=info # Pause times-Xlog:gc+cpu=info # GC CPU usageAnalyzing GC Logs
Section titled “Analyzing GC Logs”Use tools like GCViewer, GCEasy.io, or JDK Mission Control to analyze GC logs. Key Metrics to watch:
- Pause time distribution — P50, P95, P99, P99.9.
- Throughput — percentage of time the application is running vs. In GC.
- Allocation rate — MB/s allocated (high rate drives frequent GC).
- Promotion rate — MB/s promoted to old generation.
- Old generation growth — if it grows linearly, you have a memory leak or insufficient heap.
# Print GC summary at JVM exit-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log # JDK 8-Xlog:gc*:file=gc.log:time,uptime:filecount=5,filesize=100m # JDK 9+Memory Leaks
Section titled “Memory Leaks”Common Causes
Section titled “Common Causes”- Static collections. Accumulating entries without removal.
- Unclosed resources. Holding references through unclosed streams, connections, or
ThreadLocalinstances. - Listener registration. Registering listeners but never unregistering.
- Internal caches without eviction —
HashMapused as a cache without size limits or eviction policy. ThreadLocalin thread pools. Values not cleaned up when tasks complete.- String interning. Interning unbounded unique strings.
Detection with Heap Dumps
Section titled “Detection with Heap Dumps”# Trigger a heap dump on OOM-XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath=/tmp/heap.hprof
# Manual heap dumpjmap -dump:live,format=b,file=heap.hprof <pid>Analyze heap dumps with Eclipse MAT (Memory Analyzer Tool), VisualVM, or IntelliJ IDEA Profiler:
- Open the heap dump in MAT.
- Run the “Leak Suspects” report.
- Look at “Dominator Tree”. Objects that retain the most memory.
- Check “GC Roots” to find what is holding references to leaked objects.
Weak, Soft, and Phantom References
Section titled “Weak, Soft, and Phantom References”// WeakReference — GC'd as soon as no strong references remain// Use for caches where entries can be reclaimed when memory is neededWeakReference<byte[]> weak = new WeakReference<>(largeData);byte[] data = weak.get(); // null if GC'd
// SoftReference — GC'd only when the JVM needs memory// Use for memory-sensitive cachesSoftReference<byte[]> soft = new SoftReference<>(largeData);byte[] data = soft.get(); // may survive several GC cycles
// PhantomReference — enqueued after the object is finalized// Use for cleanup (e.g., releasing native resources)ReferenceQueue<byte[]> queue = new ReferenceQueue<>();PhantomReference<byte[]> phantom = new PhantomReference<>(largeData, queue);// phantom.get() always returns null// Object is enqueued after GC, but before memory reclamationReference Queues
Section titled “Reference Queues”Reference queues allow you to be notified when a reference is cleared by the GC. This is useful for Cleanup:
ReferenceQueue<Object> queue = new ReferenceQueue<>();Map<Object, String> metadata = new ConcurrentHashMap<>();
// Create a phantom reference with a queueObject resource = new Object();PhantomReference<Object> ref = new PhantomReference<>(resource, queue);
// When the GC clears the reference, it is enqueued// A background thread can poll the queue and perform cleanupThread cleaner = new Thread(() -> { while (true) { try { Reference<? extends Object> cleared = queue.remove(); String meta = metadata.remove(cleared); if (meta != null) { cleanupNativeResource(meta); } } catch (InterruptedException e) { break; } }});cleaner.setDaemon(true);cleaner.start();Finalizers
Section titled “Finalizers”Object.finalize() is called by the GC before an object’s memory is reclaimed. Do not use Finalizers. They are deprecated for removal since JDK 18.
Problems with Finalizers
Section titled “Problems with Finalizers”- Unpredictable timing. There is no guarantee when (or if) the finalizer will run.
- Performance. Finalizable objects require extra GC cycles.
- Resurrection. A finalizer can make the object reachable again by storing
thisin a static field. - Exceptions in finalizers are silently swallowed. They do not propagate.
- Finalizer thread bottleneck. A single finalizer thread processes all finalizable objects, creating a bottleneck.
// DEPRECATED — do not usepublic class Resource { @Override protected void finalize() throws Throwable { try { close(); } finally { super.finalize(); } } public void close() { /* release resources */ }}
// CORRECT — use try-with-resourcespublic class Resource implements AutoCloseable { public void close() { /* release resources */ }}
try (Resource r = new Resource()) { // use resource} // close() called automaticallyCommon Pitfalls
Section titled “Common Pitfalls”Setting Xms Much Lower Than Xmx
Section titled “Setting Xms Much Lower Than Xmx”# BAD — JVM starts small and grows, causing multiple GC cycles during startupjava -Xms128m -Xmx4g MyApp
# GOOD — allocate the full heap upfrontjava -Xms4g -Xmx4g MyAppIgnoring GC Logs Until Production
Section titled “Ignoring GC Logs Until Production”Never ship to production without GC logging. You cannot tune what you cannot measure. Enable GC Logging from day one, even in development.
Premature Promotion
Section titled “Premature Promotion”If objects are promoted to the old generation too quickly, the old generation fills up and triggers Expensive full GCs. Causes: young generation too small, survivor spaces too small, large objects (allocated directly in old generation if they exceed the PretenureSizeThreshold).
# Monitor promotion rate-Xlog:gc+age=debug # Shows object aging and promotionUsing System.gc()
Section titled “Using System.gc()”System.gc() is a hint to the JVM to perform a full GC. It is not guaranteed to run, and it can Trigger an unexpected long pause. Never call it in application code. Disable explicit GC calls with:
-XX:+DisableExplicitGCOver-Tuning GC
Section titled “Over-Tuning GC”Do not tune GC until you have measured and identified a problem. Many applications run fine with Default settings. Start with G1 (the default) and only tune specific flags if you have concrete Evidence of a problem (long pauses, low throughput, high memory usage).
Full GC Under G1
Section titled “Full GC Under G1”If G1 falls back to full GC, it means the concurrent cycle could not keep up with allocation Pressure. This is a sign that the heap is too small, the marking threshold is too high, or the mixed Collections are not reclaiming enough memory. Address the root cause rather than trying to tune Around it.
Large Objects and Humongous Allocations
Section titled “Large Objects and Humongous Allocations”Objects larger than half a G1 region size are allocated directly in the old generation as “humongous” objects. This bypasses the young generation entirely, which defeats the generational Hypothesis. Large byte arrays (e.g., pre-allocated buffers for I/O), large String objects, and big Collections are typical culprits.
# Monitor humongous allocations-Xlog:gc+humongous=debug
# Tune region size to reduce humongous allocations# Default region size is 1/2048 of heap, rounded to power of 2# For a 4 GB heap, default region size is 2 MB (humongous threshold = 1 MB)-XX:G1HeapRegionSize=8m # increases humongous threshold to 4 MBIf your application allocates many objects larger than the humongous threshold, consider: increasing The region size, pooling large buffers, or using MappedByteBuffer for large data structures that Do not need to live on the GC-managed heap.
String Deduplication in Detail
Section titled “String Deduplication in Detail”G1’s string deduplication identifies String objects in the old generation whose backing byte[] Arrays are identical, and replaces duplicates with a single shared array. This operates at GC time And is transparent to the application.
-XX:+UseG1GC -XX:+StringDeduplication-XX:StringDeduplicationAgeThreshold=3 # deduplicate strings promoted at least 3 GC cycles agoDeduplication works by maintaining a queue of candidate String objects. During a GC pause, the JVM Compares the backing byte[] of candidate strings. If a duplicate is found, the JVM replaces the Duplicate’s reference to point to the canonical array. The old array becomes garbage and is Collected in a subsequent GC cycle.
When to enable: Applications with high heap usage dominated by duplicate strings (web servers Processing similar requests, XML/JSON parsers, log aggregation). The overhead is minimal — a few Percent of GC pause time — but the memory savings can be substantial (20-30% reduction in live set Size for string-heavy workloads).
When NOT to enable: Applications with few duplicate strings (the deduplication work has no Benefit), or when using ZGC/Shenandoah (which do not support string deduplication).
GC in Containers and Cloud Environments
Section titled “GC in Containers and Cloud Environments”Container-Aware JVM
Section titled “Container-Aware JVM”Modern JVMs (JDK 8u191+, JDK 10+) automatically detect container limits (cgroups v1 and v2) and set Heap size defaults accordingly. Earlier JVMs would use host machine memory, leading to OOM kills in Containers.
# Verify container detectionjava -XshowSettings:system -version# Container count: 1 (if running in a container)# Memory Limit: 512M (from cgroup)Recommended GC Settings for Containers
Section titled “Recommended GC Settings for Containers”# G1 with container-aware defaults (JDK 11+)java -XX:+UseG1GC -XX:MaxRAMPercentage=75.0 -jar app.jar# Uses 75% of the container memory limit for heap
# Explicit heap sizing (if MaxRAMPercentage is insufficient)java -XX:+UseG1GC -Xms2g -Xmx2g -jar app.jar
# ZGC for latency-sensitive servicesjava -XX:+UseZGC -XX:MaxRAMPercentage=75.0 -jar app.jarThe JVM’s heap is only part of the total memory used by a Java process. Native memory includes Metaspace, thread stacks, direct buffers, code cache, and JNI allocations. In containers, native Memory usage can cause OOM kills even when heap usage is low.
# Enable native memory tracking (small runtime overhead, ~5%)-XX:NativeMemoryTracking=summary
# Print native memory summary at JVM exit-XX:+PrintNMTStatistics
# Runtime dump via jcmdjcmd <pid> VM.native_memory summaryjcmd <pid> VM.native_memory detail scale=MBA typical memory breakdown for a JVM process:
| Component | Typical Allocation |
|---|---|
| Java heap | 60-75% of container memory |
| Metaspace | 50-256 MB |
| Thread stacks | 1 MB per thread (default -Xss1m) |
| Code cache | 128-256 MB |
| Direct buffers | Application-dependent |
| GC overhead | 10-20% of heap |
GC Troubleshooting Workflow
Section titled “GC Troubleshooting Workflow”Step 1: Enable GC Logging
Section titled “Step 1: Enable GC Logging”-Xlog:gc*=info:file=gc.log:time,uptime,level,tags:filecount=5,filesize=100MStep 2: Identify the Symptom
Section titled “Step 2: Identify the Symptom”- Long pauses — check pause time distribution. Is it minor GC or full GC?
- Low throughput — check GC time ratio. If GC takes more than 5-10% of CPU time, investigate.
- Heap exhaustion — check old generation growth. Is it linear (memory leak) or stable?
- Allocation failure — check allocation rate. If the young generation fills too fast, increase its size.
Step 3: Analyze and Fix
Section titled “Step 3: Analyze and Fix”# Parse GC logs with JDK toolsjcmd <pid> GC.heap_infojcmd <pid> GC.run # suggest GC (diagnostic, not System.gc())jstat -gc <pid> 1000 10 # GC stats every 1 second, 10 timesjstat -gccause <pid> 1000 # include GC causejmap -histo <pid> | head -20 # top 20 objects by sizeStep 4: Verify
Section titled “Step 4: Verify”After making changes, compare GC logs before and after. Look for:
- Reduced pause times (P50, P99).
- Higher throughput (lower GC time ratio).
- Reduced promotion rate (fewer objects surviving to old generation).
- No increase in full GC frequency.
Mark-Sweep-Compact (Serial and Parallel)
Section titled “Mark-Sweep-Compact (Serial and Parallel)”The classic GC algorithm used by Serial and Parallel collectors:
Mark phase. Starting from GC roots, traverse the object graph and mark all reachable objects. This requires a STW pause. The Parallel collector uses multiple threads for marking.
Sweep phase. Scan the heap linearly. Unmarked objects are garbage. Their memory is added to free lists. Live objects are not moved.
Compact phase. Move all live objects to the beginning of the heap, eliminating fragmentation. Update all references to moved objects. This requires another STW pause.
Time complexity: O(heap size) for mark, O(heap size) for sweep, O(live objects) for compact. The Entire heap is scanned, so pause times scale linearly with heap size. This is why Serial and Parallel GC are unsuitable for large heaps.
G1 Region-Based Collection
Section titled “G1 Region-Based Collection”G1 divides the heap into fixed-size regions (1-32 MB). Each region can be Eden, Survivor, Old, Humongous, or Empty. The key insight: instead of collecting the entire heap, collect only the Regions with the most garbage.
G1 concurrent marking cycle:
Initial mark. STW pause. Marks GC roots and marks the regions they point to as “dirty.” Piggybacks on a young GC pause (minimal additional cost).
Root region scanning. Concurrent. Scans the dirty regions from the initial mark to find references to old generation regions.
Concurrent marking. Concurrent. Traces the object graph from roots to mark all live objects. Uses a SATB (Snapshot-At-The-Beginning) write barrier to handle mutations during marking.
Remark. STW pause. Processes any remaining SATB buffers and completes marking. Reclaims completely empty regions.
Cleanup. Optional concurrent phase. Resets region state and reclaims empty regions. May trigger mixed collections.
Mixed collections: After a marking cycle, G1 selects old regions with the most reclaimable space (along with all young regions) and collects them. The number of mixed collections per cycle is Controlled by -XX:G1MixedGCCountTarget (default 8).
ZGC Colored Pointers and Load Barriers
Section titled “ZGC Colored Pointers and Load Barriers”ZGC’s design is fundamentally different from G1 and Parallel:
Colored pointers: On 64-bit platforms, ZGC uses the high bits of each reference (which are Normally unused because no system has 2^48 bytes of addressable memory) to store metadata bits:
| Bits | Usage |
|---|---|
| 0-41 | Object address (4 TB addressable) |
| 42-43 | Metadata (Finalizable, Remapped, Marked1, Marked0) |
| 44-62 | Unused (reserved for future use) |
| 63 | Unused |
The metadata bits encode the state of the object: whether it has been marked, relocated, or Finalized. No per-object header space is needed for GC metadata.
Load barriers: Every reference read from the heap triggers a load barrier. The load barrier Checks the metadata bits of the loaded reference:
- If the reference points to a relocated object, the barrier fixes it to point to the new location.
- If the reference points to a not-yet-marked object, the barrier marks it (or adds it to a marking queue).
This is how ZGC achieves concurrent relocation — the GC can move objects while the application is Running, and the load barriers transparently fix any stale references the application encounters.
Phases:
- Pause mark start. STW (sub-ms). Mark GC roots.
- Concurrent mark. Concurrent. Trace the object graph using load barriers.
- Pause mark end. STW (sub-ms). Process remaining marking work.
- Concurrent relocate prepare. Concurrent. Identify regions to evacuate.
- Pause relocate start. STW (sub-ms). Select relocation set.
- Concurrent relocate. Concurrent. Move live objects to new regions. Load barriers fix stale references.
Generational ZGC (JDK 21+)
Section titled “Generational ZGC (JDK 21+)”Generational ZGC separates the heap into young and old generations, like G1. Most objects are Allocated in the young generation and collected frequently (young-only collections). Objects that Survive multiple young collections are promoted to the old generation. This reduces the amount of Data the GC must scan during old generation collections.
-XX:+UseZGC -XX:+ZGenerationalGenerational ZGC is the default ZGC mode in JDK 21+. It uses 4-8x less memory for GC Metadata compared to non-generational ZGC and achieves higher throughput for most workloads.
Practical GC Tuning Examples
Section titled “Practical GC Tuning Examples”Low-Latency Web Service
Section titled “Low-Latency Web Service”# Target: P99 pause under 50msjava -XX:+UseZGC \ -XX:SoftMaxHeapSize=4g \ -Xmx8g \ -XX:+ZGenerational \ -Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=5,filesize=100M \ -jar web-service.jarHigh-Throughput Batch Processing
Section titled “High-Throughput Batch Processing”# Target: Maximum throughput, pauses acceptable up to 2sjava -XX:+UseParallelGC \ -XX:ParallelGCThreads=8 \ -Xms16g -Xmx16g \ -XX:+UseParallelOldGC \ -Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=5,filesize=100M \ -jar batch-processor.jarBalanced General-Purpose Service
Section titled “Balanced General-Purpose Service”# Target: P99 pause under 200ms, good throughputjava -XX:+UseG1GC \ -XX:MaxGCPauseMillis=200 \ -Xms4g -Xmx4g \ -XX:G1HeapRegionSize=8m \ -XX:InitiatingHeapOccupancyPercent=45 \ -XX:+StringDeduplication \ -Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=5,filesize=100M \ -jar app.jarContainerized Microservice
Section titled “Containerized Microservice”# Target: Auto-size to container limits, low latencyjava -XX:+UseZGC \ -XX:MaxRAMPercentage=75.0 \ -XX:ZAllocationSpikeTolerance=5 \ -Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=5,filesize=50M \ -jar microservice.jarSummary
Section titled “Summary”This topic covers the biological principles of garbage collection, including key concepts, experimental evidence, and real-world applications.
Key concepts include:
- key biological principles and concepts
- experimental methods and data analysis
- applications of biology in medicine and industry
- ethical considerations in biological research
- the relationship between structure and function
Success requires the ability to recall specific factual content, apply knowledge to novel scenarios, and evaluate experimental evidence critically.
Worked Examples
Section titled “Worked Examples”Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.
Cross-References
Section titled “Cross-References”- Types and Variables: Object lifecycle and reference types that GC manages.
- Concurrency Deep Dive: Thread-safe memory access and GC pause impact on concurrent applications.
- I/O and NIO: Memory-mapped files and direct buffers that bypass the GC-managed heap.
Intuition
Section titled “Intuition”Garbage collection frees you from manually managing memory, but understanding how it works helps you write code that works with the collector instead of against it. The core insight of generational GC is that most objects die young. By separating short-lived objects (young generation) from long-lived ones (old generation), the collector can run frequent, fast scans on the small young generation while doing expensive full collections rarely on the old generation. This matches real-world allocation patterns where temporary buffers, iterators, and intermediate results are created and discarded constantly.
The evolution from Serial to G1 to ZGC shows a clear trajectory: minimize pause times by doing more work concurrently with your application. Serial and Parallel collectors stop the world for the entire collection. G1 divides the heap into regions and only collects the dirtiest ones, keeping pauses bounded. ZGC goes further by doing almost all work concurrently — marking, relocating, and compacting while your application keeps running. Load barriers in ZGC transparently fix stale references as your code reads them, making concurrent relocation invisible to the application.
The practical takeaway is to match your GC choice to your latency requirements. Throughput-bound batch jobs can tolerate longer pauses, so Parallel GC is fine. Latency-sensitive web services need G1 or ZGC. And in containers, always set -Xms == -Xmx and use MaxRAMPercentage so the JVM knows its memory limits from the start. GC logging should be enabled from day one — you cannot tune what you cannot measure.