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

Java虚拟机:串行回收器

在 JVM 的垃圾回收器家族中,串行回收器(Serial GC) 是最古老、最基础的一位成员。虽然它不像 G1、ZGC 那样“高大上”,但正是它奠定了现代垃圾回收的基石。理解 Serial GC,不仅有助于我们应对老系统的性能问题,更能帮助我们深刻理解 GC 的设计思想。

本文将带你从工作原理、回收算法、日志解读、参数配置到应用场景,全方位剖析 Serial GC。


一、什么是串行回收器?

串行回收器是 JDK 中最基本的垃圾回收器之一,它的核心特点可以用两个词概括:单线程 和 独占式。

1.1 单线程回收

串行回收器在进行垃圾回收时,仅使用一个线程来完成所有 GC 工作。这意味着:

  • 没有多线程协作的复杂逻辑

  • 没有线程切换的开销

  • 实现简单,执行高效

1.2 独占式回收(Stop-The-World)

串行回收器是独占式的——当它开始工作时,Java 应用程序中的所有用户线程都必须完全暂停,等待 GC 完成。这个过程被称为 "Stop-The-World"(简称 STW)。

在 GC 期间,所有业务线程都被“冻结”,这对实时性要求较高的系统来说,可能是不可接受的。但在某些场景下,这种“简单粗暴”的方式反而带来了最佳性能。


二、新生代串行回收器(Serial Young GC)

2.1 算法:复制算法

新生代串行回收器采用 复制算法(Copying Algorithm)。

其核心思想是:将新生代内存划分为一个较大的 Eden 区 和两个较小的 Survivor 区(From 和 To)。每次 GC 时,将 Eden 和 From 中存活的对象复制到 To 区,然后清空 Eden 和 From,最后交换 From 和 To 的角色。

为什么用复制算法?

  • 新生代对象朝生夕死,存活率极低

  • 复制算法的效率与存活对象数量成正比,非常适合新生代

  • 实现简单,没有内存碎片问题

2.2 优势与局限

优势局限
实现简单,逻辑高效 单线程无法利用多核 CPU
无线程切换开销 GC 期间必须 STW
在单 CPU 环境下性能优异 堆内存较大时停顿时间过长

2.3 参数配置

-XX:+UseSerialGC

使用该参数后,JVM 会同时启用:

  • 新生代:Serial 收集器(使用复制算法)

  • 老年代:Serial Old 收集器(使用标记-压缩算法)

在 Client 模式 下,Serial GC 是 JVM 的默认垃圾收集器。


三、老年代串行回收器(Serial Old GC)

3.1 算法:标记-压缩算法

老年代串行回收器采用 标记-压缩算法(Mark-Compact Algorithm)。

