Skip to content

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 参数

bash
# 启用 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 的对比

特性SerialParNew
线程数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 被移除的原因

  1. CMS 的问题:标记-清除产生内存碎片,长期使用后导致 Full GC
  2. G1 的崛起:G1 解决了碎片问题,且停顿可控
  3. 维护成本:ParNew 和 CMS 的组合代码复杂,难以维护

5.2 如何迁移

bash
# JDK 8
-XX:+UseConcMarkSweepGC  # ParNew + CMS

# JDK 9+ 推荐
-XX:+UseG1GC  # G1,兼容性好
# 或
-XX:+UseZGC   # ZGC,超低延迟

六、性能调优

6.1 调整线程数

线程数直接影响 GC 停顿时间:

bash
# 如果 GC 停顿时间过长,增加线程数
-XX:ParallelGCThreads=8

# 如果 CPU 使用率过高,减少线程数
-XX:ParallelGCThreads=4

6.2 与 CMS 的配合调优

bash
# CMS 老年代空间占比达到多少时触发
-XX:CMSInitiatingOccupancyFraction=68

# 允许 CMS 在回收期间并行
-XX:+UseCMSInitiatingOccupancyOnly

6.3 常见问题

问题 1:ParNew GC 时间过长

可能原因:

  • 年轻代太大
  • Survivor 区太小,对象频繁晋升老年代
  • 线程数不足

解决方案:

bash
# 减小年轻代
-Xmn256m

# 增大 Survivor 区
-XX:SurvivorRatio=4

问题 2:ParNew 频繁触发

可能原因:

  • 对象创建速度太快
  • Survivor 区太小

解决方案:

bash
# 增大年轻代
-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 可以多线程工作,那是不是线程越多越好?

答案是:不一定

原因:

  1. 线程切换开销:太多线程会导致上下文切换,降低效率
  2. 内存开销:每个线程都需要栈空间和局部缓存
  3. CPU 竞争:用户线程和 GC 线程竞争 CPU 资源

所以 ParNew 的默认线程数是根据 CPU 核心数和经验公式得出的,是一个平衡点。

如果你在 JDK 8 上使用 ParNew + CMS,建议尽快迁移到 G1 或 ZGC。因为 JDK 9 之后,CMS 和 ParNew 都已被标记为废弃。

下一节,我们来聊聊 Parallel Scavenge 和 Parallel Old——吞吐量优先的收集器。

基于 VitePress 构建