为什么Java等待这么长时间才运行垃圾收集器?

49

我正在使用 Play! Framework 构建 Java web 应用,并将其托管在 playapps.net 上。我一直在研究有关内存消耗的提供的图表,这是一个示例:

堆内存

该图来自于一段持续但正常的活动期间。我没有执行任何操作触发内存下降,因此我认为这是因为垃圾收集器运行了,因为它几乎达到了允许的内存消耗。

我的问题:

  • 我是否可以认为我的应用程序没有内存泄漏,因为当垃圾收集器运行时,所有内存都被正确回收了?
  • (从标题) 为什么 Java 要等到最后一刻才运行垃圾收集器?随着内存消耗增长到图表的前四分之一,我看到了明显的性能下降。
  • 如果我上面的说法是正确的,那么我该如何解决这个问题?我在 Stack Overflow 上读到的其他帖子似乎反对调用 System.gc(),从中立的("这只是运行 GC 的请求,因此 JVM 可能会忽略你")到彻底反对的("依赖于 System.gc() 的代码基本上是有缺陷的")。或者我说错了,我应该寻找自己代码中引起此行为和间歇性性能损失的缺陷?

更新:

我已在 PlayApps.net 上开启了一次讨论,指向这个问题并提到了这里的一些要点;特别是 @Affe 的评论关于完整 GC 的设置非常保守,以及 @G_H 的评论关于初始堆大小和最大堆大小的设置。

这里是一篇讨论链接,不过需要一个playapps账户才能查看。

当我得到反馈后,我会在这里报告。非常感谢大家的回答,我已经从中学到了很多!

解决方法
Playapps的支持人员非常好,但他们没有太多建议。他们唯一的想法是,如果我经常使用缓存,可能会使对象的生命周期比必要的长,但事实并非如此。我仍然学到了很多(呼啦哇啦!),我给@Ryan Amos打了勾,因为我采纳了他的建议,每半天调用一次System.gc(),现在它正常运行。


2
可能导致性能问题的一个因素是交换;如果您没有足够的物理内存用于堆(例如,它是一个共享机器,有很多其他进程使用大量内存),将页面交换到磁盘会影响您的虚拟机性能。 - vanza
我认为它可能是一个存储大量数据的内存数据库,但我没有看到堆在现在-2小时时的巨大减少。 - rds
2
此外,我同意 +vanza 关于交换灾难的观点。从我在SharePoint上的(糟糕)经验中,我记得为一个进程分配太多内存并不总是性能最佳的策略。你能否更改JVM设置并设置-Xmx 80m,看看它的表现如何? - rds
@vanza 交换操作会对速度产生非常明显的影响,但内存使用情况基本保持不变,是吗? - G_H
1
@Raedwald:我以前没有听说过这个。32位JVM的最大堆大小限制比现在平均RAM内存容量要低得多。对于64位JVM来说,情况已经不再如此了。但即使如此,在主机上运行许多进程并且其内存变满时,就会开始进行交换。这由操作系统处理,JVM对此毫不知情。它只会分配内存,不知道其中一些可能被映射到磁盘上。 - G_H
显示剩余8条评论
6个回答

22
任何详细的答案都取决于您使用的垃圾回收器,但所有(现代的、sun/oracle)GC都有一些基本相同的特点。每次你看到图表中的使用情况下降,这就是一个垃圾回收。堆的释放只能通过垃圾回收来完成。然而,有两种类型的垃圾回收,分别是 minor 和 full。 heap 被划分为两个基本区域,young 和 tenured。Young 区中占用空间并在 minor GC 释放内存时仍在使用的任何对象都将被“晋升”到 tenured 区。一旦某个对象跨入 tenured 区,它将无限期地保留,直到堆没有可用空间并需要进行 full 垃圾回收。因此,对于该图表的一种解释是,您的 young generation 相对较小(默认情况下,在某些 JVM 上它可能只占总堆的一小部分),并且您将对象“保持活动状态”的时间相对较长。 (也许您在 Web 会话中保存了对它们的引用?)因此,您的对象在垃圾回收中“幸存”,直到它们被晋升到 tenured 空间,在那里它们会无限期地保留,直到 JVM 确实没有可用内存为止。再次强调,这只是符合您所拥有的数据的一种常见情况。要确切了解发生了什么,需要详细了解 JVM 配置和 GC 日志。

