Higher performance crafting: Using JDK11+ and ZGC.

Still using JDK8 and G1 GC? Upgrade!


Disclaimer: I am in no way, shape, or form responsible for your actions. If you burn down your dirt house or hurt your feelings, don’t blame me. But anyway, happy tweaking.


What do you need?

    • JDK13+ (JDK11 is not recommended due to a slow ZGC implementation!) on Linux or JDK14+ on Mac and Windows,
    • Install privileges or just run a portable version of the JDK,
    • Sheep.

Step by Step:

  • 0. Install the JDK11 or higher on your setup

On almost any platform, AdoptJDK will get you up to date with the latest JDK, just follow their instructions (scroll down to the installer section): https://adoptopenjdk.net/installation.html
  • 0.5. Verify that the new JDK is detected and being used

Run  java -version in your terminal, powershell or cmd. You should get something like this: 2020-03-25_15-16-12
If you don’t get the correct version information, make sure to remove old versions from your setup and set the correct JAVA_HOME path.
  • 1. Why should I use ZGC instead of G1 GC?

Simple, ZGC is WAY faster, like super fast. Here are some graphs that show garbage collection time on a MC server with 8GB of allocated ram and ~60 players online at peak time:
Testing was done on an Intel 9900k (non OC), HT enabled, 4x8GB 2133Mhz memory, 2x 1TB Crucial NVMe drives, transparent huge pages enabled and running on Java 13.


Garbage collection time scaled to 1 ms:

And the same graph scaled to 50 ms (1 Minecraft tick):


G1 GC with flags from my startup script on a server that has 1-3 players online at peek.
(If anyone else is prepared to log G1 GC on a bigger server please contact me.)
Graph scaled to 1 ms:

And the same graph scaled to 50 ms (1 Minecraft tick):

The GC time with ZGC is negligible, on the other hand G1 with the suggested flags tries to stay below 100 ms, whereas ZGC was designed to stay below 10 ms, at any memory range, without thread blocking.
  • 2. Enable ZGC

Enabling ZGC depends on your startup method (some flags are not required, but are added to avoid issues):
Linux bash script: You can just use my script from here.
Mark2: Add the following line to your mark2.properties:  java.cli_extra=-XX:+UnlockExperimentalVMOptions -XX:+DisableExplicitGC -XX:-UseParallelGC -XX:-UseParallelOldGC -XX:-UseG1GC -XX:+UseZGC
Anywhere else: Just replace your flags with:  -XX:+UnlockExperimentalVMOptions -XX:+DisableExplicitGC -XX:-UseParallelGC -XX:-UseParallelOldGC -XX:-UseG1GC -XX:+UseZGC
Quick explanation of the flags:
+UnlockExperimentalVMOptions – Unlocks experimental flags/options,
+DisableExplicitGC – Disables System.gc() calls from code, you really don’t want people playing around with your GC,
-UseParallelGC – Disables Parallel GC, this should already be disabled, but we set this just to be sure,
-UseParallelOldGC – ^ but disables ParalledOld GC,
-UseG1GC – ^ but disables G1 GC,
+UseZGC – Enables ZGC.

*If you notice degraded performance, higher CPU usage, more memory commits/uncommits, setting the following flag might help you -XX:-ZUncommit.
That will tell ZGC not free up unused memory. This can help on shared systems or system with low memory bandwidth, do note the flag only being added in JDK13.
IF you still want uncommits, you can try using Large Pages/Transparent Huge Pages. Also see point 3.


  • 3. Monitor ZGC

ZGC has some issues with servers that as leaking or just allocating memory too quickly (Survival servers for example).
To see if your ZGC is performing well, you can monitor the servers memory usage using JMX or logging GC to a file.
To log GC to a file add the following flag:  -Xlog:gc*:logs/gc.log:time,uptime:filecount=2,filesize=8M, this will add a file called gc.log in your logs directory.
After running the server for at least an hour at normal load, open the file and search for the following text  Allocation Stall (. If you can’t find a single line matching that text, look at the last occurrence of the following text: Critical: Allocation Stall. If it looks something like this:

Critical: Allocation Stall  0.000 / 0.000  0.000 / 0.000  0.000 / 0.000  0.000 / 0.000 ms

then your ZGC should be running fine without issues.
If you see a bunch of lines showing allocations stalls (example below) and a high number in the summary, you should try an investigate the issue, allocate more memory or switch back to G1.

[2020-04-13T00:11:41.490+0200][682.338s] Allocation Stall (Craft Scheduler Thread - 46) 290.661ms
[2020-04-13T00:11:41.490+0200][682.338s] Allocation Stall (Server thread) 269.893ms
[2020-04-13T00:11:41.490+0200][682.338s] Allocation Stall (mysql-cj-abandoned-connection-cleanup) 0.207ms
Critical: Allocation Stall  0.000 / 0.000  223.572 / 458.018  200.367 / 458.018  200.367 / 458.018  ms

How the same information can be seen on JMX graphs:
On the start of the graph, the committed memory is close to the max, but never reaches it, this in most cases indicates that ZGC has enough room to clean without stalls.
After the middle you can see the committed memory hit the max and stay there, this will result in stalls resulting in lag. You can try to fix this issue by adding more memory, but this might not work in all cases as shown below:
In this case I recommend switching back to G1GC, as it’s better at handling memory leaks and high allocation speeds.
Some example of well running ZGC setups:

Bungee:

Lobby:

Vote Bungee:

As you can see there is plenty of ram leftover. In a case like this you can lower the max ram to save some memory or leave some headroom if you like flexing with memory.


That’s It, You’re Done!

No tuning needed, it just works. There are some things you could tune, but AdoptJDK already does that for you.
To verify that you are actually using ZGC you can use Timings v2 (that are build into Paper).

Don’t use plugins to check GC time, as they are not yet optimized for ZGC (Spark for example), use something like Java Management Extensions (JMX) or Timings V2 (Thanks to Aikar for adding a GC option).

If you have plugin issues due to Java being updated, yell to the plugin authors! There are 7-year-old plugins that still run on JDK 14 and Minecraft 1.15.2, while they can’t create a plugin that works on JDK9+ in 2020?