OpenJDK JVM是否会将堆内存还回给Linux?

57

我们有一个长时间运行的服务器进程,在短时间内偶尔需要大量RAM。我们发现一旦JVM从操作系统获取了内存,就不会将其退回给操作系统。如何要求JVM将堆内存返回给操作系统?

通常,对于这种问题的接受答案是使用-XX:MaxHeapFreeRatio-XX:MinHeapFreeRatio。(例如,参见1234)。但我们是这样运行Java的:

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

但在VisualVM中仍然可以看到以下内容:

Visual VM memory usage

显然,JVM没有遵守-XX:MaxHeapFreeRatio=50,因为heapFreeRatio非常接近100%,远离50%。无论点击多少次“执行GC”,都不能将内存返回给操作系统。

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。所有版本都表现相似,没有将内存释放回操作系统。

我认为我需要这个功能,交换和分页并不能“解决”这个问题,因为让磁盘IO接近2GB的垃圾进出分页只是浪费资源。如果您有不同看法,请启迪我。

我还编写了一个带有malloc()/free()的小型memoryUsage.c程序,并且它确实将内存返回给了操作系统。所以在C语言中是可以实现的。或许在Java语言中不行?

编辑:Augusto指出,搜索会让我找到-XX:MaxHeapFreeRatio-XX:MinHeapFreeRatio,只有在使用-XX:+UseSerialGC时才能正常工作。我非常兴奋,尝试了一下,惊异于我自己居然没能找到这个选项。是的,它确实可以在我的MemoryUsage.java上工作:

-XX:+UseSerialGC working with simple app

然而,当我尝试将-XX:+UseSerialGC应用于我们的真实应用程序时,它并没有起到作用:

-XX:+UseSerialGC not working with real app

我发现在一段时间后运行gc()确实有所帮助,因此我创建了一个线程,执行了以下操作:

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

这就解决了:

GC thread working

之前我曾在多次尝试中使用过 -XX:+UseSerialGC 和多个 System.gc() 调用来解决问题,但不喜欢需要 GC 线程的方式。而且随着我们的应用程序和 java 的发展,谁知道它是否会继续工作。一定有更好的方法。

是什么逻辑强制我要调用 System.gc() 四次(但不是立即),这些内容在哪里记录?

为了寻找关于 -XX:MaxHeapFreeRatio-XX:MinHeapFreeRatio 只能与 -XX:+UseSerialGC 配合使用的文档,我阅读了java 工具/可执行文件的文档,但未提到 -XX:MaxHeapFreeRatio-XX:MinHeapFreeRatio 仅在使用 -XX:+UseSerialGC 时才有效。事实上,已经修复的问题 [JDK-8028391] Make the Min/MaxHeapFreeRatio flags manageable 表示:

为了让应用程序控制何时以及何时允许更多或更少的 GC,应使标志 -XX:MinHeapFreeRatio 和 -XX:MaxHeapFreeRatio 可管理。这些标志的支持还应该在默认并行收集器中实现。

已修复问题的注释表示:

作为自适应大小策略的一部分,这些标志的支持也添加到了 ParallelGC 中。

我已经检查过,在我使用的 openjdk-8 版本的源软件包 tarball中,参考已修复的问题补丁确实包含在其中。因此,它应该可以在“默认并行收集器”中工作,但是根据我在这篇文章中所示的情况,它并没有。我还没有找到任何文档表明它只能与 -XX:+UseSerialGC 配合使用。正如我在此处记录的那样,即使如此也不可靠。

难道我不能只通过简单地调整 -XX:MaxHeapFreeRatio-XX:MinHeapFreeRatio 而无需经过所有这些麻烦吗?


