Every Minecraft server host eventually pastes the same wall of JVM flags into their start script. It works, which is the problem: it works well enough that nobody ever finds out what the twenty arguments do, and so nobody knows which one to change when the server starts stuttering anyway.
The flag set is Aikar's flags, documented by PaperMC. What follows is what it is actually asking the garbage collector to do.
The thing being tuned
Java does not free memory when you stop using an object. A collector decides later, and while it decides, it may stop the application. On a game server, a stop-the-world pause is a freeze: every player, at once, for as long as it lasts.
G1 is the collector these flags configure. It divides the heap into equal-sized regions and collects a subset of them at a time, aiming to keep each pause under a target you give it. That target is the first flag worth understanding.
MaxGCPauseMillis=200
The documentation describes 200ms as "aiming for at most loss of 4 ticks". That framing is the whole design. A Minecraft tick is 50ms. A 200ms pause is four ticks the server did not run, which players feel as a stutter and not as lag.
This is a goal, not a guarantee. G1 will size its collections to try to hit it, and will miss it under pressure. Lowering it does not make pauses shorter; it makes G1 collect smaller batches more often, which raises total overhead. People who set it to 50 usually make things worse.
G1HeapRegionSize=8M
The page calls this "super important for Minecraft", and the reason is a G1 concept called a humongous allocation: an object larger than half a region is not allocated normally. It gets its own set of contiguous regions and is collected on a slower path. Oracle's G1 tuning guide describes how these are handled and why they are expensive.
Minecraft allocates large arrays constantly: chunk sections, block state palettes, lighting data. With small regions, a large share of those become humongous, and the heap fills with objects the collector cannot handle cheaply. An 8MB region raises the humongous threshold to 4MB, and most of those allocations become ordinary again.
The new-generation pair
G1NewSizePercent=30 and G1MaxNewSizePercent=40 set the floor and ceiling for the young generation. G1's defaults start the young generation much smaller and grow it adaptively.
Minecraft produces enormous quantities of short-lived garbage: every tick, every entity, every packet. A larger young generation means more of that dies before it is ever promoted to the old generation, and objects that die young are nearly free to collect. Objects that get promoted are not.
G1MixedGCCountTarget=4, down from a default of 8, and InitiatingHeapOccupancyPercent=15 both push in the same direction: start cleaning the old generation earlier, and finish in fewer, larger passes.
G1ReservePercent=20, up from a default of 10, holds back more free space as a buffer against the failure mode G1 is worst at: running out of room mid-collection and falling back to a full stop-the-world pass.
-Xms equal to -Xmx, and AlwaysPreTouch
The guide is blunt: "unused memory is wasted memory". Setting the starting heap equal to the maximum stops the JVM growing the heap in stages, which it does by pausing.
AlwaysPreTouch makes the JVM touch every page of that heap at startup. Boot takes noticeably longer. In exchange, the memory is genuinely committed before the first player connects, rather than being faulted in during play. The documentation notes it is mandatory for transparent huge pages.
Both of these assume the memory is yours. On an oversubscribed container it is not, and committing a heap you were never really given is how a host's out-of-memory killer finds you first, which is the part shared and dedicated hosting differ on that the sales page never mentions.
Reading a GC log instead of guessing
If you want to know whether any of this is working, the flags themselves tell you nothing. The log does.
-Xlog:gc writes one line per collection: when it happened, what kind it was, how much the heap dropped, and how long the pause was. On a healthy server that is a steady rhythm of young collections a few seconds apart, each finishing in tens of milliseconds, with the heap sawtoothing between roughly the same two figures all evening.
Three shapes are worth recognising, because each points somewhere different:
Young collections getting longer over hours. The young generation is filling with objects that survive. Usually entities, usually a farm or a mob build somebody added, and it is a world problem rather than a flag problem.
Frequent mixed collections that free very little. The old generation is full of live data. Either the heap is genuinely too small, or something is holding references.
A Pause Full line. G1 gave up and did a complete stop-the-world collection. On a Minecraft server that is measured in seconds and every player felt it. One after a /reload is unremarkable; one during ordinary play means the reserve was not enough, and it is the single clearest signal that the heap needs to be larger.
The rhythm is the diagnostic, not any individual number, which is the same reason a profile beats a snapshot everywhere else on a server.
Where the memory goes that is not the heap
-Xmx is not the server's memory usage, and the gap surprises people who sized a machine from it.
The JVM also allocates metaspace for loaded classes, a thread stack for every thread, plus direct byte buffers (which Netty, and therefore every Minecraft server, uses heavily), plus the garbage collector's own bookkeeping. On a plugin-heavy server that overhead runs to several hundred megabytes and occasionally more than a gigabyte.
The practical rule is to leave at least a gigabyte above -Xmx on a dedicated box, and more than that if the plugin list is long. A 4GB machine running -Xmx4G is not a 4GB server; it is a server that will be killed by the kernel at an unpredictable moment, with nothing in the application log to explain it.
What none of this fixes
A flag set cannot fix a plugin that leaks. If memory climbs steadily across hours and a restart is the only thing that helps, that is not the collector's failure: that is something holding references it should have dropped, and the restart-schedule question is a symptom rather than a cure.
It also cannot fix a heap that is simply too small for the world and the player count. If you are giving a 60-player server 4GB, no arrangement of these flags will save it, and the honest answer lives in what the hardware actually needs.
Give the JVM a sensible heap, paste the flags, and then spend your time on monitoring what it does over days rather than on the flags themselves. They are already close to right. That is why everyone uses them.
- Tags
- minecraftperformancejava
- Share
Published · 5 min read