虽然Parallel Scavenge 在 GC 期间会触发 STW(Stop-The-World) 并暂停所有应用线程,表面上看确实可以将 GC 线程数设置为全部核心数 来尽可能加快垃圾收集速度,但以下几点原因解释了为什么默认情况下将 GC 工作线程数限制为 CPU 核心数的 1/4,而不是全部核心数:
1. 减少并行线程间的开销
- 当 GC 工作线程数增加时,线程之间需要频繁地同步和协调(例如任务分配和处理进度)。特别是在某些垃圾回收阶段(如根扫描和对象复制),线程之间的同步开销会随着线程数增加而显著增加。
- 如果使用全部核心,会导致线程之间的竞争加剧,反而可能增加线程间的通信和管理开销,从而降低 GC 的整体效率。
示例: 当线程数过多时,可能会在 GC 的部分阶段中,出现因为协调而浪费 CPU 时间的情况,导致性能提升不如预期。
2. 为系统线程和操作系统任务保留资源
即使在 GC 的 STW 阶段,JVM 仍需要依赖操作系统的基础服务(如 I/O 操作、网络通信等),这些服务需要占用系统的 CPU 资源。
如果 GC 工作线程数等于 CPU 核心数,操作系统的其他线程可能会因为 CPU 饱和而延迟调度,影响 JVM 和整个系统的稳定性。尤其是在多实例部署或共享资源的环境中,保留部分资源对于平衡系统的整体性能尤为重要。
3. 高并行度的边际收益递减
随着 GC 线程数的增加,每增加一个线程带来的性能提升是有限的(存在边际收益递减现象)。具体原因包括:
- 垃圾回收任务可能无法无限制地切分为更多的小任务。
- 线程间的资源竞争和调度开销增加。
因此,在一定的线程数量之后,继续增加 GC 线程数对停顿时间的减少效果会越来越小。
默认 1/4 的设计是一种经验值,通常可以在资源利用和 GC 性能之间取得平衡。
4. 避免线程切换开销
- 在多核系统中,如果线程数大于 CPU 核心数,就会触发线程上下文切换。线程切换会增加额外的 CPU 开销,影响整体的垃圾回收效率。
- 将 GC 线程数设置为 1/4 核心数,可以降低线程切换的开销,同时仍然能够充分利用并行计算的优势。
5. 动态调整 GC 线程数
JVM 提供了一些参数,允许根据应用场景调整 GC 线程数:
-XX:ParallelGCThreads=<n>
:设置 GC 线程数。开发者可以根据场景需求(如高吞吐量或低停顿时间)调整这个值。- 默认值(1/4 核心数)是一个适合大多数场景的平衡点,但不是最佳值,具体配置仍需根据性能测试结果来优化。
6. 停顿时间与吞吐量的权衡
Parallel Scavenge 的设计目标是 高吞吐量,而不是 最短停顿时间。
- 高吞吐量意味着减少 GC 对应用整体性能的影响,因此需要让应用线程有更多的 CPU 时间运行,而不是完全追求最短的停顿时间。
- 如果需要尽量减少停顿时间,可以考虑 G1 或 CMS 垃圾收集器,这些收集器可以以不同的方式降低停顿时间。
总结
- Parallel Scavenge 默认将 GC 线程数设置为 CPU 核心数的 1/4,目的是在 STW 阶段和系统资源占用之间找到一个平衡点。
- 虽然增加 GC 线程数可能缩短停顿时间,但线程间的同步开销、系统任务资源需求、边际收益递减等问题,都会限制线程数的实际收益。
- 如果对停顿时间要求极高,建议调整
-XX:ParallelGCThreads
,或者根据业务需求选择更合适的垃圾收集器(如 G1 或 ZGC)。