Serial垃圾收集器

84 阅读4分钟

Serial垃圾收集器

Serial 垃圾收集器:单线程 STW 之王的深度解析

Serial 收集器是 JVM 中最古老、最基础的垃圾收集器,作为单线程 STW 收集器的典范,它在特定场景下仍具有不可替代的价值。至今仍是许多垃圾收集器收集失败时的逃生门,进行兜底的垃圾收集。以下是全面剖析:

graph TD
    A[Serial 收集器] --> B[设计哲学]
    A --> C[工作模式]
    A --> D[内存管理]
    A --> E[适用场景]
    A --> F[演进地位]

    B --> B1[单线程]
    B --> B2[STW 暂停]
    B --> B3[简单高效]

    C --> C1[新生代复制]
    C --> C2[老年代标记-整理]

    D --> D1[连续内存]
    D --> D2[无碎片]

    E --> E1[客户端应用]
    E --> E2[嵌入式系统]
    E --> E3[单核环境]

    F --> F1[JDK1.3]
    F --> F2[JDK5 默认]
    F --> F3[现代替代]

一、核心设计哲学

1. 单线程 STW 模型

sequenceDiagram
    应用线程->>Serial GC: 内存分配请求
    Serial GC-->>应用线程: 空间不足,触发GC
    Serial GC->>所有线程: Stop-The-World暂停
    Serial GC->>Serial GC: 单线程执行回收
    Serial GC-->>应用线程: 恢复运行

设计优势

  • 零并发开销:无锁、无屏障、无竞争
  • 内存效率:元数据占用最小(<50KB)
  • 确定性:GC 行为完全可预测

二、工作模式详解

1. 新生代回收(Serial Copying)

flowchart TD
    触发[Eden区满] --> STW[暂停所有线程]
    STW --> 标记[标记存活对象]
    标记 --> 复制[复制到Survivor区]
    复制 --> 清空[清空Eden和已处理Survivor]
    清空 --> 恢复[恢复应用线程]

2. 老年代回收(Serial Mark-Compact)

flowchart LR
    触发[老年代空间不足] --> STW[完全暂停]
    STW --> 标记[标记存活对象]
    标记 --> 整理[滑动整理内存]
    整理 --> 清除[清除垃圾]
    清除 --> 恢复

三、内存管理机制

1. 分代结构

graph TD
    Heap[堆内存] --> Young[新生代]
    Heap --> Old[老年代]

    Young --> Eden[Eden区]
    Young --> S0[Survivor0]
    Young --> S1[Survivor1]

    Old --> Tenured[连续空间]

2. 空间分配策略

区域默认比例可调参数
Eden80%-XX:SurvivorRatio=8
Survivor010%不可单独调
Survivor110%不可单独调
老年代自动-XX:NewRatio=2

四、性能特征与局限

1. 优势与局限对比

graph LR
    优势 --> 低开销[内存开销最小]
    优势 --> 简单[实现简单稳定]
    优势 --> 无碎片[老年代完全整理]

    局限 --> 暂停长[STW时间较长]
    局限 --> 吞吐低[单线程效率低]
    局限 --> 不扩展[不适用多核]

2. 性能数据对比

指标Serial GCParallel GCG1 GC
小堆GC暂停50ms30ms10ms
大堆Full GC2s+1s200ms
内存开销1x1.5x3x
吞吐量中高

五、适用场景分析

1. 最佳应用场景

pie
    title Serial GC适用场景
    "客户端应用" : 40
    "嵌入式设备" : 35
    "单核服务器" : 15
    "测试环境" : 10

2. 典型案例

  • 智能家居设备:RAM < 512MB 的物联网设备
  • 桌面应用:Java Swing 客户端程序
  • CI/CD 构建节点:短生命周期的构建任务
  • 云函数:AWS Lambda 等无服务器环境

六、配置与调优

1. 基础参数

# 启用Serial GC
-XX:+UseSerialGC

# 新生代大小
-Xmn256m

# 晋升阈值
-XX:MaxTenuringThreshold=15

2. 高级调优

# 避免过早晋升
-XX:PretenureSizeThreshold=1M

# 调整Eden/Survivor
-XX:SurvivorRatio=10

