In our previous chapter we have talked about the JVM memory configuration: when to use the defaults and when it might be necessary to fine tune the settings. We also touched on potential side effects of manual tuning and how it can sometimes backfire, for example, forcing the Garbage Collector (GC) to work harder.
In the second part of this series we are taking a look at the different Garbage Collectors available, which is the best for a specific use case, and when it’s useful to switch to a different implementation.
» The default Garbage Collector
At the time of writing this article (Sept. 2021) SmartFoxServer 2X runs on Java 8 by default, which in turn uses the Parallel GC. This is a multi-threaded collector that manages the heap in parallel while pausing the application threads, so it can momentarily stop the execution of our code while sweeping away the garbage.
These pauses are usually very short, in the order of a few milliseconds for ordinary heap sizes but they can increase with very large memory settings (i.e. 8GB+ of allocated heap) and high server activity.
The Parallel GC can also be fine tuned to control the number of threads used and the ideal pause length for each collection cycle, to avoid latency.
If you’re interested to learn more about these settings you can take a look at the details on Oracle’s website.
» More Garbage Collectors
Beyond the default GC the JVM offers other garbage collection options that can be used in different scenarios, depending on the use case, hardware available and Java Runtime (JRE) version.
As of Q3 2021 the main options available in the JVM are:
CMS GC (Concurrent Mark Sweep): uses a similar multi-thread approach to the Parallel GC but doesn’t stop your application. Pauses are overall shorter and your application will share some CPU resources with the GC while the everything is running. It’s a tradeoff where you pay a bit of the CPU budget for a less intrusive GC cycles and virtually no pauses.
G1GC (Garbage First): is a garbage collector designed for multi-processor machines using large heap sizes (tens of GBs according to the docs) aiming at consistent, small GC times. This is typically not recommended for SFS2X as it occupies a very small heap and the need to switch to G1GC would be warranted only by very memory hungry Extensions.
ZGC: this is a more recent implementation, released as production-ready since JDK15. It uses a low-latency, concurrent algorithm and guarantees pauses < 10ms for application threads. This is particularly good for low-latency environments such as game servers and it can also replace the G1GC. It could be a consideration to switch to ZGC for developers running on JRE 15 or higher, for a high traffic applications.
This is not meant as a comprehensive list of all possibile GC options and in fact there are more, but these represent the most commonly used on the server side these days.
Which GC should I use?
We will answer the question by taking a look at a real-life production example where we tested different JREs and GC implementations under the same testing conditions.
We tested a real-time, memory-intensive SFS2X Extension where users are grouped in Rooms of 8-10 players and sending 20 positional updates per second. For the tests we used an Intel Xeon (quad-core 2.0Ghz) with 32GB RAM, and Ubuntu 20.04 Server installed without any custom JVM or Linux settings.
Here’s the example running on vanilla SFS2X 2.17.0 / JRE 8 / default GC (Parallel)
Next is the same test running under the same SFS2X / JRE 8 / using the G1GC collector:
Finally the same test once again, this time under JRE 15, using the ZGC:
It can be noted that these GCs work in fairly different ways and use different strategies for memory management. The default Parallel GC tends to be more conservative, often reducing the allocated heap, when possible. Instead G1GC and ZGC (designed for memory hungry apps) tend to leave more allocated heap even when it’s not strictly needed.
Generally speaking we recommend using the standard Parallel GC found in JDK8 as it is more than enough for a multitude of turn-based and real-time games that don’t use very large amounts of heap (e.g. tens of GBs).
However if your application does fall in the “memory-hungry” category with heap sizes that easily reach the 10-20GB mark (and higher) you will likely benefit from using the G1GC or, even better, the ZGC (which requires switching to a different JDK).
» Useful resources
If you want to dive deeper into this subject and learn more about the various Garbage Collection options we recommend these external articles:
- Tuning Garbage Collection (Oracle)
- JVM Garbage Collectors (Baeldung)
- Seven Types of Garbage Collectors (Medium)