本篇教程由作者设定使用 CC BY-NC 协议。
引言
本文主要持“知其然,不如知其所以然”的态度,补充了大量内容,篇幅较长,如果你没有时间和/或耐心读完,请读完引言,直接跳转到本文的“通用优化方案”部分。
JVM 调优较为复杂,若配置错误可能导致严重的性能下降,甚至游戏崩溃损坏存档!
本篇教程基于 Java 25 编写,并且使用了在特定 Java 版本(包括 Java 25)中引入的新功能,对于其他的 Java 版本(主要是更低的)很可能不适用。
本篇教程主要针对基于 HotSpot VM 的 JDK(OpenJDK 和 GraalVM),对于如 Azul Prime 或 OpenJ9 等非主线 JDK 发行不完全适用。
本教程仅供参考,请自行在实际游戏环境中测试。
JVM 调优是 Minecraft Java 版(也几乎是所有 Java 程序)优化的最终手段,通过调整 JVM 内部的功能,提高 JVM 应对特定性能压力的能力。在进行 JVM 优化前,请一定记住:①如果你的客户端/服务器本来就运行良好,就不要进行过多的、深层的优化;②深层的 JVM 调优永远只能缓解问题,不能消灭问题,很多时候,垃圾模组和不当游戏行为(如100万个TNT)才是问题的源头!因此,本教程主要提供易于使用和理解的优化方案,而非追求极致优化。
JVM 发行版
要进行 JVM 调优,首先要选一个合适的 JVM。总体上,有四类可选的 JDK:
OpenJDK(HotSpot JVM)
标准的 JVM 实现,Mojang 官方提供的 JVM (属 Microsoft)。通常性能与稳定性均衡,对于一般的客户端和小型服务器已经足够。以下也提供一些第三方 JVM 发行:
Alibaba Dragonwell Extended Edition:保证模组兼容性的同时,移植了更高版本的 OpenJDK 的优化与修复。
Azul Zulu:使用更旧的编译器,牺牲了少量性能,换取了更好的稳定性和系统兼容性。
GraalVM
Oracle 主导的新兴 JVM 实现,其使用的 Graal 编译器能够以牺牲预热时间为代价,换取相比 OpenJDK 10%~20% 的性能提升,尤其适合长时间运行的服务器和安装大量模组的客户端。
GraalVM 有 Oracle 版和社区版,前者通常具有更高的峰值性能,因此更建议使用 Oracle GraalVM。
OpenJ9
Eclipse 和 IBM 主导的、重新设计的 JVM 实现,牺牲了一部分性能,显著降低了内存占用。只建议物理内存极度紧张时试用。(注:OpenJ9 与 Spark 模组/自带 Spark 的插件端不兼容,必须禁用 Spark)
OpenJ9 的主流发行版为 IBM Semeru Runtime,使用 Open Edition 即可,性能与 Certified Edition 无区别。
Azul Prime Runtime (Zing)
Azul 开发的私有商用的、重新设计的 JVM 实现。具有极为特殊的 LLVM 编译器和 C4 GC,设计极为激进和极端化,据 Azul 所言能够兼顾极高性能和无停顿 GC。
但是几乎没有 Minecraft 服务器会使用 Zing,原因有:
只支持 Linux x86_64/aarch64;
自从 Azul 把官网改了,并且包管理器仓库里迟迟不提供 Java 25(目前只有 Java 6~23),非付费客户极其难以下载,需要用企业邮箱申请试用;
预热时间极其长,预热完成前性能低下,且其缓解功能 ReadyNow 经常与模组/插件冲突;
……
综上,如果你真那么有实力,愿意搞到这玩意(或者去找罪受,用这个把服务端降级到 Java 23),还能忍受不兼容和慢启动,那就去 Azul 官网 下载,试着在服务端试一试,客户端绝对没有必要使用 Zing。
JVM 参数
JVM 参数对 JVM 内部功能进行调整,以期提高性能、缓解性能瓶颈。
参数格式与查询
一般来说,JVM 参数有四种格式,不同参数类型不能混用:
-X[参数] # 特殊参数,用于如 -Xmx、-Xms 等
-XX:+[参数名] # 启用参数
-XX:-[参数名] # 禁用参数
-XX:[参数名]=[值] # 设置参数的值
-D[参数名]
-D[参数名]=[值] # 属性参数,用得相对少
OpenJDK 25 的所有可用参数均可在 chriswhocodes.com 上查询。也可以使用以下命令行查询当前 JVM 所有可用参数:
java -XX:+PrintFlagsInitial
管理 JVM 参数
我们当然可以直接设置所有 JVM 参数,比如在服务端上这样设置:
java -Xmx16G -Xms16G -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational -XX:+UseCompactObjectHeaders -XX:+UseStringDeduplication -XX:+AlwaysPreTouch -XX:+UseTransparentHugePages -XX:LargePageSizeInBytes=8M -jar server.jar nogui
但这样未免有点太繁琐了,一不小心就写错了,还有可能忘记之前为什么要设置某些参数。OpenJDK 开发人员又不傻,为我们开发了从文件中读取 JVM 参数的实用功能:
①创建一个文件(比如 Args.txt),在里面填上 JVM 参数:
# 文件名:Args.txt
# 以半角“#”开头的行是注释,不会被读取,注意不能行尾注释
# 每行最多设置一个参数
# 内存
-Xmx16G
-Xms16G
# GC
-XX:+UseShenandoahGC
-XX:ShenandoahGCMode=generational
# 其他参数
-XX:+UseCompactObjectHeaders
-XX:+UseStringDeduplication
-XX:+AlwaysPreTouch
-XX:+UseTransparentHugePages
-XX:LargePageSizeInBytes=4M
②将原先的 JVM 参数/命令行替换为 @[参数文件路径](支持相对路径,可以设置多个),在本例中:
java @D:\Server\Args.txt -jar server.jar nogui
重要误区——多就是好?
网上有很多的 JVM 参数方案,但我们都不推荐直接使用,尤其是那些把三四十个参数放在一起的。
一个很简单的道理:如果一个参数这样设置,在长期使用中被证明有益无害或者利大于弊,那就应该作为默认设置。通常来说,需要手动设置的参数,要么是刚加入 OpenJDK 不久的新功能,为保证稳定性而默认禁用;要么是该参数有利有弊,需要根据实际情况选择性设置的功能(通常 JVM 会自动分析并决定如何设置这些参数,通常也不应手动更改)。
常用参数
此部分列举了(在本教程基于的 OpenJDK / GraalVM 版本中)需要手动设置的 JVM 参数(不包括内存和 GC 参数,详见下一章“内存和 GC”)
推荐参数
这些参数能带来显著的性能提升,同时不需要复杂配置、没有明显的负面影响,在任何情况下都可以使用。
-XX:+UseCompactObjectHeaders
启用紧凑对象头。显著降低(至少15%)内存占用,并可能明显提升 CPU 性能。
条件参数
这些参数要么只在特定情况下有性能提升,要么需要复杂的配置,需要根据实际情况配置。
-XX:+UseStringDeduplication
启用字符串去重。小幅延长 GC 暂停时间用于字符串去重(通常不明显),而可以显著降低内存占用(幅度不定,模组/插件越多越显著)。但是该参数与 ZGC 存在不兼容,导致长暂停时间,故不应该和 ZGC 同时使用。
-XX:UseAVX=2
只使用 AVX2 指令集,不使用 AVX512(UseAVX=3)。由于 Intel 的 AVX512 指令集普遍性能差劲,不如 AVX2,当且仅当使用支持 AVX512 的 Intel CPU 时应使用该参数,以保证性能和稳定性。其他情况下不应该设置该参数。
-XX:+AlwaysPreTouch
提前申请所有内存。当且仅当最小堆内存(-Xms)不等于最大堆内存(-Xmx)时,能够提升堆内存扩展时的性能,并且只在最大堆内存非常巨大(32G以上)时,或是禁用了压缩对象指针(此功能通常不应手动禁用)时效果比较明显。
-XX:+UseLargePages / -XX:+UseTransparentHugePages
使用大内存页/透明大页。可以改善内存瓶颈,显著提升性能。这两个参数虽然功能类似,但只能使用其中一个参数,不能同时使用,并且配置复杂、各有优缺点,配置方法能再写一章,此处不做补充。如果你不想找罪受,请不要使用此参数。
-XX:-DontCompileHugeMethods
允许编译巨型方法。可以提升处理性能,通常可以选择使用该参数,但在极端情况下,可能在多模组/插件环境下导致编译任务挤压或编译缓存不足,造成后续性能下降。
-XX:+PerfDisableSharedMem
禁用共享内存。可能在硬盘读写频繁时提升性能,但会导致 Jconsole、JMC、VisualVM 等外部工具无法直接识别 JVM,导致调试困难。
不推荐参数
这里列出了不推荐使用的参数,仅作警示作用。
-XX:+DisableExplicitGC
禁用显式 GC。尽管能避免某些模组/插件的屎山代码频繁调用 System.gc() 导致的卡顿,但会导致原版阻止爆内存的机制、一些模组的资源清理机制失效。如果你真的怀疑模组的屎山代码,请使用 -XX:+ExplicitGCInvokesConcurrent。
内存和 GC
内存和 GC 调优大概是 JVM 优化中最重要也是最复杂的部分。相比较上面的小修小补,这部分的优化能极为显著地改善综合性能、减少卡顿。
由于 Java 是使用 GC(垃圾收集/收集器)的编程语言,其内存管理概念较为特殊,在分配的堆内存即将耗尽前,JVM 会自动进行 GC,清理无用内存,而通常不需要开发者手动清理。但有利就有弊,不用手动清理内存,就得手动调教内存分配和 GC 策略了。
内存
内存部分的优化,指调整 Java 堆内存,在启动器中也就是“游戏分配内存”。而其对应的 JVM 参数为 -Xmx(最大堆内存)和 -Xms(最小堆内存)。
# 在 -Xmx 和 -Xms 后添加分配内存量(如 8G 或 2536M),数字只能是整数
# 通常,-Xmx 和 -Xms 应设置相等,防止动态分配
-Xmx8G -Xms8G
不当的内存配置会导致 GC 压力,带来长时间卡顿;或是直接爆内存,游戏都崩掉。总体上,你需要遵循以下几点:
分配内存时,不能直接压着系统可用内存,至少要预留 1~2GB。
设置的分配内存 ≠ JVM 实际占用的内存。通常来说,JVM 实际占用的内存要比设置的分配内存多 1~2GB。
尽量保证堆内存至少有 50% 的空间。
这能保证你的游戏能应对突然的性能压力(比如大量区块/实体加载)。
打开你的 F3 菜单,或者去掉服务端的 nogui,设置一个你认为符合日常游玩情况的测试环境,观察内存那一栏,当其突然降低时,将其记录下来,然后把分配内存设为记录到的最低内存的至少两倍。
一般不要分配 >24GB 的堆内存。
首先,Minecraft 通常并不需要非常巨大的堆内存,模组较少、视野距离不高的情况下,4~8GB 足矣;哪怕要玩大型模组,如果没有特殊要求,8GB~16GB 也足够了。
其次,JVM 有一项重要功能——压缩对象指针,能够有效降低堆内存占用、缓解 CPU 缓存压力。在 24GB 的堆上,此功能可能回退到 Non-zero 模式(此时性能有所下降,GC 暂停也会延长),甚至在 ≥32GB 的堆上无法启用而自动禁用(此时堆内存占用显著升高)。除非有极特殊的需求,否则不应该设置 31~32GB 的堆内存分配,或者添加参数 -XX:-UseCompressedOops 以防止 Non-zero 模式开销。(除非你使用 ZGC,因为 ZGC 与压缩对象指针不兼容,但仍不建议分配超过 32GB)
最好不要使用任何通过触发 GC 实现的“内存清理”模组。
这些模组手动触发 GC 会导致额外的卡顿,并且可能干扰 GC 的算法,反而导致体验变差。如果一定要使用,请添加参数:-XX:+ExplicitGCInvokesConcurrent。
GC
GC(垃圾收集器)在 JVM 堆内存不足时识别并清理无用内存,保证 JVM 总有充足的可用内存,其总体上有三种回收方式:
并行回收(Concurrent GC):在 Java 程序运行的同时,利用一部分 CPU 资源,或是为第二种回收方式做准备,或是直接回收可以安全回收的堆内存。由于其占用了一部分 CPU 资源,可能降低运行时性能,通常是“突然掉帧一会”的成因。
常规暂停回收(Pause GC):通常发生在一次并行回收之后,某些内存无法在 Java 程序运行的同时回收,就暂停整个 Java 程序进行回收,完成后再恢复程序运行。其基本不占用 Java 程序的 CPU 资源,但通常需要较长的暂停时间来完成回收,通常是“突然卡了一下”的成因。
完整暂停回收(Full GC):暂停回收的变种,当一轮并行回收+常规暂停回收仍然无法回收足够的堆内存,或是并行回收的过程中堆内存不足(并行回收失败),为了不让 JVM 直接爆内存,就再次强制暂停,对整个内存堆进行扫描和回收。这个过程极其耗费时间,造成数百毫秒至几秒甚至更长的暂停,通常是“突然卡死不动”的原因。
另外,目前主要的 GC 都有“分代模式”,将新使用的内存归到新生代,在多次回收中都没被回收的内存归到老年代。由于几乎所有 Java 程序(包括 Minecraft)都存在大量临时使用的内存,这些新生代的临时内存就可以快速回收,而长期使用的内存放在老年代,隔一段时间检查一下就好了。这种回收模式效率通常更高,由于 Minecraft 普遍内存分配速率很高,产生大量临时对象,这也是最适合 Minecraft 的方案。
众所周知,“既要”“又要”很多时候是做不到的,GC 也是如此。想要更高的性能,就不能有太多的并行回收,导致暂停时间变长;想要更少的突然卡顿、更少的暂停时间,就需要更多的并行回收,性能就有所下降。根据实际需求,你应该选择适合的 GC。(不想看细节可以看后面的总结)
Serial / Parallel
从上古时期的 JVM 就有的、极其古老的 GC,二者区别在于是否是多线程回收。其只进行常规暂停回收,虽然没有任何 GC 性能损失,暂停时间极长(极端情况下达到数十秒),目前(尤其是 OpenJDK 26 之后)已经全方位差于 G1GC,无论如何都不应该在 Minecraft 中使用。
CMS Old GC 在 OpenJDK 14 被移除,此处不做讨论。
G1GC
旨在平衡“高性能”和“低暂停时间”的 GC。其主要在 Parallel 的基础上加入了一部分并行回收,并且优先回收可回收内存更多的内存区域而不是全部回收,二者相结合,在最小化性能损失的前提下,其 ~99% 的常规暂停回收时间在 200ms 以内(对 Minecraft 通常是 50ms 以内)。
但是“不是全部回收”的特点,又导致垃圾内存的堆积,最终每隔一段时间因为内存不足产生完整暂停回收,暂停时间从数百毫秒到数秒不等。
对于不希望产生任何卡顿的使用环境(比如轻量客户端、PvP 服务器等),G1GC 不会是最好的选择,毕竟你也不想正玩得酣畅突然卡死。
而对于性能压力巨大的使用环境(比如大型生电服、大型模组端),G1GC 不失为一个好的选择。
OpenJDK 的默认 GC 就是 G1GC,如果不放心,也可以自己再添加一次参数:-XX:+UseG1GC
特别注意:使用 G1GC 时最好不要分配超过 16GB 的堆内存,否则会积压大量垃圾内存,完整暂停回收时间会极长!
ZGC
旨在追求极端的“低暂停时间”的 GC。其通过染色指针等黑魔法,将几乎整个回收过程都改成了并行回收,平均暂停时间极其低(几到几十微秒级),如果不发生完整暂停回收基本等于没有暂停,没有可感的突然卡顿。
但是,其不兼容压缩对象指针,导致在 <32GB 的堆上,其内存占用显著高于其他 GC,可能产生 CPU 缓存压力,在内存较少的环境下(如 4GB 堆内存),容易在突然加载大量区块/实体时产生完整暂停回收,反而产生卡顿。其并行回收耗时极长,通常数百毫秒至上千毫秒,在物理核心数较少的 CPU 上会占用大量资源,导致整体性能低下。
对于 ZGC,其适用场景基本和 G1GC 恰好相反,即在希望几乎无卡顿的使用环境(比如轻量客户端、PvP 服务器等)更推荐使用 ZGC 这样的“无暂停”GC。但是应该注意的是,ZGC 需要大量堆内存(建议至少 6GB)才能保证性能和暂停时间的稳定性,对于内存缺乏的设备,你可能更需要下面的 ShenandoahGC。
使用下面的参数启用 ZGC(OpenJDK 25 默认启用分代 ZGC,无需 -XX:+ZGenerational):-XX:+UseZGC
ShenandoahGC
同样是追求“低暂停时间”的 GC。但与 ZGC 不同的是,ShenandoahGC 更像 G1GC 的改良版,是一个更加综合性的方案。ShenandoahGC 同样引入了大量的并行回收,但又没有 ZGC 那么极端,暂停时间依然远低于 G1GC,比 ZGC 高一些(几百微秒到几毫秒),通常也不会有可感的卡顿。但是 ShenaodoahGC 支持压缩对象指针,内存占用和 G1GC 基本相同,相对应的 CPU 缓存压力更低,低内存条件下也更不容易触发完整暂停回收,卡顿反而可能相对 ZGC 更少。另外,虽然 ShenandoahGC 的并发回收占比也不低,但并发回收的平均耗时一般在 100ms 上下,频率也更低,相比于 ZGC 的几百毫秒少了太多,即使在物理核心数较少的 CPU 上,也不会对性能产生太大影响。
当然,你很可能没看到有什么人宣传 ShenandoahGC,因为它太新了,它的分代模式在 OpenJDK 25 才正式加入,而 ZGC 的分代模式在 OpenJDK 21 就加入了。并且,ShenandoahGC 在 某些 JDK 发行版(其实只有 Oracle OpenJDK/JDK,其他的 JDK 甚至是 Oracle GraalVM 全都支持) 中不可用,并且有一定的兼容性风险。
使用下面的参数启用 ShenandoahGC:-XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational
总结
性能压力巨大的使用环境(比如大型生电服、大型模组端),建议使用 G1GC。(-XX:+UseG1GC)
在希望几乎无卡顿的使用环境(比如轻量客户端、PvP 服务器等):
内存分配至少 6GB,CPU 至少 6 核,可以使用 ZGC(-XX:+UseZGC)。
不满足 ZGC 的条件,或者 ZGC 卡顿,推荐使用 ShenandoahGC(-XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational)。
通用优化方案
客户端
JVM 使用 Alibaba Dragonwell 25 Extended Edition (下载时记得看好版本)。
游戏分配最大内存通常不需要超过 16GB。如有特殊需求需要更大的内存,请尽量设置为 <24GB 的值(不包括 24GB),否则请添加 JVM 参数:-XX:-UseCompressedOops。
按如下配置启动参数:
# 启用紧凑对象头
-XX:+UseCompactObjectHeaders
# 下列 GC 参数三选一
# 大型生存/模组更建议使用 G1GC
# 注意:使用 G1GC 时最好不要分配超过 16GB 的内存!
-XX:+UseG1GC
# 内存充裕(分配 6GB 以上)且 CPU 6 核以上时可以使用 ZGC
-XX:+UseZGC
# 不满足 ZGC 条件时更推荐使用 ShenandoahGC
-XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational
# 如果是支持 AVX-512 的 Intel 处理器,请务必额外添加以下参数
-XX:UseAVX=2
服务端
JVM 使用 Oracle GraalVM 25 (Innovation) (下载时记得看好版本,只有 25.4 及以上的 GraalVM 支持 ShenandoahGC)。
游戏分配最大内存通常不需要超过 16GB。如有特殊需求需要更大的内存,请尽量设置为 <24GB(不包括 24GB) 的值,否则请添加 JVM 参数:-XX:-UseCompressedOops。
按如下配置启动参数:
# 启用紧凑对象头
-XX:+UseCompactObjectHeaders
# 下列 GC 参数三选一
# 大型生存/生电/模组服更建议使用 G1GC
# 注意:使用 G1GC 时最好不要分配超过 16GB 的内存!
-XX:+UseG1GC
# 小型生存/小游戏服务器更建议使用 ZGC/ShenandoahGC
# 内存充裕(分配 6GB 以上)且 CPU 6 核以上时可以使用 ZGC
-XX:+UseZGC
# 不满足 ZGC 条件时更推荐使用 ShenandoahGC
-XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational
# 如果是支持 AVX-512 的 Intel 处理器,请务必额外添加以下参数
-XX:UseAVX=2
鸣谢和拓展
该教程发展自作者的上一篇教程,是作者以自身实际经验为基础、结合以下内容整理的:
如果有兴趣进行更加深层次的优化,可以选择阅读这些文章。但是再次强调,请永远牢记:①如果你的客户端/服务器本来就运行良好,就不要进行过多的优化;②深层的 JVM 调优永远只能缓解问题,不能消灭问题,很多时候,垃圾模组和不当游戏行为才是问题的源头!
特别指出,Cubic Wiki 中的内容极为丰富,但其中提供的部分 JVM 参数太过冗长,且未提供足够的说明,而本文旨在提供简单、通用的优化方案,故不作为建议。而 PaperMC 文档中的参数虽然说明详细,但是只针对 G1GC,并且疑似存在过时问题。


