OpenJDK JVM 会把堆内存还给 Linux 吗?

Will OpenJDK JVM ever give heap memory back to Linux?

我们有一个长期存在的服务器进程,它很少在短时间内需要大量 RAM。我们看到,一旦 JVM 从 OS 获取内存,它 永远不会 return 将其返回 OS。我们如何要求 JVM 将 return 堆内存返回到 OS?

通常,此类问题的公认答案是使用 -XX:MaxHeapFreeRatio-XX:MinHeapFreeRatio。 (参见例如 1,2,3,4)。但是我们 运行 java 是这样的:

java -Xmx4G -XX:MaxHeapFreeRatio=50 -XX:MinHeapFreeRatio=30 MemoryUsage

并且仍然在 VisualVM 中看到这个:

很明显,JVM 没有遵守 -XX:MaxHeapFreeRatio=50,因为 heapFreeRatio 非常接近 100%,离 50% 还很远。没有多少点击"Perform GC" return的内存到OS.

MemoryUsage.java:

import java.util.ArrayList;
import java.util.List;

public class MemoryUsage {

    public static void main(String[] args) throws InterruptedException {
        System.out.println("Sleeping before allocating memory");
        Thread.sleep(10*1000);

        System.out.println("Allocating/growing memory");
        List<Long> list = new ArrayList<>();
        // Experimentally determined factor. This gives approximately 1750 MB
        // memory in our installation.
        long realGrowN = 166608000; //
        for (int i = 0 ; i < realGrowN ; i++) {
            list.add(23L);
        }

        System.out.println("Memory allocated/grown - sleeping before Garbage collecting");
        Thread.sleep(10*1000);

        list = null;
        System.gc();

        System.out.println("Garbage collected - sleeping forever");
        while (true) {
            Thread.sleep(1*1000);
        }
    }
}

版本:

> java -version
openjdk version "1.8.0_66-internal"
OpenJDK Runtime Environment (build 1.8.0_66-internal-b01)
OpenJDK 64-Bit Server VM (build 25.66-b01, mixed mode)

> uname -a
Linux londo 3.16.0-4-amd64 #1 SMP Debian 3.16.7-ckt11-1+deb8u5 (2015-10-09) x86_64 GNU/Linux

> lsb_release -a
No LSB modules are available.
Distributor ID: Debian
Description:    Debian GNU/Linux 8.2 (jessie)
Release:    8.2
Codename:   jessie

我也尝试过 OpenJDK 1.7 和 Sun Java 的 1.8。所有行为都相似 none 把记忆还给 OS.

我确实认为我需要这个,而交换和分页不会 "solve" 这个,因为将磁盘 IO 用于分页接近 2GB 的垃圾进出只是浪费资源。如有异议,请赐教。

我也用malloc()/free()写了一点memoryUsage.c,它确实return记忆OS。所以在 C 中是 possible。也许 Java?

编辑:Augusto 指出搜索会引导我到 -XX:MaxHeapFreeRatio,而 -XX:MinHeapFreeRatio 只适用于 -XX:+UseSerialGC。我欣喜若狂并尝试了一下,对我自己没有发现这一点感到困惑。是的,它确实适用于我的 MemoryUsage.java:

然而,当我尝试 -XX:+UseSerialGC 使用我们的真实应用程序时,并没有那么多:

一段时间后我发现 gc() 确实有 帮助,所以我创建了一个或多或少的线程:

while (idle() && memoryTooLarge() && ! tooManyAttemptsYet()) {
    Thread.sleep(10*1000);
    System.gc();
}

成功了:

实际上我之前在我的许多实验中的一些实验中看到过 -XX:+UseSerialGC 和多个 System.gc() 调用的行为,但不喜欢 GC 线程的需要。谁知道这是否会随着我们的应用程序和 java 的发展而继续发挥作用。一定有更好的办法。

迫使我调用 System.gc() 四次(但不是立即)的逻辑是什么,这些东西记录在哪里?

在搜索 -XX:MaxHeapFreeRatio-XX:MinHeapFreeRatio 仅使用 -XX:+UseSerialGC 的文档时,我读到 the documentation for the java tool/executable and it isn't mentioned anywhere that -XX:MaxHeapFreeRatio and -XX:MinHeapFreeRatio only works with -XX:+UseSerialGC. In fact, the fixed issue [JDK-8028391] Make the Min/MaxHeapFreeRatio flags manageable 说:

To enable applications to control how and when to allow for more or less GC, the flags -XX:MinHeapFreeRatio and -XX:MaxHeapFreeRatio should be made manageable. Support for these flags should also be implemented in the default parallel collector.

A comment 对于已解决的问题说:

Support for these flags have also been added to the ParallelGC as part of the adaptive size policy.

我已经检查过 patch referenced in the fixed issue backported to openjdk-8 is indeed contained in the source package tarball 我正在使用的 openjdk-8 版本。所以它显然应该在 "the default parallel collector" 中工作,但并不像我在这个 post 中所展示的那样。我还没有找到任何说明它只能与 -XX:+UseSerialGC 一起使用的文档。正如我在此处记录的那样,即使这样也是 unreliable/dicey.

我不能让 -XX:MaxHeapFreeRatio-XX:MinHeapFreeRatio 履行他们的承诺,而不必经历所有这些麻烦吗?

G1 (-XX:+UseG1GC), Parallel scavenge (-XX:+UseParallelGC) and ParallelOld (-XX:+UseParallelOldGC) do return memory when the heap shrinks. I'm not so sure about Serial and CMS, they didn't shrink their heap in my experiments.

Both parallel collectors do require a number of GCs before shrinking the heap down to an "acceptable" size. This is per design. They are deliberately holding on to the heap assuming that it will be needed in the future. Setting the flag -XX:GCTimeRatio=1 will improve the situation somewhat but it will still take several GCs to shrink a lot.

G1 is remarkably good at shrinking the heap fast, so for the usecase described above I would say it's solvable by using G1 and running System.gc() after having released all caches and class loaders etc.

http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6498735