为什么java等这么久才能运行垃圾回收器?

我正在构build一个Java Web应用程序,使用Play! 框架 。 我在playapps.net上托pipe它。 对于内存消耗的图表,我一直困惑不解。 这是一个示例:

堆内存

该图来自一致但名义上的活动时期。 我没有做任何事情来触发内存的衰退,所以我认为这是因为垃圾收集器几乎达到其允许的内存消耗量而运行的结果。

我的问题:

  • 假设我的应用程序没有内存泄漏,这对我来说是公平的,因为看起来垃圾收集器在运行时正确回收了所有的内存?
  • (从标题)为什么Java等待,直到最后可能秒运行垃圾回收器? 内存消耗增长到图表的前四分之一,我看到显着的性能下降。
  • 如果我上面的说法是正确的,那么我怎么能解决这个问题呢? 我读到的其他post似乎反对调用System.gc() ,从中性(“这只是一个请求运行GC,所以JVM可能会忽略你”),直接反对(“代码依赖System.gc()从根本上被打破“)。 或者我在这里基地,我应该在我自己的代码寻找缺陷造成这种行为和间歇的性能损失?

我为问题的大脑转储道歉。 我一直在debugging这个问题一段时间,并会非常感谢任何指针。

UPDATE
我在PlayApps.net上开了一个关于这个问题的讨论,并提到了一些要点。 具体来说,@About的评论关于一个完整的GC的设置非常保守,@ G_H关于初始和最大堆大小设置的评论。

这里有一个讨论链接 ,不幸的是你需要一个playapps帐户来查看它。

我会在这里报告反馈; 非常感谢大家的回答,我已经从他们身上学到了很多东西!

parsing度
Playapps的支持,这还是很棒的,对我来说没有太多的build议,他们唯一的想法是,如果我广泛使用caching,这可能会让对象的存活时间比需要的长,但事实并非如此。 我还是学了一吨(呜呼!),我给@Ryan Amos一个绿色的支票,因为我每半天都会打电话给System.gc() ,现在工作正常。

再次感谢所有的帮助,一如既往!

Java不会运行垃圾清理器,直到必须执行,因为垃圾清理器会降低速度,并且不应该频繁运行。 我认为你可以更频繁地安排清洁,例如每3小时一次。 如果应用程序永远不会消耗完全内存,那么就没有理由运行垃圾清理器,这就是为什么Java只在内存非常高的时候才运行它。

所以基本上,不要担心别人说什么:做最好的。 如果您发现在66%的内存下运行垃圾清理程序可以提高性能,请执行此操作。

任何详细的答案将取决于你正在使用哪个垃圾回收器,但是在所有(现代,太阳/ oracle)GCs中都有一些基本相同的东西。

每当你看到图中的使用情况下降,那就是垃圾收集。 堆获得释放的唯一方法是通过垃圾收集。 事情是有两种types的垃圾收集,轻微和完整。 堆被分成两个基本的“区域”。 年轻和终身。 (现实中有更多的小组。)任何在Young中占据空间的东西,当小GC开始释放一些记忆时仍然在使用的东西,将会被“提升”到终身。 一旦有什么东西使得这个飞跃变成了终结,那么它将无限期地存在下去,直到堆没有空闲空间,并且需要完整的垃圾回收。

所以对这个图的一个解释是,你的年轻一代相当小(默认情况下,它可能是一些JVM中堆的一小部分),而且你在相当长的时间内保持对象“活着”。 (也许你在Web会话中持有对它们的引用?)所以你的对象是“存活”垃圾集合,直到它们被提升到终身空间,在那里他们无限期地坚持下去,直到JVM完好无损。

再次,这只是一个适合你的数据的常见情况。 需要关于JVMconfiguration和GC日志的完整细节来确切地说明发生了什么事情。

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

Java中有less量和主要的集合。 次要collections经常发生,而主要collections更为罕见,而且性能更差。 次要集合可能倾向于像方法中创build的短期对象实例一样。 一个主要的集合将删除更多,这可能是在图表的最后发生的。

现在,我打字的时候发布的一些答案给出了关于垃圾收集器,对象代和其他方面差异的很好的解释。 但是,这仍然不能解释为什么在严肃的清洁工作之前要花费这么长的时间(接近24小时)。

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