# 禁用System.gc()
-XX:+DisableExplicitGC

七、现代演进与替代

1. 历史地位

timeline
    title Serial GC发展史
    section JDK1.3
      “ 初始引入 ” : 基础收集器
    section JDK5
      “ Client模式默认 ” : 桌面应用主力
    section JDK8
      “ 被Parallel取代 ” : 多核时代边缘化
    section JDK17
      “ 仍作为备用 ” : 特殊场景使用

2. 现代替代方案

场景Serial GC问题替代方案
多核环境无法利用多核Parallel GC
大堆内存暂停时间过长G1/ZGC
低延迟STW不可控Shenandoah

八、最佳实践

1. 启用建议

# 嵌入式Linux设备
java -XX:+UseSerialGC -Xmx64m -jar iot-app.jar

# 桌面应用优化
java -XX:+UseSerialGC -XX:SurvivorRatio=10 -jar desktop-app.jar

2. 监控诊断

# GC日志分析
-XX:+PrintGCDetails -Xloggc:gc.log

# 实时监控
jstat -gc <pid> 1000

3. 升级时机判断

flowchart TD
    监控[监控系统] --> 暂停{GC暂停>200ms?}
    暂停 -->|是| 升级[升级到Parallel/G1]
    暂停 -->|否| 保持[继续使用Serial]

    业务[业务需求] --> 扩展{需要多线程?}
    扩展 -->|是| 必须升级
    扩展 -->|否| 保持

九、如何解决跨带引用

1. 跨代引用定义

graph LR
    老年代对象 --> 年轻代对象
    年轻代对象 --晋升时--> 老年代对象

核心矛盾

  • 年轻代GC时需确定对象存活
  • 老年代对象可能引用年轻代对象
  • 但老年代不在年轻代GC扫描范围

2. 问题示意图

image.png

3.卡表

Serial 收集器使用了卡表来解决跨带引用扫描的问题,思路是将老年代的区域按照一定大小进行分片,例如分为1024片,用一个长度1024的bit数组表示,如果老年代对象引用了新生代,就将老年代所在的区域的bit位设置为1,也叫变脏。其中会用到一个技术叫写屏障**(GC Barriers)垃圾收集屏障(GC Barriers)** ,这个与内存模型的屏障不是一个东西,我们可以把写屏障理解为修改对象属性时的拦截器,可以在修改之前和之后插入自己的逻辑代码。完整的操作示意图如下:

sequenceDiagram
    应用线程->>对象A: 更新字段引用
    对象A->>写屏障: 拦截写操作
    写屏障->>卡表: 计算卡页地址
    卡表->>卡表项: 标记为脏(0→1)
    卡表项-->>应用线程: 继续执行

4.新生代引用老年代

我们不止要关注老年代引用新生代的问题,还要关注新生代引用老年代,如果触发old gc,需要知道那些新生代对象持有了老年代对象的引用。但是遗憾的是serial、parNew、ps、cms这些传统的分代收集器,对于老年代的回收都需要停顿所有用户线程,然后扫码整个新生代,找到跨代引用。工作流程如下:

sequenceDiagram
    Old GC触发->>准备阶段: Stop-The-World(STW)
    准备阶段->>新生代: 挂起所有应用线程
    准备阶段->>新生代: 禁止新对象分配
    
    Old GC->>新生代: 扫描所有新生代对象
    新生代-->>Old GC: 返回引用关系
    
    Old GC->>老年代: 标记存活对象
    Old GC->>老年代: 清理垃圾
    
    Old GC->>所有线程: 恢复运行

十、技术总结

Serial 收集器作为 JVM 垃圾回收的基石

  • 在资源受限环境下仍是首选
  • 提供最简 GC 实现参考
  • 在确定性要求高的场景不可替代
  • 现代 JDK 中作为故障恢复的"安全网"

最终建议:在 IoT、客户端应用等场景,Serial GC 的内存效率优势明显。但当堆超过 200MB 或需要低延迟时,应优先考虑 Parallel 或 G1 收集器。在 JDK17+ 环境中,对于内存敏感型应用,Serial GC 仍是经过时间验证的可靠选择。