原文出处:Aikar(Daniel Ennis),JVM Tuning: Optimized G1GC for Minecraft,发表于 aikar.co,长期运营 Empire Minecraft 生存服的实战沉淀。本文对其核心思想做系统化梳理与解读,并标注引用来源。


一、问题从哪来:为什么默认 JVM 参数跑 MC 会卡?

Minecraft 服务端是一个极其特殊的 Java workload:

  • 分配速率惊人——一个 30 人左右的服务器,内存分配速率可达 至少 800MB/s,且绝大多数对象是极短命的(如 BlockPosition这类临时对象)

  • 默认 G1GC 的 New Generation 太小(默认只给堆的 ~5%),导致 Young GC 触发过频,对象被过早晋升(promote)到 Old Gen

  • Old Gen 一旦涨起来就会触发 Full GC / Mixed GC 的大暂停,玩家感受到的就是那次经典的 TPS 骤降 → 所有人瞬移一下

Aikar 做的事,本质上就是一句话:

让短命对象在 Young Gen 死掉,别让它们漏进 Old Gen;同时把 GC 暂停压扁、摊平,消除尖峰。


二、推荐启动参数(基准版)

下面就是那套被称为 Aikar's Flags​ 的推荐 JVM 启动参数。只改 Xms/Xmx和你自己的 jar 名,其余原样照搬

java -Xms10G -Xmx10G \
  -XX:+UseG1GC \
  -XX:+ParallelRefProcEnabled \
  -XX:MaxGCPauseMillis=200 \
  -XX:+UnlockExperimentalVMOptions \
  -XX:+DisableExplicitGC \
  -XX:+AlwaysPreTouch \
  -XX:G1NewSizePercent=30 \
  -XX:G1MaxNewSizePercent=40 \
  -XX:G1HeapRegionSize=8M \
  -XX:G1ReservePercent=20 \
  -XX:G1HeapWastePercent=5 \
  -XX:G1MixedGCCountTarget=4 \
  -XX:InitiatingHeapOccupancyPercent=15 \
  -XX:G1MixedGCLiveThresholdPercent=90 \
  -XX:G1RSetUpdatingPauseTimePercent=5 \
  -XX:SurvivorRatio=32 \
  -XX:+PerfDisableSharedMem \
  -XX:MaxTenuringThreshold=1 \
  -Dusing.aikars.flags=https://mcflags.emc.gs \
  -Daikars.new.flags=true \
  -jar paper.jar --nogui

⚠️ 先说最重要的警告:如果你的面板/主机说你有 8000M,请 不要设 Xmx=8000M。Java 在 -Xmx之外还需要额外 native 内存,建议扣掉约 1000–1500M,或者跟主机确认他们是否已帮你兜底。例如 8G 机器通常设 6500M~7000M​ 更安全。


三、逐组参数:它们在「物理层」到底做了什么?

1. -Xms= -Xmx,且为什么必须相等

Aikar 的核心论点:

  • 如果 -Xms < -Xmx,意味着你给了 Java 一个"可以膨胀到上限但实际没预留"的承诺——闲置内存就是浪费内存

  • G1 在有更多堆可用时,会更从容地让短命对象留在 Young 区自然死亡,而不是被迫提前晋升

  • +AlwaysPreTouch配合相等的 Xms/Xmx,让内存在进程启动时就被真正分配并连续化,避免在运行时才缺页申请,从而提升访问效率

2. 把 New Gen 拉大到 30%~40% —— 整套 flags 的灵魂

这是最关键的一组:

-XX:G1NewSizePercent=30
-XX:G1MaxNewSizePercent=40

G1 默认 New Gen 只占 5%​ 堆,而 MC 的分配速率会把 Eden 瞬间打满——Young GC 每秒可能触发 1~2 次以上,不但暂停频繁,还会把大量临时对象冲进 Old Gen。

Aikar 把 New Gen 的下限拉到 30%,上限 40%,等于告诉 G1:"给新生代更多空间,让它慢点满,让那些 BlockPosition 级别的短命对象有机会在 Eden/Survivor 里直接被回收。"

效果:Young GC 间隔变长、每次更有成效,Old Gen 增速显著放缓。

3. MaxTenuringThreshold=1+ SurvivorRatio=32

-XX:MaxTenuringThreshold=1
-XX:SurvivorRatio=32

MC 的绝大多数分配活不过两轮 GC。Aikar 的做法是:

  • 最多让对象在 Survivor 里熬 1 轮,第 2 轮还活着就直接当"较长寿命"处理,进入 Old 区的 Mixed GC 路径去回收——而不是在 Survivor 之间来回拷贝 15 次

  • 既然 Survivor 的使用被压缩了,就用 SurvivorRatio=32把省出来的 region 还给 Eden,让 Eden 更大

这是一个非常"懂 MC 对象生命周期"的设计,不是通用 Java 应用的套路。

4. IHOP=15早启动并发标记 —— 防 Full GC 于未然

-XX:InitiatingHeapOccupancyPercent=15

默认 IHOP 通常在 45% 左右才开始考虑回收 Old Gen。Aikar 把它压到 15%,核心目的:在 Old Gen 还没涨到危险水位之前,就让 G1 开始并发标记周期,用一系列轻量的 Mixed GC 逐步清理,而不是等爆了再全停

配合:

-XX:G1MixedGCLiveThresholdPercent=90
-XX:G1MixedGCCountTarget=4

让 Mixed GC 更积极地回收 Old 中可回收区域,同时保持每次暂停可控。

