前排提示:

[!] 注意,该教程仅面向具有独立服务器的情况,如果你是面板服,就不用看了

[!] 注意,该教程假定你有一块还算不错的CPU,2.XHz的就别看了

[!] 注意,该教程需要一定的Linux知识,并且假定你正在使用Ubuntu,其它系统自己百度

0. 一些废话和声明

Q: 为什么要写这一篇帖子?
A: 这两天在开服,随手翻看了几篇优化帖子,发现都是基于老版本Java的,于是就想写一篇新的。

Q: 我可以转载这篇帖子么?
A: 很抱歉,你不可以这么做,除非你联系我(MCBBS, Twitter, 知乎:@VeroFess)并获得了明确的授权

1. 服务器准备

1.1 升级Java

为啥要升级Java?很简单,更新的Java可以获得更好的性能,但是在高版本里其实你选择哪个Java的影响已经不是很大了。

不论如何,请使用你的方法安装Java 17,最好是Jdk, 完成后, 输入 java --version, 来检查一下是否安装正常

你应该可以看到类似的输出

$ java --version
openjdk 17.0.3 2022-04-19
OpenJDK Runtime Environment (build 17.0.3+7-Ubuntu-0ubuntu0.20.04.1)
OpenJDK 64-Bit Server VM (build 17.0.3+7-Ubuntu-0ubuntu0.20.04.1, mixed mode, sharing)

1.2 开启大页支持

大页 (LargePages) 究竟有什么好处并不是我们今天的重点,一句话来说就是可以让你白捡百分之十几的性能

当然,在动手前,让我们先试一试是不是系统已经支持了这项功能

在控制台中, 输入 java -Xlog:gc+init -XX:+UseLargePages -Xmx1g -version , 你应该会看到类似的输出

$ java -Xlog:gc+init -XX:+UseLargePages -Xmx1g -version
[0.000s][warning][pagesize] UseLargePages disabled, no large pages configured and available on the system.
[0.023s][info ][gc,init ] Version: 17.0.3+7-Ubuntu-0ubuntu0.20.04.1 (release)
[0.023s][info ][gc,init ] CPUs: 16 total, 16 available
[0.023s][info ][gc,init ] Memory: 31972M
[0.023s][info ][gc,init ] Large Page Support: Disabled
[0.023s][info ][gc,init ] NUMA Support: Disabled
[0.023s][info ][gc,init ] Compressed Oops: Enabled (32-bit)
[0.023s][info ][gc,init ] Heap Region Size: 1M
[0.023s][info ][gc,init ] Heap Min Capacity: 8M
[0.023s][info ][gc,init ] Heap Initial Capacity: 500M
[0.023s][info ][gc,init ] Heap Max Capacity: 1G
[0.023s][info ][gc,init ] Pre-touch: Disabled
[0.023s][info ][gc,init ] Parallel Workers: 13
[0.023s][info ][gc,init ] Concurrent Workers: 3
[0.023s][info ][gc,init ] Concurrent Refinement Workers: 13
[0.023s][info ][gc,init ] Periodic GC: Disabled
openjdk version "17.0.3" 2022-04-19
OpenJDK Runtime Environment (build 17.0.3+7-Ubuntu-0ubuntu0.20.04.1)
OpenJDK 64-Bit Server VM (build 17.0.3+7-Ubuntu-0ubuntu0.20.04.1, mixed mode, sharing)

如果你像我一样, 有一行 UseLargePages disabled, no large pages configured and available on the system. 那么就说明当前系统并不支持大页,不过不要急,可以试一下这一行命令 java -Xlog:gc+init -XX:+UseTransparentHugePages -Xmx1g -version , 你应该会看到类似的输出:

