吞吐量本质是用户线程执行业务代码时间占“用户执行时间+gc时间”总和的百分比,计算公式为:吞吐量 = 用户线程执行时间 /(用户线程执行时间 + gc时间)× 100%;它不包含i/o等待、锁竞争等非cpu执行时间,高吞吐取决于gc总耗时占比低而非次数少。

吞吐量在 JVM 垃圾回收中,本质是用户线程“真正干活”的时间占比。它不看 GC 多快或多慢,只看一段时间内,程序花在业务逻辑上的时间占总运行时间的多少。比如 100 秒里,用户代码跑了 98 秒,GC 占了 2 秒,吞吐量就是 98%。
吞吐量怎么算?关键不是绝对时间
公式很直接:吞吐量 = 用户线程执行时间 /(用户线程执行时间 + GC 时间) × 100%。注意两点:
- 分母不是“系统总耗时”,而是“用户执行 + GC 执行”这两块实际占用 CPU 的时间之和;后台线程空转、I/O 等待、锁竞争等不计入分母
- 高吞吐量 ≠ GC 次数少,而是 GC 总耗时占比低。一次大停顿可能拉低吞吐,多次小停顿如果总时间短,照样能维持高吞吐
哪些因素会悄悄拉低吞吐量?
吞吐量下降往往不是因为 GC 算法本身差,而是运行环境或配置放大了 GC 开销:
- 堆内存长期处于高位:每次 GC 都要扫描大量对象,标记、清理、复制耗时增加,GC 线程占用 CPU 时间变长
- 频繁 Minor GC 后对象过早晋升到老年代:导致老年代快速填满,触发代价更高的 Full GC,单次耗时飙升
- Survivor 区过小或分配担保失败多:Eden 区对象无法顺利复制到 Survivor,直接进老年代,加速老年代压力
- GC 线程数与 CPU 核心不匹配:Parallel GC 开太多线程反而引发上下文切换开销;开太少又不能充分利用多核
想稳住高吞吐,得盯住这几个参数
以 Parallel Scavenge + Parallel Old(JDK 8 默认组合)为例,它专为吞吐量设计,但需要主动调优:
- -XX:GCTimeRatio=N:直接设定目标吞吐量比例。例如设为 19,表示允许 GC 时间最多占总时间的 1/(1+19) = 5%,即吞吐量目标 ≥95%
- -XX:MaxGCPauseMillis=N:这是个启发式目标,不是硬性上限。设得太小会让 GC 更频繁、更激进地回收,反而可能降低吞吐;设得太大则失去调控意义
- -Xms 与 -Xmx 设为相等:避免运行中堆扩容带来的额外开销和潜在 Full GC
- -XX:NewRatio 和 -XX:SurvivorRatio:合理划分新生代/老年代、Eden/Survivor 比例,减少对象过早晋升
吞吐量和停顿时间真不能兼得吗?
不是绝对不能,而是有明确取舍边界。Parallel GC 把“尽量少 GC”作为前提来保吞吐,结果是单次 GC 时间变长;G1 或 ZGC 则用并发标记、增量回收等方式把大工作拆成小片,换来了更可控的停顿,但整体 GC 时间通常更长——吞吐量自然让步。选哪个,取决于你的服务类型:
- 批处理、离线计算、定时任务类应用,用户不感知延迟,优先压 -XX:GCTimeRatio
- API 网关、订单支付、实时风控等,哪怕 200ms 停顿也会超时,就得接受一定吞吐损耗,转向 G1 或 Shenandoah