2
搜索引擎是你的好朋友 - 答案是“取决于你选择的GC算法”:在运行时释放Java内存以供操作系统使用。 - Augusto
1
只是提醒一下...调用gc()不会执行JVM垃圾回收器,它只会建议JVM执行。它可能会执行,也可能不会执行... 另外一件事是:为了让JVM GC正常工作,你必须编写正确的代码。我的意思是:注意使用重量级对象,比如StringBuffer、可克隆对象、不必要的实例、循环内部连接字符串、单例模式等等...所有这些东西都会增加堆空间。 - Lucas
1
垃圾回收和释放堆到操作系统是两个不同的操作。堆是线性的;释放堆空间需要一个专用堆变得完全空或者对堆进行非常昂贵的碎片整理操作。注意:我不是Java专家,但我了解在非托管代码中的堆使用情况。 - Brian A. Henning
1
虽然这可能是一般情况下的真相,但在我们的情况下,我们会快速分配大量内存,然后很快释放(大部分)内存。可以看出,使用正确的指令(例如 -XX:+UseSerialGC 和一个线程),这是可能的。我看到了四种可能性:1)承认 -XX:MaxHeapFreeRatio-XX:MinHeapFreeRatio 存在缺陷,并且不能按照广告所述工作。2)修复它们的实现。3)认识到无论出于什么原因,它们永远无法被正确实现,并将其弃用/删除。4)更改文档以准确反映我的期望。 - Peter V. Mørch
1
@ErickG.Hagstrom:我一开始的问题是“我们如何要求JVM将堆内存返回给操作系统?” 我想答案似乎是“你不能 - -XX:MaxHeapFreeRatio-XX:MinHeapFreeRatio的效果不像广告中宣传的那样”。 - Peter V. Mørch
显示剩余15条评论
1个回答

7
G1(-XX:+ UseG1GC),Parallel scavenge(-XX:+ UseParallelGC)和ParallelOld(-XX:+ UseParallelOldGC)在堆缩小时会返回内存。我不确定Serial和CMS是否会在我的实验中缩小它们的堆。并行收集器都需要多次垃圾回收才能将堆缩小到“可接受”的大小。这是按设计来的。它们有意保留堆,认为未来会需要它。设置标志-XX:GCTimeRatio = 1会在一定程度上改善情况,但仍需要进行多次垃圾回收才能大幅缩小堆。G1非常擅长快速缩小堆,因此对于上述用例,我会说使用G1并在释放所有缓存和类加载器等后运行 System.gc()可以解决问题。

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


1
谢谢@Sammy,提供的链接很有用。我已经试过了,确实,-XX:+UseG1GC 对于我的 MemoryUsage.java 示例是有效的。然而,无论是 -XX:+UseParallelGC 还是 -XX:+UseParallelOldGC 都没有使堆缩小。我对 -XX:+UseG1GC 可以可靠地在我们真实的应用程序中工作的信心不高。虽然您在技术上回答了我的问题,但我会接受它。然而,我确实认为真正的答案是“好吧,要么是 -XX:MaxHeapFreeRatio-XX:MinHeapFreeRatio 的实现或文档完全出错了”+ 一个新的错误报告链接。 - Peter V. Mørch
1
我清楚地记得在JavaOne大会上,GC团队被问及这个问题,答案始终如一,即Max/MinHeapFreeRatio标志只是对jvm的提示,没有更多也没有更少。大多数GC操作只是尽力而为,不提供任何保证。这是这些标志在-XX下公开的原因之一(而不是-X或仅-)。正如我们所知道的那样,-XX实际上是内部不受支持的标志,Sun/Oracle jvm开发人员创建它们以公开某些行为,这些行为仅在特定条件下有效,有时仅在特定平台和实现中有效。 - M.Z
1
CMS在进行完整的STW收集时应该会这样做,但由于其设计目标是尽可能避免STW收集,因此它不能可靠地及时将未使用的内存返回给操作系统。 - Perkins
我曾经遇到过一个类似的问题,是在OpenJDK中运行的应用程序。在运行大任务后,操作系统内存(win)会达到9GB,并且永远不会缩小。 我添加了useG1GC参数,操作系统内存从未超过700M,并且该进程运行速度提高了几个百分点。 - CasaDelGato

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接