Minecraft Servers vs Malloc implementations - I cut RAM usage in half for some servers

So I have been hosting Minecraft servers for friends for a while now, and I noticed that some of them are using way more ram than they should.

The main server I will be focusing on this thread has the following config:

  • Paper with plugins
  • Java 21
  • Runs in a Docker container (Ubuntu based)
  • Managed by Pterodactyl
  • 10GB Heap allocated in JVM arguments.

Full list of JVM arguments: java -Xmx10G -Xms10G -XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=200 -XX:+UnlockExperimentalVMOptions -XX:+DisableExplicitGC -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 server.jar --nogui

Disclaimer: I am running this test in parallel to normal server things, so things may be updated in the middle of the test, and player activity also affects the results, so take them with a grain of salt.

I learned about a thing called “memory fragmentation” a while ago and was suspecting if this was the case, rather than an actual memory leak. All of my plugins are very popular and leaks would have already been caught much earlier, especially ones that are to the degree I am seeing.

Memory usage using different memory allocators:

Malloc provider Memory usage after a while
Glibc (Baseline, version 2.35) about 25GB after 5 weeks and still growing (I killed it before it could use up all my RAM)
Jemalloc (5.2.1) About 11.4GB after 1-2 weeks and not growing further at the 4 week mark
Mimalloc (2.0.5) Currently being tested
Tcmalloc (2.9.1) Going to be tested next
Idk any suggestions for other alternatives?

Thought the results are interesting so sharing them here. Will come back and edit the post with results when I finish testing other implementations.

2 Likes

Try running glibc with a fixed heap size (Xms set to the same value as Xmx). If the JVM is constantly adjusting the heap size, this can lead to fragmented memory allocations from the kernel, which glibc never fully releases, since glibc is allocating 64 MB “arenas” from the kernel and not memory from the kernel directly for every request from the JVM.

Run pmap on the JVM process to see the allocations from the kernel.

I would also pass -XX:NativeMemoryTracking=summary to the java arguments, then run jcmd <pid> VM.native_memory summary to get the JVM’s perspective on allocations.

1 Like

I was already using -Xms10G flag. Edited the original post with the full JVM flags

If it’s in a docker container, you can just cap the memory usage at the docker level:

    deploy:
      resources:
        limits:
          cpus: "2.00"
          memory: "512M"
          pids: 50

or whatever relevant limits. I have limits on all of my containers. :man_shrugging:t2:

It will not stop Glibc malloc from fragmenting the memory and the container will get OOM killed sooner or later which is not ideal as that might lead to data corruption.

You may also want to try -XX:+AlwaysPreTouch, which will immediately allocate the full heap size.