Last week we mentioned the two most important factors affecting garbage collection performance. These are total available memory and the ratio of the heap allocated to the young generation.
This week we’ll look at some of the JVM runtime options to set memory sizes. This is as important as choosing the right garbage collector.
Remember that most garbage collectors have two generations: young and old. The young generation has three areas: Eden and two equal-sized survivor spaces. There is also a certain amount of virtual space in both generations. This is uncommitted heap space which will be used as the heap grows.
Total Heap Size
The most important setting is the total available memory. The maximum heap size is set with the -Xmx
flag. The initial/starting size of the heap is set with the -Xms
flag. The space for the entire heap is reserved when the JVM starts up.
It’s very important to always set the maximum heap size to less than the amount of physical memory. This minimises page faults and thrashing (copying memory contents to and from disk).
A rule of thumb is to start with -Xmx
set at twice the size of the live set. The live set is the stable runtime size of the heap after garbage collections.
The -Xmx
option is the same as the -XX:MaxHeapSize
option.
If -Xms
is smaller than -Xmx
, then not all the reserved space is immediately committed to the VM. The uncommitted space is the virtual space. Both generations can grow to the limit of the virtual space as needed, based on the ratio between them.
Setting -Xms
and -Xmx
to the same value makes it easier to predict the application footprint (total memory used). However, this prevents the JVM growing and shrinking the heap to meet any throughout goals we may have set. If we choose incorrect/poor values, the JVM can’t adjust the heap size up or down.
Young Generation Size
The second most important setting is the ratio of the heap dedicated to the young generation.
The parameter –XX:NewRatio
specifies the ratio of the young to the old generation.
For example, setting -XX:NewRatio=3
means that the ratio between the young and old generation is 1:3. In other words, the combined size of Eden and the survivor spaces will be one quarter of the total heap size. We can only use whole numbers when setting this ratio.
-Xmnsize
sets both the initial and maximum size of the heap for the young generation.
We can use -XX:NewSize
to set the initial size and -XX:MaxNewSize
to set the maximum size separately. These options are useful for fine-tuning the young generation.
The bigger the young generation, the less often minor collections occur. For a fixed heap size, a larger young generation forces a smaller old generation. This increases the number of major collections. The optimal ratio will depend on the lifetimes of the objects created by the application.
Survivor Space Size
We can use the -XX:SurvivorRatio
flag to set the survivor space sizes. This isn’t usually necessary for performance tuning.
The -XX:SurvivorRatio
parameter changes the size of the survivor spaces. For example, -XX:SurvivorRatio=6
sets the ratio between Eden and a survivor space to 1:6. Each survivor space will be one-sixth of the size of Eden.
If survivor spaces are too small, then the copying collection overflows directly into the old generation. If survivor spaces are too large, then they are almost empty. At each garbage collection, the virtual machine chooses a threshold number. This is the number of times an object can be copied before it’s old. The threshold is chosen to keep the survivor spaces half full.
Guidelines
The performance of a garbage collector is dependent on the size of the heap, the amount of live data, and the number and speed of available processors. The following are guidelines for heap sizes for server applications:
-
Allocate as much memory as possible to the JVM. The default size is often too small. Usually more heap memory is better for low pause collections. However, with huge heap sizes the GC pauses can become problematic.
-
Decide on the maximum heap size to allocate to the JVM. Then check the performance with different young generation sizes to find the best setting.
-
Increase the heap memory as the number of processors increases. This allows the use of a parallel garbage collector.
-
If the total heap size is fixed, then increasing the young generation size reduces the old generation size. Keep the old generation large enough to hold all the live data, plus some extra space (10% to 20% or more).
-
Bearing the previous constraints on the old generation in mind:
-
Allocate plenty of memory to the young generation.
-
Increase the young generation size as the number of processors increases.
-
If the current garbage collector doesn’t give us the performance we need, we should first adjust the heap and generation sizes to meet the goals. If performance is still too slow, then we should try a different collector. For example, we can use the concurrent collector to reduce pause-times. We can use the parallel collector to increase throughput on multiprocessor hardware.
Extra Reading
For more than you’ll probably ever need to know about garbage collection, see this [Java 18 article]
(https://docs.oracle.com/en/java/javase/18/gctuning/index.html) on the Oracle website.
This is the Java 11 version of the previous article, while this is the Java 8 version.
This is an excellent article by Jack Shirazi. Be sure to watch the associated Youtube video where he explains how to choose a garbage collector and tune it.
Until next week, stay safe and keep on learning! And don’t forget to share your thoughts and comments on this post.