如果您的应用程序的实际内存使用量开始达到当前堆大小,则通常会启动垃圾回收。 这可能会减less内存的使用,所以不需要增加堆大小。 但是,目前的应用程序也可能需要所有的内存,并且会超出堆的大小。 在这种情况下,如果尚未达到最大设定限制,则会增加。

现在,您的情况可能是初始堆大小设置为与最大值相同的值。 假设会是这样,那么JVM会立即抓住所有的内存。 在应用程序累积足够的垃圾以达到内存使用的堆大小之前,需要很长时间。 但是那一刻你会看到一个大集合。 从一个足够小的堆开始,让它增长,使内存使用限于需要的。

这是假设您的graphics显示堆使用和未分配的堆大小。 如果事实并非如此,而且实际上你已经看到堆本身就像这样成长,那么其他的事情就会发生。 我承认,对于垃圾收集的内部情况和时间安排,我不是很了解,绝对能够确定发生在这里的事情,其中​​大部分是通过观察泄漏的应用程序。 所以,如果我提供了错误的信息,我会把这个答案。

正如你可能已经注意到的,这并不影响你。 如果JVM觉得需要运行这个垃圾收集器,那么垃圾收集只会在这里进行,这是为了优化而发生的,如果你能做一个完整的收集并做一个完整的清理,就不需要做很多小的收集。

当前的JVM包含一些非常有趣的algorithm,垃圾收集本身的ID分为3个不同的区域,在这里可以find更多关于这个的东西 ,下面是一个示例:

三种types的收集algorithm

HotSpot JVM提供三种GCalgorithm,每种algorithm都针对特定代中的特定types的集合进行调优。 副本(也称为清除)集合迅速清理新一代堆中的短暂对象。 mark-compactalgorithm采用更慢,更稳健的技术来收集老一代堆中的更长寿的对象。 增量algorithm试图通过执行稳健的GC来改善老一代收集,同时最大限度地减less暂停。

复制/清理收集

通过使用复制algorithm,JVM可以简单地通过小型清除(收集和清除垃圾的Java术语)来回收新一代对象空间(也称为eden)中的大多数对象。 长寿命的对象最终被复制或固定到旧的对象空间中。

标记紧凑集合

随着越来越多的对象变成终身,旧的对象空间开始达到最大的占有率。 用于收集旧对象空间中的对象的mark-compactalgorithm具有与新对象空间中使用的复制收集algorithm不同的要求。

mark-compactalgorithm首先扫描所有对象,标记所有可到达的对象。 然后压缩所有剩余的死物。 mark-compactalgorithm比copy收集algorithm占用更多的时间; 但是,它需要较less的内存并消除了内存碎片。

增量(火车)集合

新一代的复制/清除和老一代的mark-compactalgorithm不能消除所有的JVM暂停。 这种暂停与活动对象的数量成正比。 为了解决对暂停GC的需求,HotSpot JVM还提供增量式或训练式集合。

增量收集将旧对象收集暂停,即使在大对象区域也会暂停很多微小的暂停。 这个algorithm不仅仅是一个新的和老一代,而是具有包含许多小空间的中间世代。 增量收集有一些开销; 你可能会看到多达10%的速度下降。

-Xincgc和-Xnoincgc参数控制如何使用增量收集。 Hotspot JVM 1.4版的下一个版本将尝试连续的,暂停的GC,这可能是增量algorithm的变体。 我不会讨论增量收集,因为它很快就会改变。

这代垃圾收集器是我们现在解决问题的最有效的解决scheme之一。

我有一个应用程序,生成一个这样的graphics,并按照你所描述的行事。 我正在使用CMS收集器(-XX:+ UseConcMarkSweepGC)。 这是我的情况。

我没有为应用程序configuration足够的内存,所以随着时间的推移,我遇到了堆中的碎片问题。 这导致GC的频率越来越高,但实际上并没有抛出OOME,或者将CMS从串行收集器(在这种情况下应该这样做)中排除,因为它保留的统计信息只能计算应用程序暂停时间(GC块这些计算忽略了应用程序并发时间(GC与应用程序线程一起运行)。 我调整了一些参数,主要是给它一个整体垃圾加载更多的堆(有一个非常大的新空间),设置-XX:CMSFullGCsBeforeCompaction = 1,并且问题停止发生。

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