Does Gc Release Back Memory to Os

Does G1GC release back memory to the OS even if Xms = Xmx?

Note: I have deleted my previous answer and have studied the sources (also build my own JVM just to find out this particular moment), here is the answer.

The short answer

JVM 11 version (at the moment), will not go below Xms when making the heap smaller.

The long answer

The absolute truth is in the source code. And here is the decision taken to shrink the heap or not. A few lines below, you can see that if we enter that if, there will be a log statement:

Attempt heap shrinking (capacity higher than max desired capacity after Full GC).

So in essence, if we can understand two parameters : capacity_after_gc and maximum_desired_capacity - we can solve this mystery. In general, capacity_after_gc is not something easy to grasp; mainly because it depends on how much garbage there was and how much the current GC could reclaim. For simplicity, I am going to write some code that does not generated any garbage, so that this value is constant.

In such a case, we only need to understand maximum_desired_capacity.

A few lines above, you can see that this is computed as :

maximum_desired_capacity =  MAX2(maximum_desired_capacity, min_heap_size);

Unfortunately, this is where it gets tricky, because it's a lot of code to follow and understand to really see how these ergonomics are set; especially since they depend on various arguments that the JVM has been started with.

For example min_heap_size is set as:

// If the minimum heap size has not been set (via -Xms),
// synchronize with InitialHeapSize to avoid errors with the default value.

Notice, that they even refer to -Xms as minimum; though the documentation says it's initial. You can also notice that it further depends on another two properties :

 reasonable_minimum , InitialHeapSize

This will be difficult to explain further; that is why I will not. Instead, I will show you some simple proof (I did go through the majority of that code...)


Suppose you have this very simple code:

public class HeapShrinkExpand {

public static void main(String[] args) throws InterruptedException {

for (int i = 0; i < 10; i++) {
Thread.sleep(500);
System.gc();
}
}
}

And I run it with:

-Xmx22g 
-XX:InitialHeapSize=1g
"-Xlog:heap*=debug"
"-Xlog:gc*=debug"
"-Xlog:ergo*=debug"

In logs, I will see:

[0.718s][debug][gc,ergo,heap   ] GC(0) Attempt heap shrinking (capacity higher than max desired capacity after Full GC). Capacity: 1073741824B occupancy: 8388608B live: 1018816B maximum_desired_capacity: 27962026B (70 %)
[0.719s][debug][gc,ergo,heap ] GC(0) Shrink the heap. requested shrinking amount: 1045779798B aligned shrinking amount: 1044381696B attempted shrinking amount: 1044381696B

This tells you some stats around how much shrinking is desired, what is the current capacity, etc. The next line will show you how much the heap has gone down, actually:

[0.736s][debug][gc,ihop] GC(0) Target occupancy update: old: 1073741824B, new: 29360128B

The heap did shrink, down to around 29MB.

If I add a single JVM start-up flag: -Xms10g, those GC logs that are responsible for showing how much the heap was shrank to; will not be present anymore.

And in fact if I run my own JMV (with some logging enabled), those two values: capacity_after_gc and maximum_desired_capacity will always have the same values; meaning that if statement will never be entered and heap will never go below -Xms.


I have run the same code with JDK-13 and, while the shrinking logs are present there (when -Xms is given as an argument), the underlying heap stays at the -Xms, still. What I find even more interesting is that under java-13, trying to run with:

 -Xmx22g -Xms5g -XX:InitialHeapSize=1g

will correctly error out with:

Incompatible minimum and initial heap sizes specified

GC1 does not release OS-Memory

Resource efficiency is a major concern when it comes to optimizing the performance and scalability of an application. In the Java world, one aspect that usually dominates resource concerns is memory usage - more specifically, the size of the Java heap.

In contrast to .NET and the CLR, the JVM is known for the need to size the heap at startup time without any chance of space reclamation, which would make resources available to other processes. Does that still hold true today? Let's find out.

The garbage collector is in charge of managing the heap and associated OS allocations. Since JDK 9, the G1 garbage collector has been the default GC. Generally speaking, the GC tries to avoid the deallocation of heap space (to give memory back to the OS) because that can lead to performance degradation if that very space is needed at a later point-in-time.

Heap shrinking depends on the collection cycle, which is comprised of the young-only and space reclamation phases.

The young-only phase happens if the associated eden space, where new objects are allocated initially, is exhausted. Reachable objects are evacuated (moved) to the survivor space. If the survivor space is exhausted, reachable objects are evacuated to another survivor space or the old generation (if objects are reachable or "survive" a given number of collections).

