ParNew:多线程年轻代收集器
Serial 是单线程的,但如果你的服务器有 8 核、16 核呢?
显然,一个 GC 线程无法充分利用 CPU 的全部能力。ParNew 就是来解决这个问题的。
一、ParNew 是什么?
ParNew 是 Serial 的多线程版本,专门收集年轻代。
Serial vs ParNew:
┌─────────────────────────────────────────────────────────────┐
│ │
│ Serial(单线程) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ GC 线程 ──→ [标记] ──→ [复制] ──→ [清除] │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ParNew(多线程) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ GC 线程1 ──→ [标记区域1] ──→ [复制] │ │
│ │ GC 线程2 ──→ [标记区域2] ──→ [复制] │ │
│ │ GC 线程3 ──→ [标记区域3] ──→ [复制] │ │
│ │ GC 线程4 ──→ [标记区域4] ──→ [复制] │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 多个 GC 线程并行工作,缩短 Stop The World 时间 │
│ │
└─────────────────────────────────────────────────────────────┘1.1 核心特点
- 多线程并行:使用多个 GC 线程同时工作
- Stop The World:GC 期间所有用户线程暂停
- 复制算法:年轻代使用复制算法
- CMS 专用:JDK 8 及之前,ParNew 是 CMS 的默认年轻代收集器
二、JVM 参数
# 启用 ParNew + CMS 组合
-XX:+UseConcMarkSweepGC
# 指定 ParNew 的线程数
-XX:ParallelGCThreads=4
# 或者让 JVM 根据 CPU 核心数自动选择
-XX:ParallelGCThreads=0 # 0 表示自动线程数计算规则(JDK 11+):
- CPU 核心数 <= 8:线程数 = CPU 核心数
- CPU 核心数 > 8:线程数 = 8 + (CPU核心数 - 8) * 5/8
三、与 Serial 的对比
| 特性 | Serial | ParNew |
|---|---|---|
| 线程数 | 1 | 多 |
| 停顿时间 | 长 | 短(并行缩短) |
| 吞吐量(多核) | 低 | 高 |
| 吞吐量(单核) | 高 | 低 |
| CPU 开销 | 低 | 高(线程切换) |
| CMS 配合 | 不支持 | 支持 |
四、CMS 的黄金搭档
JDK 8 及之前,ParNew 是 CMS 收集器的默认年轻代收集器。
ParNew + CMS 组合:
┌─────────────────────────────────────────────────────────────┐
│ │
│ 年轻代 老年代 │
│ ┌─────────┐ ┌─────────┐ │
│ │ ParNew │ ── Minor ──→│ CMS │ │
│ │(复制算法)│ │(标记-清除)│ │
│ └─────────┘ └─────────┘ │
│ │
│ ParNew 收集年轻代,CMS 并发收集老年代 │
│ 两者配合,实现"低停顿"的垃圾收集 │
│ │
└─────────────────────────────────────────────────────────────┘为什么是 ParNew 而不是其他年轻代收集器?
因为 CMS 的老年代并发标记需要年轻代提供支持。CMS 使用 card table 记录跨代引用,ParNew 与 CMS 的 card table 格式兼容。
五、为什么 JDK 9 后被移除?
这是一个重要的变化:
- JDK 9 移除了 ParNew + CMS 组合
- CMS 收集器被标记为
@Deprecated - G1 成为默认收集器
5.1 被移除的原因
- CMS 的问题:标记-清除产生内存碎片,长期使用后导致 Full GC
- G1 的崛起:G1 解决了碎片问题,且停顿可控
- 维护成本:ParNew 和 CMS 的组合代码复杂,难以维护
5.2 如何迁移
# JDK 8
-XX:+UseConcMarkSweepGC # ParNew + CMS
# JDK 9+ 推荐
-XX:+UseG1GC # G1,兼容性好
# 或
-XX:+UseZGC # ZGC,超低延迟六、性能调优
6.1 调整线程数
线程数直接影响 GC 停顿时间:
# 如果 GC 停顿时间过长,增加线程数
-XX:ParallelGCThreads=8
# 如果 CPU 使用率过高,减少线程数
-XX:ParallelGCThreads=46.2 与 CMS 的配合调优
# CMS 老年代空间占比达到多少时触发
-XX:CMSInitiatingOccupancyFraction=68
# 允许 CMS 在回收期间并行
-XX:+UseCMSInitiatingOccupancyOnly6.3 常见问题
问题 1:ParNew GC 时间过长
可能原因:
- 年轻代太大
- Survivor 区太小,对象频繁晋升老年代
- 线程数不足
解决方案:
# 减小年轻代
-Xmn256m
# 增大 Survivor 区
-XX:SurvivorRatio=4问题 2:ParNew 频繁触发
可能原因:
- 对象创建速度太快
- Survivor 区太小
解决方案:
# 增大年轻代
-Xmn512m
# 减小对象大小(优化代码)七、面试高频问题
问题 1:ParNew 和 Serial 的区别?
ParNew 是多线程版本,Serial 是单线程版本。在多核环境下,ParNew 的停顿时间更短。但如果是单核环境,Serial 的吞吐量可能更高。
问题 2:ParNew 线程数如何确定?
默认情况下:
- CPU 核心数 <= 8:线程数 = CPU 核心数
- CPU 核心数 > 8:线程数 = 8 + (核心数 - 8) * 5/8
可以通过 -XX:ParallelGCThreads 手动设置。
问题 3:ParNew 适合什么场景?
JDK 8 及之前:
- 需要 CMS 配合的低停顿场景
- 多核服务器
JDK 9+:
- 不再推荐使用,应迁移到 G1 或 ZGC
问题 4:CMS 为什么要用 ParNew?
CMS 的老年代并发标记依赖年轻代的 card table 机制。ParNew 与 CMS 的 card table 格式兼容,是 CMS 的"官方搭档"。
留给你的问题
ParNew 教会我们一个重要的原则:并行化可以缩短停顿时间,但也有开销。
你有没有想过:既然 ParNew 可以多线程工作,那是不是线程越多越好?
答案是:不一定。
原因:
- 线程切换开销:太多线程会导致上下文切换,降低效率
- 内存开销:每个线程都需要栈空间和局部缓存
- CPU 竞争:用户线程和 GC 线程竞争 CPU 资源
所以 ParNew 的默认线程数是根据 CPU 核心数和经验公式得出的,是一个平衡点。
如果你在 JDK 8 上使用 ParNew + CMS,建议尽快迁移到 G1 或 ZGC。因为 JDK 9 之后,CMS 和 ParNew 都已被标记为废弃。
下一节,我们来聊聊 Parallel Scavenge 和 Parallel Old——吞吐量优先的收集器。
