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. 空间分配策略
| 区域 | 默认比例 | 可调参数 |
|---|---|---|
| Eden | 80% | -XX:SurvivorRatio=8 |
| Survivor0 | 10% | 不可单独调 |
| Survivor1 | 10% | 不可单独调 |
| 老年代 | 自动 | -XX:NewRatio=2 |
四、性能特征与局限
1. 优势与局限对比
graph LR
优势 --> 低开销[内存开销最小]
优势 --> 简单[实现简单稳定]
优势 --> 无碎片[老年代完全整理]
局限 --> 暂停长[STW时间较长]
局限 --> 吞吐低[单线程效率低]
局限 --> 不扩展[不适用多核]
2. 性能数据对比
| 指标 | Serial GC | Parallel GC | G1 GC |
|---|---|---|---|
| 小堆GC暂停 | 50ms | 30ms | 10ms |
| 大堆Full GC | 2s+ | 1s | 200ms |
| 内存开销 | 1x | 1.5x | 3x |
| 吞吐量 | 低 | 高 | 中高 |
五、适用场景分析
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. 问题示意图
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 仍是经过时间验证的可靠选择。