这听起来很像我在本地使用YourKit分析应用程序时看到的情况。正如您所看到的,playapps.net提供的图表并不那么详细,但是YourKit向我展示了“年轻”代内存的锯齿状模式,而“老年”代则稳步增长,直到进行垃圾回收。然而,这些都不应该导致性能下降,对吗?我应该在其他地方寻找原因来解释为什么一旦内存位于图表的前1/4处,应用程序就会变慢,然后在内存被回收后加速吗? - goggin13
很奇怪的是JVM会在运行完整个收集之前竭尽全力地压缩空间。你控制JVM还是由你的托管公司提供环境?他们可能出于自己的原因进行了配置。 - Affe
所有内容都由托管公司PlayApps.net在VPS服务器上控制。您认为值得联系他们以获取建议吗? - goggin13
2
是的,你可以提到它看起来像是完整集合触发的设置非常吝啬,以至于在完整集合发生之前,次要集合的频率就已经影响了性能。另外,它可能正在使用只有一个处理器核心的并发垃圾收集器,这可能会与你的问题一致。 - Affe
@goggin13:“JVM在运行完整的收集之前会竭尽全力地压榨自己来节省空间,这很奇怪。”我不确定是否有其他更容易的方法。一旦GC确定某些内容是垃圾,不清除这些垃圾就是浪费。确定哪些对象是垃圾可能是昂贵的。 - Raedwald
对的,只是有一种外观,它在做着一些次要的收集,只能释放非常少量的堆。如果不知道是哪个收集器,就很难给出更详细的解释。 - Affe

19

Java不会在必要之前运行垃圾清理程序,因为垃圾清理程序会显著降低速度,并且不应该频繁运行。我认为你可以安排更频繁的清理,例如每3小时清理一次。如果一个应用程序永远不会消耗满内存,就没有必要运行垃圾清理程序,这就是Java仅在内存非常高时运行它的原因。

所以基本上,不要担心别人说什么:做最好的事情。如果你发现在66%内存时运行垃圾清理可以提高性能,请这样做。


2
点赞。不要听那些说“永远不要这样做”的人。如果它能够正常工作,不会使事情变得非常复杂,不会创建难以维护的架构,并且没有更好的解决方案,那就去做吧。 - G_H
@G_H 没有其他原因,人们不会要求永远不要这样做 :P - shabby

12

我注意到图表在下降前并没有严格向上倾斜,而是有较小的局部变化。虽然我不确定,但如果没有垃圾回收,我认为内存使用不会显示这些小的下降。

Java有小型和大型垃圾回收。小型垃圾回收频繁发生,而大型垃圾回收则更少,会减少性能。小型垃圾回收可能倾向于清除在方法中创建的短寿命对象实例之类的东西。大型垃圾回收将删除更多内容,这可能是发生在您图表末尾的情况。

现在,当我在输入时发布的一些答案对垃圾收集器、对象生成等差异给出了很好的解释。但这仍然无法解释为什么需要如此荒谬的时间(将近24小时)才能进行严重的清理。

可以在JVM启动时设置两个有趣的值:最大允许堆大小和初始堆大小。最大值是一个硬限制,一旦达到该限制,进一步的垃圾回收就不会减少内存使用量,如果需要为对象或其他数据分配新空间,则会得到OutOfMemoryError。但是,在内部还有一个软限制:当前堆大小。 JVM不会立即吞噬最大内存量。相反,它从初始堆大小开始,然后在需要时增加堆大小。将其视为JVM的RAM,可以动态增加。

如果应用程序的实际内存使用量开始接近当前堆大小,则通常会启动垃圾回收。这可能会减少内存使用量,因此不需要增加堆大小。但是,也可能当前应用程序确实需要所有那些内存,并且将超过堆大小。在这种情况下,只要它尚未达到最大设定限制,就会增加堆大小。

现在,可能是您的情况是初始堆大小设置为最大值。假设是这样,JVM将立即占用所有该内存。在应用程序累积足够多的垃圾以达到内存使用的堆大小之前,这将花费很长时间。但在那一刻,您将看到一个大型集合。从足够小的堆开始并允许其增长可使内存使用限制为所需的内容。

假设图表显示的是堆使用情况而不是已分配堆大小。如果不是这种情况,你实际上看到的是堆本身像这样增长,那么肯定有其他问题发生了。我承认,我对垃圾收集及其调度的内部机制并不精通,无法确定这里发生了什么,大多数观察来自于分析泄漏应用程序。因此,如果我提供了错误信息,我会删除这个答案。


这真的很有趣;我不知道JVM初始堆大小设置为最大堆大小的事情。我无法访问此托管帐户的JVM设置,但我想我会向支持部门提交一个工单,看看他们是否认为这可能是个问题。谢谢! - goggin13
2
这是可能发生的。有时候人们会想:“嘿,我只需将起始和最大值设置为相同,这样Java就不必费心进行堆调整了。”但实际上,这可能会降低性能。如果你不在乎有一大批未使用的内存在某段时间内闲置,那么这样做可能是合理的。但大多数情况下,你会希望与系统中其他需要内存的进程友好相处。 - G_H
你认为这是否会与性能损失一起出现?其他帖子已经暗示了这种内存使用不应该对我的应用程序产生影响。另外,你认为显式调用System.gc()可能会帮助启动更早的垃圾收集吗? - goggin13
既然你在一个可能有很多其他JVM进程的环境中运行,那么有很多因素会影响性能,比如像vanza在你的帖子评论中提到的交换。给JVM更高的最大堆实际上可能会对你造成不利影响,因为它可能会使用比必要更多的内存,从而妨碍其他进程。关于System.gc(),它经常被人们所鄙视,但我不喜欢绝对化的说法。即使是最糟糕的反模式有时也是奇怪情况下的正确答案。你可以尝试一下,看看会发生什么。让它在可预测的时间运行。 - G_H
我会做这两件事并稍后在此报告!非常感谢您的帮助,我真的很感激。 - goggin13

