The Garbage Collector (GC) handles the memory management in Java. As a result, programmers don’t need to explicitly take care of memory allocation and deallocation.
In Java, JVM reserves a certain amount of memory at the beginning. Sometimes, the actual memory used is significantly less than the reserved amount. In such scenarios, we prefer to return the excess memory to OS.
This entire process is dependent on the algorithms used for garbage collection. Consequently, we can choose the type of GC and JVM as per the required behavior.
In this tutorial, we'll explore memory management by GC and its interaction with OS.
2. JVM Memory Organization
When JVM is initialized, different types of memory areas are created inside it, such as Heap area, Stack area, Method Area, PC Registers, and Native Method Stack.
GC deals with heap storage. Hence, we’ll focus on memory interaction related to the heap in this article.
We can specify the initial and maximum heap sizes using the flags -Xms and -Xmx, respectively. If -Xms is lower than -Xmx, it implies that JVM hasn't committed all the reserved memory to the heap in the beginning. In short, heap size starts from -Xms and can expand up to -Xmx. This allows a developer to configure the size of the required heap memory.
Now, when the application runs, different objects are allocated memory inside the heap. At the time of garbage collection, GC deallocates the unreferenced objects and frees the memory. This deallocated memory is currently a part of the heap itself, as it is a CPU intensive procedure to interact with OS after each deallocation.
Objects reside in a scattered manner inside the heap. GC needs to compact the memory and create a free block to return to OS. It involves an additional process execution while returning the memory. Also, Java applications might need additional memory at a later stage. For this, we need to communicate with the OS again to request more memory. Moreover, we can’t ensure the availability of memory in the OS at the requested time. Hence, it is a safer approach to use the internal heap instead of frequently calling the OS to fetch memory.
However, if our applications don’t require entire heap memory, we’re just blocking the available resources, which could have been used by the OS for other applications. Considering this, the JVM has introduced efficient and automated techniques for memory release.
3. Garbage Collectors
Progressing over different release versions, Java has introduced different types of GCs. Memory interaction between heap and OS is dependent on the JVM and GC implementations. Some GC implementations actively support heap shrinking. Heap shrinking is the process of releasing back the excess memory from heap to OS for optimal resource usage.
For example, Parallel GC doesn’t release unused memory back to the OS readily. On the other hand, some GCs analyze the memory consumption and determine accordingly to release some free memory from the heap. G1, Serial, Shenandoah, and Z GCs support heap shrinking.
Let’s explore these processes now.
3.1. Garbage First (G1) GC
G1 has been the default GC since Java 9. It supports compaction processes without lengthy pauses. Using internal adaptive optimization algorithms, it analyzes the RAM required as per application usage and uncommits the memory if required.
Initial implementations support heap shrinking either after full GC or during concurrent cycle events. However, for an ideal situation, we want to promptly return the unused memory to the OS, especially for the periods when our application is idle. We want the GC to dynamically adapt to the memory usage by our applications at runtime.
Java has included such capabilities in different GCs. For G1, JEP 346 introduces these changes. From Java 12 and higher, heap shrinking is also possible in the concurrent remark phase. G1 tries to analyze the heap usage when the application is idle and triggers the periodic garbage collection as needed. G1 can either start a concurrent cycle or a full GC based on the G1PeriodicGCInvokesConcurrent option. After the cycle executes, G1 needs to resize the heap and return freed memory back to OS.
3.2. Serial GC
Serial GC also supports heap shrinking behavior. In comparison to G1, it requires additional four full GC cycles to uncommit freed memory.
3.4. Shenandoah GC
Shenandoah is a concurrent GC. It performs the garbage collection asynchronously. Eliminating the need for full GC greatly helps in the performance optimization of the application.
4. Using JVM Flags
We've previously seen that we can specify heap sizes using JVM command-line options. Similarly, we can use different flags to configure the default heap shrinking behavior of a GC :
- -XX:GCTimeRatio: To specify the desired time split between application execution and GC execution. We can use it to make the GC run longer
- -XX:MinHeapFreeRatio: To specify the minimum expected proportion of free space in the heap after garbage collection
- -XX:MaxHeapFreeRatio: To specify the maximum expected proportion of free space in the heap after garbage collection
If the available free space in the heap is higher than the ratio specified using -XX:MaxHeapFreeRatio option, then GC can return the unused memory to the OS. We can configure the value of the above flags to constrain the amount of unused memory in the heap. We’ve similar options available for concurrent garbage collection processes:
- -XX:InitiatingHeapOccupancyPercent: To specify the heap occupancy percentage required to start a concurrent garbage collection.
- -XX:-ShrinkHeapInSteps: To reduce the heap size to the -XX:MaxHeapFreeRatio value immediately. The default implementation requires multiple garbage collection cycles for this process.
In this article, we've seen that Java provides different types of GCs catering to different requirements. GC can reclaim and return the free memory to the host OS. We can choose the type of GC as per our requirements.
We've also explored the use of JVM parameters to tweak the GC behavior to reach desired performance levels. Additionally, we can opt for dynamic scaling of memory utilization by JVM. We should consider the trade-offs related to each chosen option for our application and the resources involved.