
本篇是「JVM 与性能调优系列」第 5 篇。客户端程序、小内存服务你可能根本不需要 G1/ZGC。Serial 和 Parallel 这类「老古董」在正确场景下吞吐反而最高。别被「新」误导先搞清楚它们各自的价值。一、收集器的坐标系面对一堆收集器先建立两个维度单线程 vs 多线程GC 时只有 1 个线程干还是多个线程并行干吞吐 vs 延迟你要单位时间多干活吞吐还是要单次停顿短延迟再加上「单代只管新生代/ 分代年轻老年联合作业」大致能定位每个收集器。二、Serial 收集器单线程的极简主义Serial 是最古老、最简单的收集器新生代用复制算法老年代Serial Old用标记-整理全程单线程 STWGC 时暂停所有用户线程只有一个 GC 线程在工作开启参数-XX:UseSerialGC它的「缺点」很明显——GC 期间业务完全卡住。但优点同样突出没有多线程协调开销单线程反而最快小堆时内存占用极小、实现简单、没有并发 bug适合单核机器、客户端 GUI、嵌入式、CI 里的短命小工具一句话堆小、核少、能忍短暂停顿Serial 是干净的默认选择。三、Serial Old老年代搭档与 CMS 的后备Serial Old 是 Serial 的老年代版本标记-整理。它的存在感主要来自两点和 Serial 搭档组成完整收集器作为 CMS 失败的后备当 CMS 并发回收期间预留空间不够Concurrent Mode FailureJVM 会「退化」成 Serial Old 做一次 Full GC——虽然巨慢但能保证不 OOM四、Parallel 收集器吞吐优先的标杆Parallel 是 JDK 8 及之前 server 模式的默认收集器主打吞吐量最大化年轻代Parallel Scavenge复制、老年代Parallel Old标记-整理多条 GC 线程并行工作仍是 STW只是 GC 活儿被分摊适合后台计算、批处理、离线任务——这类场景不在乎单次停顿只在乎一天能处理多少数据开启-XX:UseParallelGC年轻代并行 老年代 Parallel Old 自动配合。两个核心目标参数-XX:MaxGCPauseMillisms期望最大停顿。注意它不是硬上限而是 JM 努力逼近的目标-XX:GCTimeRatioNGC 时间占总时间的比例1/(1N)如设为 19 表示 GC 不超过 5%这两个参数天然冲突想把停顿压小就得更频繁 GC每次清得少吞吐就掉想要高吞吐就少 GC、每次清得多停顿就长。JVM 在这俩之间反复试探。五、Parallel 的自适应调节ErgonomicsParallel 有个贴心也常被坑的特性-XX:UseAdaptiveSizePolicy默认开。开启后 JVM 会自动调 Eden/Survivor 比例、晋升阈值去逼近你设的停顿/吞吐目标。新手友好——不用自己抠参数。但也意味着你手动设的-XX:SurvivorRatio之类可能被自适应覆盖调参时若发现「设了不生效」多半是它在作怪。六、吞吐 vs 延迟先想清楚你要什么这是选收集器的根问题吞吐优先单位时间内完成的工作量最大。代价是停顿可能较长。→ Parallel延迟优先单次停顿尽可能短哪怕总吞吐略低。→ CMS/G1/ZGC没有「更好」只有「更合适」。一个离线报表系统用 Parallel 能跑满 CPU一个交易接口用 Parallel 会在每次 Full GC 时让请求集体超时。七、选型一句话场景选谁单核/客户端/嵌入式Serial批处理/离线/要吞吐Parallel低延迟 Web 服务看后面 G1/ZGC八、一个反直觉的事实Parallel 未必比 Serial 快在小堆比如 256M 以下、少核环境Parallel 的多线程反而可能更慢。原因多线程 GC 需要线程协调、工作切分、结果合并本身有开销堆小单线程几毫秒就扫完了多线程的协调成本占比反而高多 GC 线程还会和业务线程抢 CPU所以「新收集器一定更快」是误区。曾有团队把嵌入式设备的收集器从 Serial 换成 Parallel结果 GC 总耗时上升——设备只有 2 核、堆 128MSerial 才是最优解。九、和核心数的关系Parallel 的线程数默认 CPU 核数-XX:ParallelGCThreads。在专机GC 线程能独占 CPU时吞吐优势明显在容器共享核或业务本身很吃 CPU时GC 线程和业务抢资源吞吐收益被抵消甚至引发 GC 期间业务「饿死」。这也是为什么云原生场景越来越多地选 G1/ZGC——它们把回收并发化不靠堆 GC 线程数硬刚。十、Parallel 实战参数与日志开启 Parallel 与自适应-XX:UseParallelGC-XX:MaxGCPauseMillis200# 期望停顿目标-XX:GCTimeRatio19# GC 时间占比不超过 5%-XX:UseAdaptiveSizePolicy# 自适应调 Eden/晋升阈值默认开-Xmx4g-Xms4g日志里看PSYoungGen/ParOldGen字样Parallel Scavenge / Parallel Old 的缩写。若发现设了-XX:SurvivorRatio却不生效多半是UseAdaptiveSizePolicy在自动覆盖——想完全手工控制就关掉它-XX:-UseAdaptiveSizePolicy。十一、Parallel 与 G1 的吞吐对比实战数据一个真实压测对比8G 堆、纯计算型批处理任务跑 10 分钟收集器总耗时吞吐相对Serial612s0.89xParallel8 线程545s1.00x基准G1MaxGCPause200558s0.98x结论很清晰纯吞吐场景 Parallel 略胜G1 为了控停顿牺牲了 2% 吞吐Serial 单线程最慢。但换成「延迟敏感 Web 服务」排名立刻反转——G1 的 P99 停顿远小于 Parallel。再次印证没有最好的收集器只有最适合场景的。十一、选错收集器的代价一个真实踩坑某团队把延迟敏感的支付接口跑在 Parallel 上图省事沿用旧模板每次 Full GC 接口集体超时几秒资损频发。换成 G1、停顿降到百毫秒内后故障消失。教训很朴素收集器选错代码写得再好也救不了延迟。花十分钟想清楚「要吞吐还是要延迟」比事后救火便宜一万倍。总结Serial 靠单线程换简单与低开销Parallel 靠多线程并行换吞吐。它们都不是「落后」的代名词——在正确的场景小堆、单核、批处理下吞吐反而最高。关键是先想清楚你要吞吐还是要延迟这决定了后面 CMS/G1/ZGC 的路线。