2
正文:
你可能已经注意到,这不会影响你。垃圾收集只有在JVM感觉需要运行时才会启动,这是为了优化而发生的,如果你可以进行一次完整的收集并进行完全清理,那么做许多小的收集就没有用处。
当前的JVM包含一些非常有趣的算法,垃圾收集本身分为3个不同的区域,你可以在这里找到更多相关信息,这里是一个示例:
三种收集算法
HotSpot JVM 提供了三种 GC 算法,每种算法都针对特定的收集类型和特定的代。复制(也称为扫描)收集快速清理新代堆中的短生命对象。标记-压缩算法采用更慢、更健壮的技术来收集老年代堆中的长寿命对象。增量算法试图通过执行健壮的 GC 来改善老年代的收集,并尽量减少暂停。
复制/扫描收集
使用复制算法,JVM 通过进行小型扫描(Java 术语:收集和清除垃圾)来回收新代对象空间(也称为伊甸园)中的大多数对象。长寿命对象最终会被复制或提升到旧的对象空间中。
标记-压缩收集
随着越来越多的对象变得长寿,旧的对象空间开始达到最大占用率。用于收集旧对象空间中的对象的标记-压缩算法与用于新对象空间中的复制收集算法有不同的要求。
标记-压缩算法首先扫描所有对象,标记所有可达对象。然后压缩所有死对象的剩余空间。标记-压缩算法占用的时间比复制收集算法长,但它需要更少的内存并消除了内存碎片。
增量(列车)收集
新代复制/扫描和旧代标记-压缩算法无法消除所有 JVM 暂停。这些暂停与活动对象的数量成比例。为了解决需要无暂停 GC 的需求,HotSpot JVM 还提供了增量或列车收集。
增量收集将旧对象收集暂停分成许多微小的暂停,即使是大型对象区域也是如此。该算法不仅具有新代和旧代,还包括许多小空间组成的中间代。增量收集存在一些开销;可能会看到高达 10% 的速度降低。
-Xincgc 和 -Xnoincgc 参数控制如何使用增量收集。HotSpot JVM 的下一个版本 1.4 将尝试连续、无暂停的 GC,这可能是增量算法的变体。我不会讨论增量收集,因为它很快就会改变。
这种分代垃圾收集器是我们现今解决问题最有效的方案之一。

谢谢提供链接和摘要。如果您看到我的评论@Affe的回复,尽管这似乎影响了我。当内存消耗达到图表顶部1/4时,我看到性能下降,然后在GC运行后似乎会反弹。这更可能是巧合吗?我应该在其他地方寻找性能问题吗? - goggin13
这是非常陈旧的信息,关于Java 1.3.1的内容,现在最新的JVM使用不同的算法。这里有一些关于Java 6的信息http://www.oracle.com/technetwork/java/javase/gc-tuning-6-140523.html,而Java 7也有新的G1垃圾回收器。 - Esko Luontola
@esko 这篇文章涵盖的是“代”的概念,它们是完全相同且没有改变的。有关最新算法的更多信息,请参阅此链接 - http://www.oracle.com/technetwork/java/gc-tuning-5-138395.html#1.1.Introduction%7Coutline - Maurício Linhares

2

我曾经使用过一款应用程序,它能够生成像这样的图形并且按照你所描述的方式运行。我正在使用CMS收集器(-XX:+UseConcMarkSweepGC)。以下是我的情况。

我没有为该应用程序配置足够的内存,所以随着时间的推移,我遇到了堆碎片问题。这导致GC频率越来越高,但实际上它并没有抛出OOME或失败转换到串行收集器(在这种情况下,它应该这么做),因为它记录的统计数据只计算应用程序暂停时间(GC阻塞世界),忽略应用程序并发时间(GC与应用程序线程一起运行)的计算。我调整了一些参数,主要是给它更多的堆空间(包括一个非常大的新空间),设置了-XX:CMSFullGCsBeforeCompaction=1,问题就不再发生了。


0

很可能你有内存泄漏,每24小时会清除一次。


2
你说的“清除”是什么意思?内存泄漏不应该无法被清除吗?我没有重新启动应用程序,内存下降(我猜测)是由于GC运行。如果存在内存泄漏,GC就不能够回收那些内存了,对吧?如果我完全错了,请不要害羞,我只是想全面了解一切!感谢您的想法! - goggin13
假设你的应用程序有一个不断增长的“某些东西”的列表;并且每24小时,该列表会被你的代码清空。 - irreputable

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