$ java -Xlog:gc+init -XX:+UseTransparentHugePages -Xmx1g -version
[0.024s][info][gc,init] Version: 17.0.3+7-Ubuntu-0ubuntu0.20.04.1 (release)
[0.024s][info][gc,init] CPUs: 16 total, 16 available
[0.024s][info][gc,init] Memory: 31972M
[0.024s][info][gc,init] Large Page Support: Enabled (Transparent)
[0.024s][info][gc,init] NUMA Support: Disabled
[0.024s][info][gc,init] Compressed Oops: Enabled (32-bit)
[0.024s][info][gc,init] Heap Region Size: 1M
[0.024s][info][gc,init] Heap Min Capacity: 8M
[0.024s][info][gc,init] Heap Initial Capacity: 500M
[0.024s][info][gc,init] Heap Max Capacity: 1G
[0.024s][info][gc,init] Pre-touch: Disabled
[0.024s][info][gc,init] Parallel Workers: 13
[0.024s][info][gc,init] Concurrent Workers: 3
[0.024s][info][gc,init] Concurrent Refinement Workers: 13
[0.024s][info][gc,init] Periodic GC: Disabled
openjdk version "17.0.3" 2022-04-19
OpenJDK Runtime Environment (build 17.0.3+7-Ubuntu-0ubuntu0.20.04.1)
OpenJDK 64-Bit Server VM (build 17.0.3+7-Ubuntu-0ubuntu0.20.04.1, mixed mode, sharing)

注意到 Large Page Support: Enabled (Transparent) 了么?说明我们的系统是支持透明大页的。

但是如果你依然不支持或者想要追求极致性能,可以去百度搜索当前的系统如何开启大页,这里就不再过多的赘述了。


2. 启动参数

2.1 G1GC vs.ZGC

GC引擎的选择是一个老生常谈的问题了,同其他教程不同,我本人更加推荐ZGC。

理由很简单,现在的CPU动辄八核十六核,但是MC又是一个出了名的吃单核游戏,用其他核心的时间换主核心的无暂停是很划算的事情。

G1GC在设计之初考虑的是吞吐量和占用的均衡,而ZGC则是用更加高昂的CPU代价换取主线程无停顿,这对我们来说简直是刚好。但是并不是每一个人都适合ZGC,在接下来的文章里我会教你怎么样查看自己适不适合它。

2.2 来吧!启动参数!

2.2.1 总览

准备好, 参数要来了,有点长,不过我会分段告诉你他们的作用。

java -server -Xmx${memory}M -XX:+UnlockExperimentalVMOptions 
-XX:+UnlockDiagnosticVMOptions -XX:+DisableExplicitGC
-XX:-UseG1GC -XX:+UseZGC -XX:-ZUncommit
-XX:+UseTransparentHugePages -XX:+UseHugeTLBFS
-XX:LargePageSizeInBytes=1g -XX:ReservedCodeCacheSize=512M
-XX:MaxInlineSize=256 -XX:+AlwaysPreTouch -XX:+OmitStackTraceInFastThrow
-XX:+DoEscapeAnalysis -XX:+OptimizeStringConcat -XX:+EliminateLocks
-XX:+UseBiasedLocking -XX:+SegmentedCodeCache -XX:+UseVectorCmov
-XX:+UseSHM -XX:+UseNewLongLShift -XX:+UseFastStosb -jar ${core}.jar

2.2.2 那些不太需要说的部分

-Xmx${memory}M-jar ${core}.jar 这两个参数,着实是不用说…

2.2.3 基础!

-server 这个参数吧,我自己测试确实是有一定的效果的,但是也没其他帖子说的这么夸张,总之加上肯定没错。

-XX:+UnlockExperimentalVMOptions-XX:+UnlockDiagnosticVMOptions 这两个参数,纯粹是我懒得去看后面的一大串是不是在他们的影响列表里,所以加上的。

2.2.4 内存管理调整

-XX:+DisableExplicitGC-XX:-UseG1GC-XX:+UseZGC-XX:-ZUncommit-XX:+UseTransparentHugePages-XX:+UseHugeTLBFS-XX:LargePageSizeInBytes=1g-XX:ReservedCodeCacheSize=512M-XX:MaxInlineSize=256-XX:+AlwaysPreTouch属于内存调整参数。

首先,我们通过-XX:+DisableExplicitGC来阻止显式的System.gc()调用,这倒不是说所有的手动调用都是不好的,但是总有些mod或者插件的编写者觉得调用的越频繁越好,但是过于频繁的GC反而会拖慢速度,所以索性就把这个方法干掉,让Java来帮我们管理。

