这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 Serial 和 Parallel 是经典分代收集器。Serial 适合小内存和单核场景,Parallel 追求吞吐量,适合批处理和对停顿不敏感的任务。理解它们有助于建立 GC 的基本模型。
15.1 Serial
Young: Serial Copying
Old: Serial Mark-Sweep-Compact
启用:
java -XX:+UseSerialGC -jar app.jar
特点:
| 优点 | 缺点 |
|---|---|
| 实现简单 | 单线程 |
| 内存开销小 | 停顿随堆增大变长 |
| 小内存表现稳定 | 不适合大堆交互服务 |
| 客户端场景经典 | 多核利用不足 |
适用:
- 小容器;
- 低内存客户端;
- 单核环境;
- 简单任务;
- 极低内存开销优先。
15.2 Parallel
Young: Parallel Scavenge
Old: Parallel Old
启用:
java -XX:+UseParallelGC -jar app.jar
特点:
| 优点 | 缺点 |
|---|---|
| 多线程标记复制 | 停顿可能较长 |
| 吞吐量高 | 停顿目标不如 G1 / ZGC |
| 适合批处理 | 并发能力有限 |
| 资源集中执行 GC | 交互服务长尾风险 |
吞吐目标参数:
-XX:GCTimeRatio=19
-XX:MaxGCPauseMillis=200
-XX:ParallelGCThreads=4
GCTimeRatio=19 表示目标 GC 时间约 1/(1+19)=5%。MaxGCPauseMillis 是软目标,不是硬保证。
15.3 分代布局
Heap
|-- New Generation
| |-- Eden
| |-- Survivor 0
| +-- Survivor 1
+-- Old Generation
参数:
-Xmn512m
-XX:NewRatio=2
-XX:SurvivorRatio=8
-XX:MaxTenuringThreshold=15
示例:
NewRatio=2
New : Old = 1 : 2
SurvivorRatio=8
Eden : one survivor : other survivor = 8 : 1 : 1
15.4 Minor GC 触发
Eden 分配失败
-> Minor GC
-> 存活对象复制到 Survivor
-> 年龄增加
-> 达阈值或放不下晋升 Old
Full GC 常见触发原因:
- 老年代空间不足;
- 元空间不足;
- 晋升失败;
- GC 实现特定策略;
- 诊断命令触发。
不建议在业务代码中显式请求 Full GC。
15.5 停顿模型
停顿与以下因素相关:
- 堆大小;
- 存活对象数量;
- 引用密度;
- 卡表和根扫描;
- 线程数;
- CPU 资源;
- 内存带宽;
- 压缩范围。
大堆问题:
10G 堆,Old 中 6G 存活
-> Full GC 需要标记和压缩大量对象
-> 停顿可能达到秒级
15.6 GC 日志
Java 9 及以上统一日志:
java -Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=5,filesize=20m \
-XX:+UseParallelGC \
-jar app.jar
Java 8:
java -XX:+PrintGCDetails \
-XX:+PrintGCDateStamps \
-Xloggc:gc.log \
-jar app.jar
关键信息:
| 信息 | 说明 |
|---|---|
| GC 前后堆 | 回收量 |
| 年轻代变化 | Minor GC |
| 老年代变化 | 晋升 |
| user/sys/real | CPU 与墙钟时间 |
| worker 数 | 并行线程 |
| 停顿原因 | 触发条件 |
15.7 调优思路
吞吐优先调优:
1. 控制分配速率
2. 增大新生代,降低 Minor GC 频率
3. 避免过早晋升
4. 保证 Old 有余量
5. 合理设置并行线程
6. 用真实数据压测
风险:
| 操作 | 可能问题 |
|---|---|
| 过大新生代 | 单次 Minor GC 变长 |
| 过小新生代 | 频繁 GC 和晋升 |
| Old 太小 | Full GC |
| Old 太大 | Full GC 停顿长 |
| 线程过多 | CPU 竞争 |
15.8 与 G1 选择
| 场景 | 推荐 |
|---|---|
| 批处理、吞吐优先 | Parallel |
| 大堆交互服务 | G1 / ZGC |
| 小内存简单任务 | Serial |
| 需要可预测停顿 | G1 / ZGC |
| 现代微服务常见选择 | G1 起步 |
选择流程:
明确 SLA
-> 测量当前停顿和吞吐
-> 先优化分配和缓存
-> 小范围调参
-> 对比 GC 日志
-> 再评估切换 GC
15.9 生产案例
案例一:夜间批任务 Full GC
现象:批处理期间吞吐下降,Full GC 停顿 3 秒。
jstat -gcutil <pid> 1000
发现:大量对象直接晋升。
处理:
- 拆小批次;
- 流式读取;
- 复用缓冲;
- 增大新生代;
- 评估 Parallel 目标;
- 压测验证。
案例二:小容器停顿抖动
现象:256Mi 容器,交互服务频繁 GC。
方向:
- 降低内存分配;
- 评估 G1 是否更适合;
- 限制并发;
- 扩容;
- 优化缓存;
- 控制堆外内存。
本章小结
Serial 简单低开销,适合小内存;Parallel 多线程吞吐优先,适合批处理。两者都是经典分代模型,停顿会受堆大小、存活对象和 CPU 影响。调优先控制分配和晋升,再调堆比例,最后才考虑切换 GC。
思考题
- Parallel 的目标是什么?
- NewRatio 和 SurvivorRatio 如何计算?
- 为什么大堆 Full GC 停顿更长?
- 什么场景仍适合 Serial?
- 如何比较 Parallel 和 G1 的压测结果?