25-Serial 与 ParNew 收集器
前面几篇讲的是"算法",从本篇开始进入"实现"——具体到 HotSpot 提供的几种垃圾收集器。最古老、也是最简单的就是 Serial 和 Serial Old 收集器,它们是理解后续所有收集器的基础。ParNew 则是 Serial 的多线程版本,专为配合 CMS 而生。本篇将剖析它们的设计、参数、组合关系与适用场景。
Serial 收集器
Serial 是 HotSpot 最古老的收集器:单线程回收,回收时必须 STW(Stop The World),期间所有应用线程挂起。它的新生代采用复制算法,老年代对应的是 Serial Old(Mark-Compact)。
应用线程: ████░░░░░░░░████████…
↑
Serial STW
设计哲学
单线程听起来"落后",但它的优势恰恰是简单:
- 没有线程同步、协调开销,单次 GC 的 CPU 利用率高。
- 适合单核或小内存环境。
在 Client 模式或嵌入式设备上,Serial 是默认选择。即使在现代 JDK 中,它依然是新生代默认收集器候选(取决于平台和 JDK 版本)。
Serial Old
Serial Old 是 Serial 的老年代版本,同样单线程,采用 Mark-Compact 算法。它主要有两个用途:
第二种场景在 JDK 8 + CMS 的生产环境中是出了名的"长停顿"来源,也是 CMS 被废弃的重要原因。
关键参数
# 显式指定使用 Serial + Serial Old
java -XX:+UseSerialGC -cp MyApp com.example.Main
-XX:+UseSerialGC 等同于同时启用 Serial(新生代)与 Serial Old(老年代)。JDK 8 在 Client 模式下默认就是这套组合。
代码示例与日志分析
/**
* 演示 Serial GC 行为
* 适用 JDK 8/11/17
*
* 运行:
* java -Xms20m -Xmx20m -Xmn10m
* -XX:SurvivorRatio=8 -XX:+UseSerialGC
* -Xlog:gc*=info -cp MyApp SerialGcDemo
*/
public class SerialGcDemo {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws Exception {
for (int i = 0; i < 8; i++) {
byte[] block = new byte[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
关注几个关键字段:
- DefNew:Default New Generation,Serial 新生代的内部名。
- 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.678ms
Tenured 是 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 + CMS
java -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -cp MyApp com.example.Main
# 控制并行 GC 线程数
java -XX:+UseParNewGC -XX:ParallelGCThreads=4 -cp MyApp com.example.Main
ParallelGCThreads 默认值与 CPU 核数相关:核数 ≤ 8 时等于核数,否则为 3 + 5N/8(N 为核数)。在容器化部署中建议显式设置,避免误用宿主机核数。
ParNew 的局限
ParNew 只能回收新生代,必须与一个老年代收集器搭配。JDK 8 可用组合:
- ParNew + CMS(推荐,低延迟)
- ParNew + Serial Old(退化,不推荐)
JDK 9 起,由于 CMS 被标记 deprecated,ParNew 也被限制:-XX:+UseParNewGC 单独使用会报警告,最终在 JDK 14 随 CMS 一起被移除。新代码应直接用 G1。
收集器组合关系
HotSpot 不同代际收集器的组合关系是有限制的。下表是 JDK 8 的合法组合:
| Serial | Serial Old | Client 模式、嵌入式 |
| ParNew | CMS | 低延迟首选 |
| ParNew | Serial Old | 不推荐,仅兼容性 |
| Parallel Scavenge | Parallel 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
*/
public class CollectorCompare {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws Exception {
// 持续分配,制造 GC 压力
for (int i = 0; i < 50; i++) {
byte[] block = new byte[512 * 1024];
Thread.sleep(50);
}
}
}
运行命令:
# Serial
java -Xmx200m -Xmn100m -XX:+UseSerialGC \\
-Xlog:gc*=info -cp MyApp CollectorCompare
# ParNew + CMS
java -Xmx200m -Xmn100m -XX:+UseParNewGC -XX:+UseConcMarkSweepGC \\
-Xlog:gc*=info -cp MyApp CollectorCompare
对比日志关键字段:
| 新生代名 | DefNew | ParNew |
| GC 线程数 | Using 1 workers | Using 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 原理。
ParNew
- JDK 8 + CMS 的低延迟应用:Web 服务、交易系统等对 STW 敏感的场景。
- 多核中小堆:堆在 4GB 以内时,ParNew + CMS 仍是合理选择。
- JDK 9+ 不再推荐:应迁移到 G1。
迁移建议
如果你的系统还在用 ParNew + CMS:
- JDK 11:切换到 G1,通常能获得相近或更好的延迟表现。
- JDK 17+:评估 ZGC 或 Shenandoah,追求个位数毫秒级停顿。
- 迁移前用 GC 日志工具(如 GCEasy)分析现有 GC 模式,作为基线对比。
实践要点
1. -XX:+UseParallelGC 不等于 ParNew
JDK 8 中 -XX:+UseParallelGC 启用的是 Parallel Scavenge + Parallel Old,不是 ParNew。两者代码不同、组合不互通。命名容易混淆,需特别注意。
2. 容器中的 GC 线程数
Docker/K8s 环境下,JVM 可能误识别宿主机核数,导致 GC 线程过多。建议显式设置:
java -XX:ParallelGCThreads=4 -XX:ConcGCThreads=2 -cp MyApp com.example.Main
JDK 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:file=gc.log:time,uptime,level,tags
4. 小堆场景下 Serial 可能更优
堆小于 100MB 时,多线程协调的开销可能超过并行收益。这时 -XX:+UseSerialGC 反而更好。某些 IoT 场景就是如此。
5. GC 日志分析工具
- GCEasy(gceasy.io):在线分析,给出停顿、吞吐量、内存分布等指标。
- GCViewer:本地工具,适合离线分析。
- JDK Mission Control(JMC):JDK 11+ 自带,可关联 GC 事件与应用行为。
小结
- Serial / Serial Old:单线程、STW、实现简单,适合嵌入式和 Client 场景;新生代用复制算法,老年代用 Mark-Compact。
- ParNew:Serial 的多线程版本,新生代复制算法,配合 CMS 使用;JDK 9 起逐步退出历史舞台。
- 收集器组合有严格限制,ParNew 只能与 CMS / Serial Old 搭配,不能与 Parallel Old 组合。
- 容器化部署应显式设置 ParallelGCThreads,避免误用宿主机核数。
- 现代应用建议直接使用 G1(JDK 9+ 默认)或 ZGC/Shenandoah(JDK 15+),Serial/ParNew 仅在特定小内存或历史系统中保留。
本模块至此完成了从"判定对象存活"到"基础回收算法"再到"早期收集器"的脉络。下一篇我们将进入更现代的收集器——Parallel Scavenge 与 CMS——继续探讨吞吐量与延迟的权衡。
更多内容:JVM调优实战
网硕互联帮助中心




评论前必须登录!
注册