该算法分为三个阶段:

  • 标记(Mark):从 GC Roots 出发,标记所有存活对象

  • 压缩(Compact):将所有存活对象向一端移动,使它们连续排列

  • 清理(Sweep):清理掉边界以外的内存空间

  • 为什么老年代不用复制算法?

    • 老年代对象存活率高,复制成本太高

    • 标记-压缩算法避免了内存碎片,且不需要额外空间

    3.2 注意:老年代 GC 停顿更长

    由于老年代空间通常更大,且存活对象更多,Full GC 的停顿时间往往远超 Young GC。在堆内存较大的应用中,一次 Serial Old GC 可能导致秒级甚至更长的停顿,这在生产环境中需要特别警惕。

    3.3 参数配置

    # 方式一:新生代和老年代都使用串行
    -XX:+UseSerialGC

    # 方式二:新生代使用 ParNew,老年代使用串行
    -XX:+UseParNewGC

    # 方式三:新生代使用 ParallelGC,老年代使用串行
    -XX:+UseParallelGC


    四、GC 日志深度解读

    4.1 新生代 GC 日志

    [GC [DefNew: 310K->194K(2368K), 0.0269163 secs] 310K->194K(7680K), 0.0269513 secs] [Times: user=0.00 sys=0.00, real=0.03 secs]

    我们来逐段拆解:

    日志片段含义
    GC 表示这是一次 Young GC(而非 Full GC)
    [DefNew 表示 GC 发生在新生代(Default New Generation),这是 Serial GC 特有的名称
    310K->194K(2368K) 新生代:GC前已使用→GC后已使用(该区域总容量)
    310K->194K(7680K) 整个堆:GC前已使用→GC后已使用(堆总容量)
    0.0269163 secs 该区域 GC 耗时
    [Times: user=0.00 sys=0.00 real=0.03 secs] 见下方详解

    Times 字段详解:

    • user:GC 线程消耗的 CPU 时间

    • sys:操作系统调用及等待系统事件的时间

    • real:应用程序实际暂停的时间(挂钟时间)

    对于 Serial GC,由于只使用单线程,real ≈ user + sys。

    4.2 老年代 GC(Full GC)日志

    25.299: [Full GC (Allocation Failure) 25.300: [Tenured: 126975K->84388K(126976K), 0.1865275 secs] 741375K->84388K(741376K), [Metaspace: 3476K->3476K(1056768K)], 0.1866490 secs] [Times: user=0.19 sys=0.00, real=0.18 secs]

    日志片段含义
    Full GC 表示这是一次 Full GC
    (Allocation Failure) GC 触发原因:分配对象失败(内存不足)
    [Tenured 老年代(Tenured Generation)的 GC 信息
    126975K->84388K(126976K) 老年代:GC前→GC后(总容量)
    741375K->84388K(741376K) 整个堆:GC前→GC后(总容量)
    Metaspace: 3476K->3476K(1056768K) 元空间信息(JDK 8+)

    4.3 不同收集器的新生代名称对照

    收集器新生代日志名称含义
    Serial [DefNew Default New Generation
    ParNew [ParNew Parallel New Generation
    Parallel Scavenge [PSYoungGen Parallel Scavenge Young Generation

    五、示例代码与运行验证

    下面我们用一段代码来模拟内存溢出,观察 Serial GC 的工作过程:

    import java.util.Random;

    public class GCDemo {
    public static void main(String[] args) {
    System.out.println("===== GCDemo, Hello =====");
    try {
    String str = "GCDemo";
    while (true) {
    str += str + new Random().nextInt(77777777)
    + new Random().nextInt(88888888);
    str.intern();
    }
    } catch (Throwable e) {
    e.printStackTrace();
    }
    }
    }

    运行参数:

    -XX:+UseSerialGC -Xms10m -Xmx10m -XX:+PrintGCDetails

    输出日志示例:

    [GC [DefNew: 1393K->25K(3072K), 0.009758 secs] 5793K->5793K(9920K), 0.0010059 secs]
    [Full GC (Allocation Failure) [Tenured: 5767K->4167K(6948K), 0.0023995 secs] …
    java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Arrays.java:3332)
    at java.lang.AbstractStringBuilder.ensureCapacityInternal(…)

    可以看到:

  • DefNew 确认了 Serial GC 在新生代工作

  • Tenured 确认了 Serial Old 在老年代工作

  • 频繁的 GC 最终导致 OutOfMemoryError


  • 六、总结:Serial GC 的优缺点与应用场景

    6.1 优点

    ✅ 实现简单,逻辑高效 — 代码成熟稳定,几乎没有复杂 bug
    ✅ 单线程无交互开销 — 没有多线程锁竞争和上下文切换
    ✅ 在单 CPU 环境下性能最佳 — 多线程反而会带来额外开销
    ✅ 内存占用小 — 不需要维护复杂的数据结构

    6.2 缺点

    ❌ STW 停顿时间长 — 堆内存越大,停顿越明显
    ❌ 无法利用多核 CPU — 在多核时代显得有些“落伍”
    ❌ 用户体验差 — 在交互式应用中,停顿可能导致卡顿

    6.3 适用场景

    场景是否适用原因
    Client 模式下的 JVM ✅ 默认选择 资源受限环境,简单高效
    单 CPU 服务器 ✅ 最佳选择 多线程无法带来收益
    堆内存 < 100MB ✅ 可接受 停顿时间可控
    实时 Web 应用 ❌ 不推荐 停顿影响用户体验
    大堆内存(>4GB) ❌ 不推荐 停顿时间过长

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Java虚拟机:串行回收器
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!