Why JVM does not release unused memory eagerly, a discussion

Why JVM does not release unused memory eagerly, a discussion

If you do a quick search on the Internet with keywords like “Java doesn’t release memory”, “JVM no heap release” and so on, you will be overwhelmed by the results. Some of the explanations are good and some are poor or outdated. In this post, we discuss the phenomena of why JVM does not proactively release unused memory eagerly.

In order to understand this issue, first I discuss the general practices of memory allocation and how it is implemented in JVM initially. Then I discuss available options to force JVM to release the allocated memory. And lastly, I conclude this post with the idea of whether it is a good idea to practice JVM memory release proactively.

How general memory allocation works

To understand this issue first we need to understand the constraints of memory allocation in general and look into how it is implemented in JVM. Then we examine available options to force JVM to release the allocated memory. Lastly, we conclude this post with the idea of whether it is a good idea to practice JVM memory release proactively.

Memory allocation constraints/considerations

There are multiple considerations to keep in mind regarding memory allocation in general.

1. Asking OS for memory is an expensive process

Communicating with the OS to ask for more memory is an expensive procedure. It does not make sense to release the memory immediately after a memory-intensive cycle. Since the process does not know when more memory will be needed again the safest approach is to keep the memory in the case that is needed immediately to reduce the overhead of communicating with the OS. The “when” concept could mean a second, a minute, or an hour from a point in time. 

2. Non-contiguous memory space and heap resizing

Memory is not used continuously. It will be fragmented over time when different processes start and end, attributed to allocating and releasing memory. That is a core aspect behind Virtual/Logical Memory Allocation to deal with non-contiguous physical memory spaces.

Additionally, there is no guarantee that a process even writes to the logical memory addresses contiguously. Take the C programming linked list as an instance. It works in a non-contiguous manner by the contrast of arrays. That creates memory fragmentation over time. Hence, if a process has to release a part of its allocated memory, the prerequisite is to resize its heap and then release a chunk of free memory to the OS. As you may have guessed by now, that procedure is CPU intensive. Since each process’s memory space is protected (avoiding processes stepping in each other memory space) increases the predicament for OS to utilize the freed memory.

Even underlying low-level free functions are not implemented in such a way as to free memory immediately.

3. Safety matters

The third reason is the matter of safety. It is safer to get needed memory once at the start and then manage it internally rather than each time calling a syscall to get memory which does not guarantee the allocation of the required memory.

Therefore, it does not make sense for a process to release the free memory frequently especially when the process is busy or could get busy at any moment and doesn’t know when it needs more memory again.

Could the OS run out of memory?

Now the question is if all processes ask for memory and don’t release it proactively does not that cause the OS to crash? The answer for modern operating systems is “no”. If an OS detects that RAM memory becomes sparse, it swaps parts of the memory content to disk. Then on page faults, the operating system reloads the needed pieces into the RAM again.

How does JVM release the free memory?

Now that you know the underlying memory allocation concept and considerations, let’s focus solely on JVM memory management.

Similar to other processes, JVM follows the same practice more or less. Matter of fact, the designers of JVM decided to sacrifice memory for stability. The reason for such a decision is simple. When JVM faces memory issues, it cannot recover from memory outages. If a JVM runs out of resources, it often enters into an unpredictable state that is resolvable only by restarting the JVM.

On the other hand, there are some mechanisms to force JVM to release parts of its free memory automatically in a proactive manner, especially with recent ongoing garbage collection enhancements since JDK 8. We discover those options in the next section.

Forcing JVM to release memory

Forcing JVM to release free memory back to OS highly depends on the JVM implementation and its version. Some JVM implementations do not release free memory by default unless are forced to, while some do that reluctantly, and others do it proactively.

In this section, we only cover two JVM implementations, Oracle HotSpot (version 8, 9, 12) and Eclipse OpenJ9. Finally, we discuss how to force JVM to return free memory to the OS programmatically.

Oracle HotSpot

HotSpot in JDK 8

By default, HotSpot JVM returns the free memory to the OS but it does that very reluctantly. That is because it requires JVM to resize the heap memory, a CPU-intensive process. That does not mean one cannot force JVM to return committed unused memory back to OS by manipulating Garbage Collector (GC) flags.

That can be achieved by tweaking JVM parameters to force GC to run more frequently, which will be resulted in returning some of the free memory to the OS. We can increase the GC cycle and then constrain the allocated-but-unused heap size as follows,

-XX:GCTimeRatio=19 -XX:MinHeapFreeRatio=20 -XX:MaxHeapFreeRatio=30

If you use a concurrent GC, you can adjust heap occupancy percent to something low to force GC to run concurrent collection almost always. Like this:

-XX:InitiatingHeapOccupancyPercent=10

Keep in mind GC like (CMS and G1) will pause the application if they perform a major GC, even though it can be switched off. The recommendation is to avoid applying these parameters lightly.

HotSpot in JDK 9

JDK 9 added a new option with the goal of the Java heap being resized immediately to the targeted size without requiring multiple GCs. This obviously comes at the cost of some performance degradation when GC is running. The option is:

-XX:-ShrinkHeapInSteps

According to the related ticket, here, the option initially proposed was -XX:+UseAggressiveHeapShrink.

Hotspot in JDK 12

In JDK 12, there is a new JDK Enhancement Proposal, here, to compel G1 to return unused committed heap memory to the OS eagerly. That is conditioned to when the application is running on a low load.

The enhancement shipped with JDK 12 on 2018-12, but it is not possible to control it by setting flags. For more technical details, check here.

OpenJ9

Eclipse OpenJ9 JVM provides an option, similar to JDK 12, to automatically detect the application load and run GC to free up the memory if the application is idle. This option has been long integrated into OpenJ9 and can be easily activated by using this flag:

–XX:+IdleTuningGcOnIdle

It is also possible to set the minimum application idle time using this flag:

-XX:IdleTuningMinIdleWaitTime

More details about OpenJ9 GC can be found in the following links:

Manual GC option

Some articles on the internet suggest running System.gc() manually either on a timely basis or when the application is idle. Well, that is technically possible. However, it is a poor idea. It introduces unnecessary complexities to the application layer and breaks the single responsibility principle. That means the application has to be aware of low-level operating system operations. That should not be a part of any application responsibility. Hence, we strongly recommend against using System.gc() in any application.

Verdict

In this article we discussed why JVM does not release unused memory eagerly. With recent developments, different JVM implementations are more memory savvy. They provide options to release unused memory proactively. However, that does not mean cases such as automatic downscaling will always work based on OS memory usage if you run your apps in a cluster designed to scale down based on memory usage.

Even though JDK 8 may release some heap memory, it does it so reluctantly to be tangible. Therefore, the only reaming approach in JDK 8 is to tweak some JVM parameters to enforce running GC more frequently even when a small percentage of the heap is occupied. JDK 9 has an option to set which affects the application performance. Finally, JDK 12 and above plus OpenJ9 offer better solutions which are to detect whether the application is idle and run a major GC to release the committed unused memory back to the OS.

The recommendation is if you are running JDK 8 or older, you better don’t do anything. It is not worth jeopardizing your application’s stability over a minor memory enhancement, especially for small and medium-sized applications. If you intend to do it, make sure you have sufficient knowledge about JVM memory management or consult with a JVM expert. The same advice is generally applicable to the newer versions of JDK.

For JDK 12 and above plus OpenJ9, you should still be cautious and analyze the application behavior over a long time to assess potential side effects.

Useful resources