한국어English日本語简体中文繁體中文DeutschไทยTiếng ViệtРусскийPortuguês (Brasil)EspañolBahasa Indonesia

游戏卡顿白皮书 › L10 内存

服务器 GC 全局停顿 Stop-the-world GC pause

原因 ID mem-gc · 主责 研发团队·服务器开发 · 配合 运维团队·系统运维

在含图示和实验的完整版中打开此卡片 →

Java、C# 服务器为回收垃圾对象而暂停所有线程(stop-the-world),这段时间里整个服务器都停住。

起因 堆满,触发 GC → 结果 暂停所有游戏线程后回收(存活数据越多,停得越久) → 画面表现 这台服务器上的所有人同时卡住,随后快进

症状
卡住, 快进
因素
停顿
谁会遇到
全服
何时出现
固定周期, 开得越久越严重
负责方
主责 研发团队·服务器开发 · 配合 运维团队·系统运维
研发团队要做的事
用启动参数显式指定停顿短的 GC(ZGC、Shenandoah、调低停顿目标的 G1),减少分配,调整堆大小。
运维团队要做的事
选用内存充足、能给堆留足空间的实例;容器至少分配 2 个 CPU、约 1.8 GB 内存(JDK 26 及以下版本低于这个配置时会默认选用 Serial GC);监控 GC 停顿时间。
数值参考
只回收新对象(新生代)的 Minor GC 需要几到几十 ms。回收整个堆、存活数据达数 GB 的 Full GC 有时会超过 1 秒。ZGC 的停顿几乎与堆大小无关,不到 1 ms;Shenandoah 的停顿也不随堆大小成比例增长,同样很短。
监控图上
周期性尖峰 · 服务器 tick 耗时、GC 停顿时间
查看位置
开启 GC 日志,把停顿的时刻和时长叠加到服务器 tick 耗时曲线上对照。Java 看启动参数 -Xlog:gc*(JDK 8 及以下用 -XX:+PrintGCDetails)输出的 Pause 行,.NET 看 dotnet-counters 的 GC 停顿指标(.NET 9 起为 dotnet.gc.pause.time,8 及以下为 % Time in GC since last GC),Go 看 GODEBUG=gctrace=1 每次 GC 输出的那一行
确认依据
tick 耗时冲高的时刻与 GC 停顿重合,停顿时长与冲高持续时间相近。服务器上所有场景、分线在同一时刻冲高
排除依据
GC 日志里没有长停顿而 tick 仍冲高,则是锁、同步调用、磁盘写入等其他原因。只有一个场景冲高,看“脚本引擎 GC 停顿”(mem-script-gc)或这个场景的负载
确认手段
运维工具即可确认(无需游戏代码)
深入了解
Java G1 的单次停顿目标默认是 200 ms,在 20 tick 服务器上相当于 4 个 tick。给容器分配的 CPU 少于 2 个或内存少于约 1.8 GB 时,JDK 26 及以下版本的 Java 会默认选用单线程回收的 Serial GC,停顿会长得多。C#(.NET)服务器通常会开启服务器 GC 和后台 GC,但存放新对象的第 0、1 代(Gen0/1)回收,以及伴随压缩的 Full GC,仍会暂停所有线程。Go 的停顿通常不到 1 ms,但分配量大时,申请内存的一方必须分担 GC 工作,tick 会因此变慢。无论哪种方式,只要分配快于回收,游戏线程最终都会停住:G1 会退化为 Full GC,ZGC 会让申请内存的线程一直停到回收结束。
真实案例
Riot Games 2021: League of Legends EUW 5 小时故障:一个辅助 DB 导致全服停摆

出处

  1. Garbage-First (G1) Garbage Collector Oracle
    G1 停顿目标默认值 200 ms(MaxGCPauseMillis);回收过程中内存耗尽时,会退化为暂停所有线程、压缩整个堆的 Full GC
  2. JEP 523: Make G1 the Default Garbage Collector in All Environments OpenJDK
    此前只有 1 个 CPU 或内存不足 1792 MB 时默认选用 Serial GC;从 JDK 27 起,任何环境下默认都是 G1
  3. JEP 439: Generational ZGC OpenJDK
    ZGC 停顿不超过 1 ms,且与堆大小无关;G1 停顿为几 ms 到几秒。分配快于回收时,有分配停顿(allocation stall)的风险
  4. JEP 189: Shenandoah: A Low-Pause-Time Garbage Collector (Experimental) OpenJDK
    Shenandoah 的停顿时间无论堆是 200 MB 还是 200 GB 都差不多
  5. Background garbage collection .NET
    后台 GC 只用于第 2 代回收;第 0、1 代回收(前台 GC)会暂停所有托管线程
  6. A Guide to the Go Garbage Collector Go
    Go GC 大部分是并发执行的,只有短暂的全局停顿;分配多时 goroutine 要分担 GC 工作(assist),带来延迟
  7. JEP 271: Unified GC Logging OpenJDK
    从 JDK 9 起 GC 日志改用统一日志(-Xlog)重新实现;-Xlog:gc 和旧的 -XX:+PrintGC 一样,每次 GC 输出一行
  8. The java Command Oracle
    旧 GC 日志参数与 -Xlog 的对照表:-XX:+PrintGCDetails 对应 -Xlog:gc*
  9. dotnet-counters diagnostic tool .NET
    .NET 9 起由 System.Runtime 的 Meter 指标显示(dotnet.gc.pause.time 等),.NET 8 及以下由旧的 EventCounter 显示(% Time in GC since last GC 等)
  10. runtime package Go
    GODEBUG=gctrace=1:每次 GC 输出一行,包含各阶段的墙上时钟时间、GC 开始和结束时的堆大小以及目标堆大小

相关原因

同一层:L10 内存

其他层中同样导致“卡住”的原因

查看含图示和实验的原卡片