即使 Xms = Xmx,G1GC 是否会向操作系统释放内存?

2022-01-16 00:00:00 garbage-collection java-11 java g1gc

在阅读了 this 和 JEP-346,我意识到 G1 确实会将内存释放回操作系统.

但是,它是否会将内存释放回操作系统,甚至到当前内存使用量可能低于初始堆内存(即在此 JEP 之前,在我的情况下为 JDK11)?

假设我有一个 Java 11 虚拟机运行 XmsXmx 设置为 5GB,在一个 8GBRAM,但是我只消耗 1GB 左右.G1 会释放足够的内存给操作系统吗?

我在任何地方都没有找到任何说明 G1 仅限于发布的文档,请牢记 Xms 阈值.

我在生产中观察到这一点,MemAvailable 一直下降到一个点,然后在 GC 之后,它在 8GB 盒子上跃升至接近 30-35%.所以我假设它正在释放内存,这就是 MemAvailable 跳回的原因.

另外,释放内存给操作系统到底是什么意思,是调用 free/unmap 吗?

解决方案

注意:我已经删除了我之前的答案并研究了源代码(也构建了我自己的 JVM 只是为了找出这个特定的时刻),这就是答案.

简短回答

JVM 11 版本(目前),当使堆变小时,不会低于 Xms.

长答案

绝对真理在源代码中.还有这里 决定是否收缩堆.下面几行,可以看到如果我们输入那个if,就会有一个log语句:

<块引用>

尝试堆收缩(Full GC 后容量高于最大所需容量).

所以本质上,如果我们能理解两个参数:capacity_after_gcmaximum_desired_capacity - 我们就能解开这个谜团.总的来说,capacity_after_gc 并不容易掌握;主要是因为这取决于有多少垃圾以及当前 GC 可以回收多少.为简单起见,我将编写一些不会产生任何垃圾的代码,以使该值保持不变.

在这种情况下,我们只需要了解maximum_desired_capacity.

上面几行,你可以看到这是计算为:

maximum_desired_capacity = MAX2(maximum_desired_capacity, min_heap_size);

不幸的是,这就是它变得棘手的地方,因为要真正了解这些人体工程学是如何设置的,需要遵循和理解大量代码;特别是因为它们依赖于 JVM 已启动的各种参数.

例如 min_heap_size 设置为:

<块引用>

//如果没有设置最小堆大小(通过-Xms),//与 InitialHeapSize 同步以避免默认值出错.

注意,他们甚至将 -Xms 称为 minimum;虽然文档说它是 initial.您还可以注意到它进一步取决于另外两个属性:

 合理的最小值,InitialHeapSize

这将很难进一步解释;这就是为什么我不会.相反,我将向您展示一些简单的证明(我确实完成了大部分代码......)

<小时>

假设你有这个非常简单的代码:

公共类 HeapShrinkExpand {公共静态 void main(String[] args) 抛出 InterruptedException {for (int i = 0; i <10; i++) {线程.sleep(500);System.gc();}}}

我运行它:

-Xmx22g-XX:初始堆大小=1g-Xlog:堆*=调试"-Xlog:gc*=调试"-Xlog:ergo*=调试"

在日志中,我会看到:

[0.718s][debug][gc,ergo,heap] GC(0) 尝试堆收缩(Full GC 后容量高于最大期望容量).容量:1073741824B 占用:8388608B 直播:1018816B maximum_desired_capacity:27962026B (70 %)[0.719s][debug][gc,ergo,heap] GC(0) 收缩堆.请求收缩量:1045779798B 对齐收缩量:1044381696B 尝试收缩量:1044381696B

这会告诉你一些关于需要缩小多少、当前容量是多少等的统计信息.下一行将显示堆实际上下降了多少:

[0.736s][debug][gc,ihop] GC(0) 目标占用更新:旧:1073741824B,新:29360128B

堆确实缩小了,降至 29MB 左右.

如果我添加一个 JVM 启动标志:-Xms10g,那些负责显示堆缩小到多少的 GC 日志;将不再存在.

事实上,如果我运行自己的 JMV(启用了一些日志记录),这两个值:capacity_after_gcmaximum_desired_capacity 将 总是具有相同的值;这意味着永远不会输入 if 语句,并且堆永远不会低于 -Xms.

<小时>

我已经使用 JDK-13 运行了相同的代码,并且虽然存在缩小日志(当 -Xms 作为参数给出时),但底层堆仍保留在 -Xms,仍然.我发现更有趣的是,在 java-13 下,尝试运行:

 -Xmx22g -Xms5g -XX:InitialHeapSize=1g

将正确错误输出:

<块引用>

指定的最小和初始堆大小不兼容

After reading some answers like this and JEP-346, I have realised that the G1 does release memory back to the OS.

However does it release memory back to the OS, even to the point that current memory use can drop below the initial heap memory (i.e before this JEP, in my case JDK11)?

Assume I have a Java 11 VM running with Xms and Xmx set as 5GB, on a 8GB RAM, however I am consuming only around 1GB. Will G1 release enough memory back to the OS?

I didn't find any documentation anywhere which says that the G1 is restricted to releasing keeping the Xms threshold in mind.

I am observing this in Production, the MemAvailable keeps on decreasing till a point, and then after a GC, it jumps upto close to 30-35% on an 8GB box. So I am assuming it is releasing memory, which is why the MemAvailable is jumping back up.

Also what does it exactly mean to release memory to the OS, is it calling free/unmap ?

解决方案

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

相关文章