GC是否将内存释放回操作系统?

69
当垃圾回收机制运行并释放内存时,这些内存会返回操作系统吗?还是作为进程的一部分被保留?我一直坚信这些内存实际上从未返回到操作系统,而是作为内存区域/池的一部分被同一进程重新使用。
因此,一个进程的实际内存永远不会减少。提醒我的是一篇文章,Java的Runtime是用C/C++编写的,所以我想应该也适用于Java?
更新:我的问题是关于Java的。我提到C/C++,因为我假设Java的分配/释放是由JRE使用某种malloc/delete形式完成的。

2
即使在C或C++中使用标准的堆分配方法也不必释放回到操作系统,操作系统可以将新释放的页面映射给该进程自身(因此看起来并未被释放)。 - Some programmer dude
到目前为止,没有将堆内存释放给操作系统。只有在尚未达到最大堆大小时才会占用额外的内存。这与C语言相同,不同之处在于C语言存在额外的问题,即内存碎片化会稍微妨碍内存重复利用(虽然C语言可以进行自己的内存分配管理)。 - Joop Eggen
1
JVM启动时有一个最小堆大小,我们可以将最大和最小堆大小设置为相同的值。如果最大和最小堆大小相同,则会预先分配JAVA堆,并且实际上可能只有很少的Java对象使用此堆,在这种情况下,GC不会将内存释放回操作系统。 GC实际上是通过回收未引用对象使用的内存来声明回收内存,以便它们可以用于其他对象。它不能直接释放内存回到操作系统。它可以通过从C库调用free来释放内存。但是malloc也可能在内部不会立即将内存释放给操作系统。 - Nipun Talukdar
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Jim
这意味着应用程序不能太贪心。Java应用程序可以使用较小的堆开始。如果操作系统检测到RAM内存变得稀缺,它将把部分内容交换到磁盘上,并在页面错误时逐个重新加载它们。请注意,每个进程的内存都是所有者并且受到保护的。因此,管理较小的内存区域将会很困难。 - Joop Eggen
显示剩余12条评论
6个回答

98

HotSpot JVM 会将内存释放回操作系统,但它不太愿意这样做,因为调整堆大小是昂贵的,而且如果你曾经需要过这个堆,就会认为你将再次需要它。

一般来说,收缩能力和行为取决于选择的垃圾收集器、JVM 版本,因为缩小能力通常是在稍后的版本中引入的,在添加 GC 本身之后很长时间。有些收集器可能还需要传递其他选项以选择缩小。而有些则很可能永远不支持,例如 EpsilonGC。因此,如果需要堆缩小,应该针对特定的 JVM 版本和 GC 配置进行测试。

JDK 8 及更早版本

这些版本没有明确的即时内存回收选项,但您可以通过设置 -XX:GCTimeRatio=19 -XX:MinHeapFreeRatio=20 -XX:MaxHeapFreeRatio=30 让 GC 更加积极地回收内存,并在 GC 循环后限制已分配但未使用的堆内存量。

如果您正在使用并发收集器,您还可以设置-XX:InitiatingHeapOccupancyPercent=N,其中N为较低的值,以使GC几乎持续进行并发收集,这将消耗更多CPU周期但更快地缩小堆。这通常不是一个好主意,但对于某些具有大量空余CPU核心但内存不足的机器类型可能是有意义的。
如果您正在使用G1GC,请注意它只能在jdk8u20中获得在堆的中间放回未使用的块的能力,早期版本只能在堆的末尾返回块,这会对可以回收的数量产生重大限制。
如果您正在使用具有默认暂停时间目标(例如CMS或G1)的收集器,则还可以放松该目标以对收集器施加较少的约束,或者您可以切换到并行收集器以优先考虑占用空间而不是暂停时间。
要验证是否发生了收缩或诊断GC为什么决定不进行收缩,您可以使用GC Logging和-XX:+PrintAdaptiveSizePolicy,这也可能提供见解,例如当JVM尝试使用更多内存来满足一些目标时。

JDK 9

添加了-XX:-ShrinkHeapInSteps选项,可以更积极地应用前面提到的选项引起的收缩。相关OpenJDK bug

对于日志记录,-XX:+PrintAdaptiveSizePolicy已被-Xlog:gc+ergo替换。

JDK 12

通过G1PeriodicGCIntervalJEP 346)引入了启用G1GC的及时内存释放选项,但会牺牲一些额外的CPU。该JEP还提到了ShenandoahOpenJ9 VM中的类似功能。

JDK 13

添加类似于ZGC的行为,但此时默认情况下启用。此外,XXSoftMaxHeapSize对于某些工作负载可能有所帮助,使平均堆大小保持在某个阈值以下,同时仍允许瞬态峰值。


12

JVM(Java虚拟机)在某些情况下确实会释放内存,但出于性能考虑,当某些内存被垃圾回收时,并不总是发生释放。这还取决于JVM、操作系统、垃圾收集器等因素。您可以使用JConsole、VisualVM或其他分析工具观察应用程序的内存消耗。

此外,请参阅相关的错误报告


