Goal

Maximise throughput (total work done per unit time) by doing GC work in parallel during stop-the-world pauses.

  • Type: Stop-the-world, multi-threaded collector
  • Focus: Throughput ✅, Latency ❌ (longer worst-case pauses are acceptable)
  • Generational: Yes (Young + Old)
  • Default history: Default up to JDK 8 (G1 is default from JDK 9+)

🧠 Mental model

[App threads run at full speed] → 
[Stop-the-world GC pause, many GC threads work in parallel] → 
[App resumes at full speed] → (repeat)

  • While the app runs, there is no concurrent GC work stealing CPU.
  • When memory pressure hits, all app threads stop; GC runs with multiple GC worker threads to finish quickly; then app resumes.

🧩 Heap layout (classic generational)

  • Young Gen (Eden + Survivors): frequent Minor GCs (parallel, STW).
  • Old Gen: occasional Major/Full GCs (parallel, STW).
  • Layout is the classic two-generation scheme; sizing is managed by ergonomics.

📈 Throughput

  • Strong side: app runs uninhibited between GCs; no concurrent barriers beyond the usual write barriers.
  • Parallelism: GC uses many worker threads to finish pauses quicker on multicore machines.
  • Result: excellent aggregate throughput for batch jobs, compute heavy services, and scenarios where tail latency isn’t strict.

⏱️ Latency

  • Trade-off: pauses can be long, especially when the Old Gen needs collection (entire old space is handled in a single STW operation).
  • If you need tighter tail latencies (p99/p999), consider G1 or ZGC.

🧵 Threads (how it scales)

  • Uses multiple GC worker threads (typically proportional to CPU count) during pauses.
  • More cores ⇒ shorter pauses (to a point), but still STW.

🔧 Quick tuning cheatsheet

Start here, then measure with JFR/JMX.

  • Enable (on modern JDKs where it’s not default):

    -XX:+UseParallelGC
    
  • Control GC worker threads (rarely needed; by default, ergonomics do fine):

    -XX:ParallelGCThreads=<N>
    
  • Young/Old balance (if you must nudge):

    -XX:NewRatio=<n>          # old/young ratio (e.g., 2 => old is 2x young)
    -XX:SurvivorRatio=<n>     # Eden:Survivor sizing hint
    
  • Adaptive sizing (on by default; let JVM chase a pause/throughput target):

    -XX:+UseAdaptiveSizePolicy
    -XX:MaxGCPauseMillis=<target>   # soft goal; Parallel may not meet strict low targets
    -XX:GCTimeRatio=<n>             # trade GC time vs app time (lower -> more GC, lower pauses)
    

Tip: With Parallel GC, throughput targets (e.g., -XX:GCTimeRatio) tend to be more meaningful than strict low MaxGCPauseMillis goals. If you truly need predictable low pauses, Parallel isn’t the right tool.


🔍 When to use Parallel GC

  • Batch/offline processing, ETL, report generation.
  • Throughput-first microservices where occasional longer pauses are acceptable.
  • Deployments with lots of CPU where parallel STW can finish fast and you don’t need sub-50 ms p99.

🧪 Observability (what to watch)

  • Pause times & frequency: GC logs/JFR (-Xlog:gc*,safepoint on JDK 9+).
  • Promotion failures / Old Gen expansions (sign of too-small heap or too-young promotion).
  • Allocation rate vs Minor GC cadence: ensure Young Gen sized for your allocation burst.

🗺️ Parallel vs G1 vs ZGC (1-liner)

  • Parallel: best throughput, STW pauses (can be long on Old Gen).
  • G1: balanced; region-based; mixed collections to avoid worst-case pauses.
  • ZGC: ultra-low latency (sub-ms pauses) with concurrent work; great for big heaps.

🧰 Minimal flag sets

Use Parallel GC on JDK 17+:

java -XX:+UseParallelGC -Xms4g -Xmx4g -jar app.jar

Nudge toward more throughput (softer pause goals):

java -XX:+UseParallelGC -XX:GCTimeRatio=9 -Xms4g -Xmx4g -jar app.jar

Heavier young gen (handle high alloc rate):

java -XX:+UseParallelGC -XX:NewRatio=2 -Xms8g -Xmx8g -jar app.jar