The space reclamation happens depending on the size of the old generation, controlled by the Initiating Heap Occupancy Percent (IHOP). The default (-XX:InitiatingHeapOccupancyPercent) is 45 percent, but the IHOP is adjusted by the GC to optimize the collection interval (known as "Adaptive IHOP"). This phase also handles the young generation, therefore it's called a "mixed collection".

According to the G1 source in JDK 9, shrinking is only done after full collections (explicit collection or not) and only in case specific thresholds are met. G1CollectedHeap::do_full_collection calls G1CollectedHeap::resize_if_necessary_after_full_collection, which handles the shrinking process and generates the following GC log output:

log_debug(gc, ergo, heap)(
"Shrink the heap. requested shrinking amount: "
SIZE_FORMAT "B aligned shrinking amount: "
SIZE_FORMAT "B attempted shrinking amount: "
SIZE_FORMAT "B",
shrink_bytes, aligned_shrink_bytes, shrunk_bytes);

The G1 GC aims to

[...] provide the best balance between latency and throughput [...]

G1 attempts to defer garbage collections for as long as possible and tries to avoid full garbage collections in general. For details, see Garbage Collection Cycle.

[...] if the application runs out of memory while gathering liveness information, G1 performs an in-place stop-the-world full heap compaction (Full GC) like other collectors [...]

The collector only performs full collections in case of space exhaustion unless triggered manually (e.g. using java.lang.System.gc()). That's when heap shrinking can happen.

According to the G1 source in JDK 15, shinking is done for full collections as well as concurrent mark-and-sweep collections. G1FullCollector::complete_collection calls G1CollectedHeap::prepare_heap_for_mutators which in turn calls G1CollectedHeap::resize_heap_if_necessary. The actual shrinking process and log message is identical to JDK 9. In contrast to JDK 9, JDK 12 and higher also performs shrinking during the so-called re-mark phase (G1ConcurrentMark::remark calls G1CollectedHeap::resize_heap_if_necessary), in the context of the normal collection cycle.

The change was introduced with 08041b0d7c08.

A straightforward means of monitoring is -verbose:gc. Use -Xlog:gc=debug to see the log output mentioned above in case of shrinking.

So, despite popular belief, the JVM does return memory to the OS. Until JDK 11, heap shrinking was only triggered after full collections, given the change is significant enough to warrant that action without affecting performance. With JDK 12+ (checked until 15), heap shrinking is also triggered during the normal collection cycle, making it more likely for applications that typically do not involve full collections to see heap shrinkage.

Releasing memory back to OS when JVM is idle

If calling System.gc() fulfills your needs, I would recomend to use spring scheduler to run a periodic task every x sedonds.

This is quite easy to implement, some annotations

@EnableAsync
@EnableScheduling
@Scheduled(cron = "...")

is all you need.
See spring scheduling for details.

Edit

Calling System.gc() gives only suggests to start the garbage collection, its still up to the JVM to decide when to do it or not.

To find out, if your system is idle or not, you could use the spring metrics.
There are some sub classes of

org.springframework.boot.actuate.endpoint.PublicMetrics

like TomcatPublicMetrics or SystemPublicMetrics that give you information about the system.
You can get them injected using @Autowire and call mertics() to get single values. Based on that you might be able to decide, if your system is idle or not,

How does Garbage Collector frees up memory before JVM exits?

The GC does NOT free up memory before the JVM exits.

What actually happens is that when the JVM exits, the process that the JVM runs in also exits1. When the process exits, the operating system reclaims all of the RAM and swap pages that the process was using (exclusively) and reclaims its virtual memory page tables.

If your Java program (or C / C++ program) has memory leaks, that doesn't matter once the process is cleaned up. The OS knows at all time which RAM / swap pages belong to which process. Nothing is lost.


1 - If the JVM was started by a C / C++ application using (say) JNI, then the process won't necessarily exit at that point. It would then be up to the C / C++ application to figure out what to do. But the GC doesn't clean up in this case either.

Release Java Heap Sapce

I've seen this trend too while doing the exact same thing that you did (and see here). The explanation is quite simple: JVM will not give back memory to the OS, even if it has lots of it free. I don't know the exact details of why this is so, but it has to do with the fact that asking memory back from the OS is expensive (this is ultra-simplified).

So I guess that the simple answer here is that - this is normal.

Here it is! I found where I have actually read about this.



Related Topics



Leave a reply



Submit