跳转至

Serial 与 Parallel:吞吐型垃圾收集器#

版本信息

  • 覆盖:Serial GC | Parallel GC(Parallel Scavenge + Parallel Old)
  • 默认:Serial 是 client 模式/小堆默认;Parallel 是 JDK 8 服务端默认JEP 248 后 JDK 9 起改默认 G1)
  • MXBean 名:Serial → Copy / MarkSweep Compact;Parallel → PS Scavenge / PS MarkSweep(官方标准名)

如果你从 JDK 8 升上来,你原来用的默认 GC 就是 Parallel。它简单、吞吐高,代价是 Full GC 整堆、停顿较长。本页讲清它的原理和参数——以及「升级后我还该不该用它」。

设计目标:吞吐优先#

Serial 与 Parallel 都属于吞吐型收集器,回收时全程 STW,不与应用线程并发:

  • Serial:单线程回收。简单、省内存,适合小堆、单核或资源受限(client 应用、IoT、单核容器)。
  • Parallel(Parallel Scavenge + Parallel Old):多线程并行回收,JDK 8 服务端默认。它有一套自适应(ergonomics)机制,能根据 -XX:MaxGCPauseMillis / -XX:GCTimeRatio 目标自动调整年轻代大小,追求最大吞吐

它们的共同代价:没有并发回收,Full GC 是整堆 STW,堆越大停顿越长。这正是后来 G1/ZGC 要解决的。

工作原理:分代 + 全 STW#

graph TD
    A["应用分配对象"] --> E["Eden 区满"]
    E --> M["Minor GC(STW)<br/>复制存活对象到 Survivor"]
    M --> S["Survivor 区"]
    S -->|"多轮存活后晋升"| O["老年代"]
    O -->|"老年代占用达阈值"| F["Full GC(STW)<br/>标记-压缩,整理全堆"]
    style M fill:#bbdefb
    style F fill:#ffcdd2
  • 年轻代复制算法:Eden 满 → Minor GC(STW),存活对象拷进 Survivor,重复存活足够多次(由 -XX:MaxTenuringThreshold 控制)的对象晋升老年代。
  • 老年代标记-压缩(mark-compact):标记可达对象、清理不可达、再滑动整理,避免碎片(这点强于不整理的 CMS)。
  • Serial 与 Parallel 的算法骨架相同,差别只在单线程 vs 多线程。所有阶段都是 STW——「并行」是指多个 GC 线程同时干活,应用线程照常暂停。

JVM 配置参数#

启用#

java -XX:+UseSerialGC      -Xmx512m -jar app.jar   # Serial:单线程
java -XX:+UseParallelGC    -Xmx4g   -jar app.jar   # Parallel:JDK 8 server 默认,多线程

Parallel 关键调优参数#

参数 含义 默认 备注
-XX:MaxGCPauseMillis=<ms> 期望最大停顿目标 不限制 ergonomics 据此缩小年轻代;设得太激进会频繁 Minor GC
-XX:GCTimeRatio=<n> GC 时间占比目标 = 1/(1+n) 99(≈1%) 越大越求吞吐;调大减少 GC 频率
-XX:+UseAdaptiveSizePolicy 自适应调整 Eden/Survivor/年轻代大小 与手动 -Xmn 冲突:手动固定后自适应失效
-XX:ParallelGCThreads=<n> STW 阶段的并行 GC 线程数 ≈ 物理 CPU 核数 容器内注意别超配,否则上下文切换反噬
-XX:NewRatio=<r> 老年代 : 年轻代 = r : 1 2(即 2:1) 想加大年轻代可调小,如 -XX:NewRatio=1
-XX:SurvivorRatio=<r> Eden : 每个 Survivor = r : 1 8 调大给 Eden 更多空间
-XX:MaxTenuringThreshold=<n> 对象晋升老年代的存活轮数上限 15 调小让对象更快进老年代
# 典型:吞吐型批处理,求高吞吐、允许较长停顿
java -XX:+UseParallelGC -Xmx8g \
     -XX:GCTimeRatio=49 \            # GC 占比目标 ≈ 2%,更重吞吐
     -XX:MaxGCPauseMillis=500 \      # 停顿软目标 500ms
     -XX:ParallelGCThreads=8 \
     -jar batch-job.jar

适用场景#

  • Serial:堆很小(一般 < 100MB)、单核 CPU、client 桌面程序、资源受限设备;或只想用最少内存跑个 JVM的场景。
  • ParallelCPU 密集、批处理、离线数据分析、ETL——对单次停顿不敏感、但求稳态吞吐最大。没有交互、没有实时 SLA 的后台任务用它很合适。

与前代 GC 对比:Serial → Parallel#

Parallel 是 Serial 的多线程并行升级——同为分代 + 全 STW、同为复制 / 标记压缩,但回收时用多个 GC 线程而非单线程,多核机器上吞吐显著提升。两者取舍看堆大小与 CPU 核数。

java -XX:+UseSerialGC -Xmx512m -jar app.jar   # 单线程回收,省内存、适合单核
java -XX:+UseParallelGC -Xmx8g \              # 多线程并行,吞吐优先
     -XX:GCTimeRatio=49 -jar batch.jar

小堆 / 单核用 Serial(无多线程开销);多核吞吐型用 Parallel。注意现代 JDK(9+)默认已是 G1,需 -XX:+UseParallelGC 显式切回。

常见坑 / 最佳实践#

  • 升级后别无脑加 -XX:+UseParallelGC:JDK 9+ 默认 G1 已对多数场景更好。除非吞吐型负载有实测收益,否则用默认 G1。
  • MaxGCPauseMillis 是软目标,不是硬保证:Parallel 会尽量靠近,设得过小只会让年轻代缩到很小、Minor GC 更频繁,吞吐反降。
  • 别同时设 -Xmn 和自适应:手动固定年轻代(-Xmn/-XX:NewRatio)会让 -XX:+UseAdaptiveSizePolicy 失去作用,两套调整打架。
  • 容器内注意 ParallelGCThreads:默认按物理核数,在 CPU 限额的容器里可能远超配额,建议显式设成容器限额核数。
  • Full GC 整堆长停顿是它的硬伤:堆一大、对象一多,Full GC 停顿可能到秒级——对延迟敏感就该换 G1/ZGC,而非死调 Parallel。

小结#

Serial 与 Parallel 是「吞吐优先、回收全 STW」的经典选择。Serial 适合小堆/单核,Parallel 曾是 JDK 8 服务端默认、适合批处理等吞吐型负载。但它的 Full GC 长停顿在交互/在线场景是硬伤——这正是 G1(停顿可控)和 ZGC/Shenandoah(亚毫秒)登场的原因。

参考#