25-Serial 与 ParNew 收集器前面几篇讲的是算法从本篇开始进入实现——具体到 HotSpot 提供的几种垃圾收集器。最古老、也是最简单的就是Serial和Serial Old收集器它们是理解后续所有收集器的基础。ParNew则是 Serial 的多线程版本专为配合 CMS 而生。本篇将剖析它们的设计、参数、组合关系与适用场景。Serial 收集器Serial 是 HotSpot 最古老的收集器单线程回收回收时必须 STWStop The World期间所有应用线程挂起。它的新生代采用复制算法老年代对应的是 Serial OldMark-Compact。应用线程: ████░░░░░░░░████████... ↑ Serial STW设计哲学单线程听起来落后但它的优势恰恰是简单没有线程同步、协调开销单次 GC 的 CPU 利用率高。适合单核或小内存环境。在 Client 模式或嵌入式设备上Serial 是默认选择。即使在现代 JDK 中它依然是新生代默认收集器候选取决于平台和 JDK 版本。Serial OldSerial Old 是 Serial 的老年代版本同样单线程采用 Mark-Compact 算法。它主要有两个用途在 Client 模式下与 Serial 配套。作为 CMS 的后备——当 CMS 出现 Concurrent Mode Failure 时退化为 Serial Old 执行 Full GC。第二种场景在 JDK 8 CMS 的生产环境中是出了名的长停顿来源也是 CMS 被废弃的重要原因。关键参数# 显式指定使用 Serial Serial Oldjava-XX:UseSerialGC-cpMyApp com.example.Main-XX:UseSerialGC等同于同时启用 Serial新生代与 Serial Old老年代。JDK 8 在 Client 模式下默认就是这套组合。代码示例与日志分析/** * 演示 Serial GC 行为 * 适用 JDK 8/11/17 * * 运行 * java -Xms20m -Xmx20m -Xmn10m * -XX:SurvivorRatio8 -XX:UseSerialGC * -Xlog:gc*info -cp MyApp SerialGcDemo */publicclassSerialGcDemo{privatestaticfinalint_1MB1024*1024;publicstaticvoidmain(String[]args)throwsException{for(inti0;i8;i){byte[]blocknewbyte[2*_1MB];System.out.println(allocated i - block.length);Thread.sleep(300);}}}JDK 11 日志-Xlog:gc*info[0.123s][info][gc,start] GC(0) Pause Young (Allocation Failure) [0.123s][info][gc,task] GC(0) Using 1 workers [0.124s][info][gc,heap] GC(0) DefNew total 9216K, used 8000K [0.124s][info][gc,heap] GC(0) Eden space 8192K, 100% used [0.124s][info][gc,heap] GC(0) From space 1024K, 0% used [0.124s][info][gc,heap] GC(0) To space 1024K, 0% used [0.124s][info][gc,heap] GC(0) Tenured generation total 10240K, used 0K [0.124s][info][gc] GC(0) Pause Young (Allocation Failure) 7M-1M(20M) 1.234ms关注几个关键字段DefNewDefault New GenerationSerial 新生代的内部名。Using 1 workers单线程回收。7M-1M(20M)回收前 7M回收后 1M堆总大小 20M。1.234ms本次 GC 停顿。老年代 Full GC 日志[5.678s][info][gc,start] GC(5) Pause Full (Allocation Failure) [5.678s][info][gc,heap] GC(5) Tenured generation total 10240K, used 8000K [5.679s][info][gc] GC(5) Pause Full (Allocation Failure) 18M-12M(20M) 5.678msTenured是 Serial Old 老年代名。可以看到 Full GC 的停顿比 Minor GC 长得多——这是 Mark-Compact 算法的固有代价。ParNew 收集器ParNew 是 Serial 的多线程版本新生代复制算法、STW但回收时用多个 GC 线程并行。它是唯一能与 CMS 配合的新生代收集器。应用线程: ████░░░░░░░░████████... ↑ ParNew STW多 GC 线程并行与 Serial 的关系ParNew 在单核环境下不会比 Serial 更快——多线程协调反而有额外开销。但在多核环境下多线程并行能显著缩短 STW 时间单线程: ████████████ (12ms) 4线程: ███ (3ms)这也是吞吐量 vs 延迟的早期权衡ParNew 通过并行降低单次停顿但总 CPU 开销与 Serial 相当甚至略高。关键参数# JDK 8显式启用 ParNew CMSjava-XX:UseParNewGC-XX:UseConcMarkSweepGC-cpMyApp com.example.Main# 控制并行 GC 线程数java-XX:UseParNewGC-XX:ParallelGCThreads4-cpMyApp com.example.MainParallelGCThreads默认值与 CPU 核数相关核数 ≤ 8 时等于核数否则为3 5N/8N 为核数。在容器化部署中建议显式设置避免误用宿主机核数。ParNew 的局限ParNew 只能回收新生代必须与一个老年代收集器搭配。JDK 8 可用组合ParNew CMS推荐低延迟ParNew Serial Old退化不推荐JDK 9 起由于 CMS 被标记 deprecatedParNew 也被限制-XX:UseParNewGC单独使用会报警告最终在 JDK 14 随 CMS 一起被移除。新代码应直接用 G1。收集器组合关系HotSpot 不同代际收集器的组合关系是有限制的。下表是 JDK 8 的合法组合新生代老年代说明SerialSerial OldClient 模式、嵌入式ParNewCMS低延迟首选ParNewSerial Old不推荐仅兼容性Parallel ScavengeParallel Old吞吐量优先G1自身分代JDK 9 默认注意ParNew Parallel Old不是合法组合。原因是 ParNew 与 CMS 共享代码框架Parallel Scavenge 走的是另一套实现。这种组合限制常常是面试题的考点也是 JDK 9 后统一向 G1 收敛的内在动因。选型决策树堆 100MB → Serial Client/嵌入式 → Serial 吞吐量优先批处理 → Parallel Scavenge Parallel Old 低延迟JDK 8 → ParNew CMS 低延迟JDK 11 → G1或 ZGC 超低延迟JDK 15 → ZGC / Shenandoah代码示例对比 Serial 与 ParNew下面这段代码在两种收集器下运行对比 GC 日志差异/** * Serial vs ParNew 对比 * 适用 JDK 8 */publicclassCollectorCompare{privatestaticfinalint_1MB1024*1024;publicstaticvoidmain(String[]args)throwsException{// 持续分配制造 GC 压力for(inti0;i50;i){byte[]blocknewbyte[512*1024];Thread.sleep(50);}}}运行命令# Serialjava-Xmx200m-Xmn100m-XX:UseSerialGC\-Xlog:gc*info-cpMyApp CollectorCompare# ParNew CMSjava-Xmx200m-Xmn100m-XX:UseParNewGC-XX:UseConcMarkSweepGC\-Xlog:gc*info-cpMyApp CollectorCompare对比日志关键字段字段SerialParNew新生代名DefNewParNewGC 线程数Using 1 workersUsing 8 workers单次停顿较长较短多核时典型输出8 核机器200M 堆# Serial [0.045s][info][gc] GC(0) Pause Young (Allocation Failure) 60M-10M(200M) 4.321ms # ParNew [0.038s][info][gc] GC(0) Pause Young (Allocation Failure) 60M-10M(200M) 1.102ms多核环境下 ParNew 的停顿显著低于 Serial。但在单核或极小堆上Serial 因无线程协调开销可能反而更快。适用场景Serial / Serial Old嵌入式设备内存小数十 MB、CPU 弱单线程更高效。Client 模式桌面应用堆不大、对停顿不敏感。微服务预热阶段某些框架在启动期用 Serial 减少开销。教学/调试日志简单便于理解 GC 原理。ParNewJDK 8 CMS 的低延迟应用Web 服务、交易系统等对 STW 敏感的场景。多核中小堆堆在 4GB 以内时ParNew CMS 仍是合理选择。JDK 9 不再推荐应迁移到 G1。迁移建议如果你的系统还在用 ParNew CMSJDK 11切换到 G1通常能获得相近或更好的延迟表现。JDK 17评估 ZGC 或 Shenandoah追求个位数毫秒级停顿。迁移前用 GC 日志工具如 GCEasy分析现有 GC 模式作为基线对比。实践要点1.-XX:UseParallelGC不等于 ParNewJDK 8 中-XX:UseParallelGC启用的是Parallel Scavenge Parallel Old不是 ParNew。两者代码不同、组合不互通。命名容易混淆需特别注意。2. 容器中的 GC 线程数Docker/K8s 环境下JVM 可能误识别宿主机核数导致 GC 线程过多。建议显式设置java-XX:ParallelGCThreads4-XX:ConcGCThreads2-cpMyApp com.example.MainJDK 10 的容器感知-XX:UseContainerSupport默认开启已能自动识别 cgroup 限制但显式设置更稳妥。3. 日志格式统一JDK 9 引入统一日志格式-Xlog:JDK 8 用-XX:PrintGCDetails。迁移时记得转换参数# JDK 8-XX:PrintGCDetails-XX:PrintGCDateStamps-Xloggc:gc.log# JDK 11-Xlog:gc*info:filegc.log:time,uptime,level,tags4. 小堆场景下 Serial 可能更优堆小于 100MB 时多线程协调的开销可能超过并行收益。这时-XX:UseSerialGC反而更好。某些 IoT 场景就是如此。5. GC 日志分析工具GCEasygceasy.io在线分析给出停顿、吞吐量、内存分布等指标。GCViewer本地工具适合离线分析。JDK Mission ControlJMCJDK 11 自带可关联 GC 事件与应用行为。小结Serial / Serial Old单线程、STW、实现简单适合嵌入式和 Client 场景新生代用复制算法老年代用 Mark-Compact。ParNewSerial 的多线程版本新生代复制算法配合 CMS 使用JDK 9 起逐步退出历史舞台。收集器组合有严格限制ParNew 只能与 CMS / Serial Old 搭配不能与 Parallel Old 组合。容器化部署应显式设置ParallelGCThreads避免误用宿主机核数。现代应用建议直接使用 G1JDK 9 默认或 ZGC/ShenandoahJDK 15Serial/ParNew 仅在特定小内存或历史系统中保留。本模块至此完成了从判定对象存活到基础回收算法再到早期收集器的脉络。下一篇我们将进入更现代的收集器——Parallel Scavenge 与 CMS——继续探讨吞吐量与延迟的权衡。更多内容JVM调优实战