1
我认为你从这些Java内存工具中看到的是在Java进程空间中释放的内存。但这并不意味着操作系统会收回内存并将其分配给另一个应用程序。 - Jim
1
您还可以查看总堆大小,但如果您不信任Java工具,也可以使用本地的操作系统级别的工具。 - lbalazscs
请纠正我,但我真的想不起来有一个Java进程在任务管理器中会缩小的情况。但是我在监控方面没有太多经验。 - Jim
2
我刚刚进行了确认,确实发现它释放了一些内存!在Windows上,我喜欢使用vmmap应用程序(这是从微软免费下载的),它可以让你看到有关进程内存使用情况的许多有趣信息。 - lbalazscs

9

如果您使用G1收集器并偶尔调用System.gc()(我每分钟调用一次),Java会可靠地缩小堆并将内存归还给操作系统。

自Java 12以来,如果应用程序处于空闲状态,G1会自动执行此操作。

我建议结合上述建议使用这些选项以获得非常紧凑的驻留进程大小:

-XX:+UseG1GC -XX:MaxHeapFreeRatio=30 -XX:MinHeapFreeRatio=10

我使用这些选项已经几个月了,它们与大型应用程序(一个基于Java的客户操作系统)一起使用,可以动态地加载和卸载类 - Java进程几乎总是保持在400到800 MB之间。


1
@StefanReich,我认为这不正确,“初始堆”和“最大堆”可以设置为相同的值,例如,应用程序可能消耗更少的内存。 - Eugene
该应用程序无法消耗更少,因为Xms设置了最小值。 - Stefan Reich
1
@StefanReich 1) 你必须通过@标记我,否则我不知道你在回复什么 2)Xms不是最小内存,而是初始内存,因此你是错误的。 - Eugene
1
@Eugene 很棒的分析!所以问题似乎更多地出在-Xms的文档上,现在越来越多的人(包括开发GC的人)说它最小堆值? - Matthieu
1
@Matthieu 特别是因为已经有了 InitialHeapSize,这使得情况更加混乱。 - Eugene
显示剩余12条评论

3

这篇文章在这里解释了Java 7中GC是如何工作的。简而言之,有许多不同的垃圾收集器可用。通常情况下,内存被保留给Java进程,只有一些GC会在请求时将其释放给系统。但是,Java进程使用的内存不会无限增长,因为Xmx选项(通常为256m,但我认为它取决于操作系统/机器)定义了上限。


1
如果堆有上限,但是堆的未使用部分最终被操作系统回收以供其他进程使用,则 OP 不会更新。 - Jim
我认为Francesco Foresti所说的是,一些垃圾回收设置会导致堆缩小并将内存归还给操作系统,这在实践中我也见过,例如:链接 - nsandersen

1

ZGC 在 Java 13 中发布,它可以将未使用的堆内存返回给操作系统。请查看链接


1
不鼓励仅提供链接的答案,因为链接可能会随着时间而失效。请从帖子中提取相关信息并将其作为引用或自己的话添加到您的答案中。 - Genhis
3
@Genhis OTOH,此特定回答中提供的链接应该是“相当”稳定的,因为它们指向java.net上的JEP。 - Flávio Etrusco
1
ZGC在JDK 11中是实验性的,现在仍然是实验性的。 - Nitsan Wakart

0

GC是否将内存释放给操作系统?

简短回答:内存会根据垃圾收集器的配置(算法)立即或稍后返回给操作系统

JDK 1.8

如果您使用的是JDK 8或更早版本,并运行以下代码片段:

public static void main(String[] args) throws InterruptedException {
    Runtime runtime = Runtime.getRuntime();
    long availableMemory = runtime.freeMemory();
    System.out.println("Initially Available Memory : " + availableMemory);

    List<Customer> customers = new ArrayList<>();
    for (int loop = 0; loop < 1_00_000; loop++) {
        customers.add(new Customer("USERNAME"));
    }
    availableMemory = runtime.freeMemory();
    System.out.println("Post Processing Available Memory : " + availableMemory);

    customers= new ArrayList<>();
    Thread.sleep(1000);
    System.gc();

    availableMemory = runtime.freeMemory();
    System.out.println("Post Garbage Collecting Available Memory : " + availableMemory);
}

使用JDK 1.8,在我的机器上会得到以下统计数据:
Initially Available Memory :                   243620776 
Post Processing Memory :                       240444976 
Post Garbage Collecting Memory :               250960720 

结果:在JDK 1.8的情况下,当我们调用System.gc()时,我们会得到空闲内存,并且JVM接收它并不将其返回给操作系统。

JDK 11+

当运行相同的代码片段时,我们得到了一个令人惊讶的结果:

Initially Available Memory:                   242021048 
Post Processing Memory:                       239171744 
Post Garbage Collecting Memory:                61171920 

结果:在JDK 11+的情况下,当我们调用System.gc()时,JVM会释放内存并将其返回给操作系统。
后果:
每当我们将空闲内存返回给操作系统时,如果我们需要更多的内存,就必须向操作系统请求额外的空闲内存块。这种开销会导致性能下降。
要求JDK 11+不要将空闲内存释放回操作系统。
我们可以将初始堆大小设置得很高,这样我们就可以确保永远不会遇到这种情况。我们可以通过-Xms来设置初始堆大小。
$ java -Xms2g MyProgram.java
Initially Available Memory :                  2124414976
Post Processing Memory :                      2120842392
Post Garbage Collecting Memory :              2144444600

我使用的是ThinkPad 内存为16,082 MB 可用物理内存为4,340 MB 虚拟内存:最大大小为21,202 MB 虚拟内存:可用为6,123 MB 虚拟内存:已使用为15,079 MB

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