5. Region Size 锁定为 8M —— 防止 Humongous 对象捣乱

-XX:G1HeapRegionSize=8M

MC 1.15+ 的某些数据结构(尤其是与区块/实体相关的分配路径)会产生接近 4MB 的对象。G1 的默认 Region Size 在较小堆下可能算出 1M~4M,于是 ≥4MB 的分配会被标记为 Humongous,直接进 Old Gen,且极难回收。手动锁到 8M​ 就是堵这个坑。

6. 其余"保护性/稳定性"项速览

参数

作用

+DisableExplicitGC

封杀插件手调 System.gc(),防止烂代码触发 Full GC 引发瞬时卡顿

+AlwaysPreTouch

启动时触达全部预留内存,保证连续性、改善 TLB 与大页效率

+PerfDisableSharedMem

阻止 GC 统计写入共享内存映射文件,避免磁盘 IO 抖动带来 latency 尖刺(尤其容器环境)

G1ReservePercent=20

为 G1 的「to-space」逃生通道多留 10% 余量,降低 to-space exhaustion风险

G1RSetUpdatingPauseTimePercent=5

把 RSet 更新更多推到并发阶段,压减 STW 暂停占比

MaxGCPauseMillis=200

这是 G1 的目标值(不是硬上限),允许每次 collection 最多 ~200ms——低于多数玩家的感知阈值,同时给 G1 足够余地别憋出 Full GC


四、如果你有超过 12G 堆:需要微调的进阶版

Aikar 给出的建议是:≤12G 用基准版不动;>12G 时做如下调整

# 替换基准版中的对应项:
-XX:G1NewSizePercent=40
-XX:G1MaxNewSizePercent=50
-XX:G1HeapRegionSize=16M
-XX:G1ReservePercent=15
-XX:InitiatingHeapOccupancyPercent=20

理由:堆大了,"to-space 不够"的风险降低,可以把更多给 New Gen(40/50),Region Size 升到 16M 减少 Humongous 碎片并加快 Remark,IHOP 延后到 20 因为 Old 绝对值更大了。但他也明确提醒:如果观察到 Old Gen 回收反而变差,回退到基准版


五、如何验证它真的在工作?

① 启用 GC 日志(滚动,几乎零开销)

Java 11+

-Xlog:gc*:logs/gc.log:time,uptime:filecount=5,filesize=1M

Java 8(老环境):

-Xloggc:gc.log -verbose:gc -XX:+PrintGCDetails \
-XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps \
-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=1M

日志只有约 5MB,可以随时拿 gc.log跟 TPS 下跌时间点做关联分析。

② 真正的性能诊断顺序

Aikar 自己也说得很直白(以及 Paper 文档的共识):

GC 调优通常是最后一段路。​ 如果你 TPS 不稳,先看:

  1. /timingsSpark profiler​ → 找出是谁在吃 tick 时间

  2. 实体堆叠(农场/刷怪)、区块加载、红stone、漏斗链、水流推送

  3. 插件逐个二分法排查

  4. 最后才是 GC 参数压榨剩余抖动

Flags 能让"本来就不该卡的"更平滑,但救不了设计层面的卡


六、今日使用须知(写给 2025/2026 的服主)

项目

说明

服务端

这套 flags 是为 Paper / Purpur​ 体系设计的基准,别用在原版或老旧 Spigot 上还指望奇迹

Java 版本

原始文章语境是 Java 8+ / 11+。如果你在 Java 17/21 上跑,部分 experimentalflag 的必要性会变,建议先在本地/测试服验证启动无 warning

别盲目抄数值

只改 Xms/Xmx(留出 OS 余量),别去手调你不理解的百分比;如果要改,先开 GC 日志看证据

内存建议底线

Aikar/Paper 均建议至少 6–10GB​ 范围(视版本/玩法而定),且强调收益递减——32GB 不会让你快 3 倍


七、总结一句话

Aikar's Flags 不是玄学,是一份针对 Minecraft 极端分配模式量身定制的 G1GC 配置档案。​ 它的核心价值在于:把 New Gen 撑大、把晋升压住、把回收做成分段增量、把外因(显式 GC / mmap 写入 / 迟分配)封掉——最终换来的是 TPS 曲线从锯齿变成平缓

如果你愿意,我可以下一步帮你做两件事之一:

  1. 按你的具体环境生成一份安全的 start.sh(告诉我:MC 版本、Paper 还是 Purpur、Java 版本号、java -version输出、面板给的总内存、平均在线人数)

  2. 教你读 GC 日志:把一段 gc.log里的 Full GC / Pause / Humongous 信号翻译成"该怎么调"的结论


引用出处

  • Aikar, JVM Tuning: Optimized G1GC for Minecraft, aikar.co, 2018-07-02 — 原始 flags 发布页与内存警告

  • PaperMC Docs, Aikar's Flags, docs.papermc.io — 官方继承版说明与技术解释(Xms=Xmx 原理、NewGen 为何给大、GC 日志)

  • Aikar, Technical Explanation of the Flags(同文展开段)— 逐项动机:NewSizePercent/IHOP/SurvivorRatio/AlwaysPreTouch/DisableExplicitGC/PerfDisableSharedMem 等

  • mcflags.emc.gs(Aikar 的 flags 镜像页)— to-space exhaustion、MaxTenuringThreshold=1 意图、G1HeapRegionSize 与 Humongous 问题详解

没人敢说自己的人生很轻松。但可以肯定,这就是它的迷人之处。