云计算百科
云计算领域专业知识百科平台

【JVM原理详解】25-Serial与ParNew收集器

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 算法。它主要有两个用途:

  • 在 Client 模式下与 Serial 配套。
  • 作为 CMS 的"后备"——当 CMS 出现 Concurrent Mode Failure 时退化为 Serial Old 执行 Full GC。
  • 第二种场景在 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

    对比日志关键字段:

    字段SerialParNew
    新生代名 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调优实战

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【JVM原理详解】25-Serial与ParNew收集器
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!