-XX:-UseG1GC-XX:+UseZGC 这两个参数用来强制Jvm使用ZGC, 不需要做过多的解释

-XX:-ZUncommit-XX:+AlwaysPreTouch 这两个参数呢,要求Jvm总是提前把要用的内存申请好,并且阻止Jvm把临时空出来的内存还给系统,这样会节约不少内存分配的开销,当然,如果你的系统内存不够用,还是去掉这两个参数吧。

-XX:+UseTransparentHugePages-XX:+UseHugeTLBFS-XX:LargePageSizeInBytes=1g 这三个参数用于启用大页支持,并且允许Jvm使用1G的大页, 如果你的系统支持非透明大页(也就是1.2中的第一个指令没有说不支持),就把-XX:+UseTransparentHugePages给改成-XX:+UseLargePages.

-XX:ReservedCodeCacheSize=512M-XX:MaxInlineSize=256这两个参数允许Jvm缓存更多Jit之后的代码,并且允许Jvm把一些比较短的方法做inline优化,是一种用比较小的内存代价来换取性能提升的方法。

2.2.5 其他优化

这一部分主要包含了 -XX:+OmitStackTraceInFastThrow-XX:+DoEscapeAnalysis-XX:+OptimizeStringConcat-XX:+EliminateLocks 这四个参数。

-XX:+OmitStackTraceInFastThrow 这个参数要求Jvm在遇到异常时省略异常栈,因为有一些编写不是很好的mod/插件总是会抛出异常,这也算是一个可能不小的优化。

-XX:+DoEscapeAnalysis 这个参数应该是默认启用的,他要求Jvm进行逃逸分析,并且以分析结果为依据进行其他优化,这里加上只是为了防止某些个例中这玩意由于未知原因被关掉。

-XX:+OptimizeStringConcat 同样, 这个也应该是默认启用的,可以优化字符串拼接的效率。

-XX:+SegmentedCodeCache 这个选项要求Jit编译器把代码拆分为小段,虽然会占用更多的内存,但是会加快运行的速度。

-XX:+UseBiasedLocking 这个选项要求使用偏向锁,由于MC的锁竞争大多发生在主线程,这可以优化一些性能,虽然提示这个东西要移除了,但是能用一天是一天。

-XX:+UseFMA ,-XX:+UseVectorCmov ,-XX:+UseSHM ,-XX:+UseNewLongLShift ,-XX:+UseFastStosb ,这几个选项要求启用一些CPU相关的快速指令,要是你的CPU不支持其中的某一些,就直接删除掉吧。不过一般来说即使不支持也只是单纯的输出一条警告而已。

3. 测试

我们使用 Spark 进行测试,来对比这份参数与另一份网上流传很广的G1GC参数。

在这里,我就使用我自己的服务器进行举例,为了模拟性能捉急的机器,我手动将-Xmx参数设置为10G

为了让服务器有足够高的负载,我将实体限制提升到5000,并且召唤300只凋零来攻击不断被命令方块刷出的牛牛君。

相关的报告可以在 https://spark.lucko.me/srA9dF1wkS (ZGC) 与 https://spark.lucko.me/gzX9cV3xdL (G1GC)看到

TPS 对比:

ZGC  : 20.00(1m)/19.50(5m)
G1GC : 19.31(1m)/19.74 (5m)

MSPT 对比:

ZGC  : 0.519(最低)/18(平均)/43.2(95%ile)
G1Gc : 0.593(最低)/20(平均)/54.4(95%ile)

CPU 对比:

ZGC  : 4.59%(5m)
G1Gc : 5.81%(5m)

内存占用对比:

ZGC  : 2 GB/9.6 GB
G1Gc : 3.8 GB/10 GB

虽然不能说完胜吧,但是还是有了不少的提升的,而且随着运行时间变长,使用我的参数性能应当会越来越好(因为有更加激进的Jit方案),而G1则可能更加频繁的STW.

看起来还不错?但是,在之前我们说过,对我好并不等于对你好,适合自己的才是真的好,在内存非常小的情况下,ZGC反而会占用非常高的CPU,如果你发现你的mspt飙升并且出现了GC分配停顿的状况,则应该考虑换回G1GC。