Understanding OpenJDK Garbage Collectors: Experiments and Performance Analysis
This article examines the pause behavior of various OpenJDK garbage collectors by running a memory‑allocation benchmark, presenting detailed GC logs for G1, Parallel, CMS, Shenandoah, and Epsilon, and drawing conclusions on how to choose the appropriate collector for a given workload.
The article introduces the common misconception that JVM GC pauses are always the root cause of performance problems, emphasizing that the real issue often lies in mismatched GC implementations or unsuitable collector choices for the workload.
Problem
Even allocating 100 MB of memory can cause several seconds of JVM pause, and many workloads suffer from inappropriate GC selection.
Experiment
A simple Java program creates 100 million short‑lived objects to stress the heap:
import java.util.*;
public class AL {
static List<Object> l;
public static void main(String... args) {
l = new ArrayList<>();
for (int c = 0; c < 100_000_000; c++) {
l.add(new Object());
}
}
}The benchmark is run on JDK 9 with a 4 GB heap using different collectors.
G1
$ time java -Xms4G -Xmx4G -Xlog:gc AL
[0.030s][info][gc] Using G1
[1.525s][info][gc] GC(0) Pause Young (G1 Evacuation Pause) 370M->367M(4096M) 991.610ms
... (additional GC log lines) ...
real 0m12.016s
user 0m34.588s
sys 0m0.964sG1 shows young‑generation pauses between 500 ms and 1 s, with concurrent phases overlapping later pauses; the “user” time exceeds “real” time because GC work runs in parallel.
Parallel
$ time java -XX:+UseParallelOldGC -Xms4G -Xmx4G -Xlog:gc AL
[0.023s][info][gc] Using Parallel
[1.579s][info][gc] GC(0) Pause Young (Allocation Failure) 878M->714M(3925M) 1144.518ms
... (additional GC log lines) ...
real 0m3.882s
user 0m11.032s
sys 0m1.516sParallel GC also exhibits short young‑generation pauses, but only two large pauses dominate, allowing the JVM to finish quickly.
Concurrent Mark Sweep (CMS)
$ time java -XX:+UseConcMarkSweepGC -Xms4G -Xmx4G -Xlog:gc AL
[0.012s][info][gc] Using Concurrent Mark Sweep
[1.984s][info][gc] GC(0) Pause Young (Allocation Failure) 259M->231M(4062M) 1788.983ms
... (additional GC log lines) ...
real 0m17.719s
user 0m45.692s
sys 0m0.588sCMS performs concurrent marking for the old generation while still having stop‑the‑world young pauses; the lack of heuristic pause‑time control leads to longer young pauses.
Shenandoah
$ time java -XX:+UseShenandoahGC -Xms4G -Xmx4G -Xlog:gc AL
[0.026s][info][gc] Using Shenandoah
[0.808s][info][gc] GC(0) Pause Init Mark 0.839ms
... (additional GC log lines) ...
real 0m2.021s
user 0m5.172s
sys 0m0.420sShenandoah is a non‑generational collector that performs most work concurrently, resulting in very short pause times; the VM termination ends the second GC cycle early.
Epsilon
$ time java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -Xms4G -Xmx4G -Xlog:gc AL
[0.031s][info][gc] Initialized with 4096M non‑resizable heap.
[0.031s][info][gc] Using Epsilon GC
[1.361s][info][gc] Total allocated: 2834042 KB.
real 0m1.415s
user 0m1.240s
sys 0m0.304sEpsilon performs no collection, showing that with a sufficiently large heap the application can run without any GC‑induced pauses.
Conclusion
Different GC algorithms make different trade‑offs; blindly disabling GC is a bad idea. Understanding the workload, available collectors, and performance requirements is essential for selecting the right collector, and even when using a no‑GC platform, one must still be aware of the underlying memory allocator.
High Availability Architecture
Official account for High Availability Architecture.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.