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

时间:2011-08-18 21:59:53

标签: java memory memory-management playframework

我正在使用Play! Framework构建Java Web应用程序。我在playapps.net上托管它。在提供的内存消耗图表上,我一直困惑不已。这是一个示例:

Heap Memory

图表来自一致但名义上的活动期。我没有做任何事情来触发内存中的衰减,所以我认为这是因为垃圾收集器运行,因为它已经几乎达到了允许的内存消耗。

我的问题:

  • 我认为我的应用程序有内存泄漏是否公平,因为看起来垃圾收集器运行时所有内存都被正确回收了?
  • (来自标题)为什么java等到最后一秒才能运行垃圾收集器?随着内存消耗增长到图表的前四位,我看到性能显着下降。
  • 如果我的断言是正确的,那么我该如何解决这个问题呢?我在SO上读到的其他帖子似乎反对调用System.gc(),范围从中性(“它只是运行GC的请求,所以JVM可能只是忽略你”)直接反对(“代码依赖于System.gc()从根本上被打破了“)。或者我不在这里,我应该在我自己的代码中寻找导致此行为和间歇性性能损失的缺陷?

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

这是link to the discussion,但遗憾的是您需要一个playapps帐户才能查看。

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

解决
Playapps支持,这仍然很棒,对我没有很多建议,他们唯一的想法是,如果我广泛使用缓存,这可能会使对象保持活动的时间超过需要,但事实并非如此。我仍然学到了很多东西(呜呜!),我给了@Ryan Amos绿色支票,因为我提出了每半天打电话System.gc()的建议,现在工作正常。

6 个答案:

答案 0 :(得分:22)

任何详细的答案都取决于您使用的垃圾收集器,但有些事情在所有(现代,太阳/甲骨文)GC中基本相同。

每次看到图表中的用法都会消失,那就是垃圾收集。堆释放的唯一方法是通过垃圾收集。事情是有两种类型的垃圾收集,轻微和完整。堆被分成两个基本的“区域”。年轻和终身。 (实际上有更多的子组。)任何占据Young的空间并且在小型GC出现以释放一些内存时仍在使用的东西将被“提升”为终身。一旦某些东西进入终身,它会无限期地停留,直到堆没有可用空间并且需要完整的垃圾收集。

因此,对该图的一种解释是,您的年轻一代相当小(默认情况下,它可能是某些JVM上总堆的相当小的百分比),并且您将对象保持“活着”相当长的时间。 (也许你在网络会话中持有对它们的引用?)所以你的对象是'幸存'的垃圾收集,直到它们被提升到终身空间,在那里它们无限期地停留在那里,直到JVM很好并且确实没有内存。 / p>

同样,这只是一种适合您所拥有数据的常见情况。需要有关JVM配置和GC日志的完整详细信息才能确切了解发生了什么。

答案 1 :(得分:19)

Java不会运行垃圾清理器,因为垃圾清理器会减慢速度并且不应该经常运行。我认为您可以更频繁地安排清洁,例如每3小时一次。如果应用程序永远不会占用全部内存,那么就没有理由运行垃圾清理器,这就是为什么Java只在内存非常高时才运行它。

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

答案 2 :(得分:12)

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

Java中有一些次要和主要的集合。次要收藏经常发生,而主要收藏品更少见,性能更低。次要集合可能倾向于清除诸如在方法中创建的短期对象实例之类的东西。一个主要的集合将删除更多,这可能发生在图表的末尾。

现在,我在输入时发布的一些答案可以很好地解释垃圾收集器,对象生成等方面的差异。但这仍然无法解释为什么在进行严肃清洁之前需要如此荒谬(将近24小时)。

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

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

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

这假设您的图表显示堆使用而未分配堆大小。如果情况并非如此,并且您实际上看到堆本身就像这样增长,那么还会发生其他事情。我承认我对垃圾收集的内部及其调度非常不够精确,以确定这里发生的事情,其中​​大部分是通过观察剖析器中泄漏的应用程序。所以如果我提供了错误的信息,我会把这个答案放下来。

答案 3 :(得分:2)

您可能已经注意到,这不会影响您。垃圾收集只有在JVM认为需要运行时才会启动,这样做是为了优化,如果你可以制作一个完整的集合并进行全面清理,就不会使用很多小集合。 / p>

当前的JVM包含一些非常有趣的算法,垃圾收集本身的ID分为3个不同的区域,你可以找到更多关于这个here的信息,这里有一个示例:

  

三种类型的收集算法

     

HotSpot JVM提供三种GC算法,每种算法针对特定一代中特定类型的集合进行调整。副本(也称为清除)集合可以快速清除新一代堆中的短期对象。 mark-compact算法采用更慢,更健壮的技术来收集旧一代堆中的长寿命对象。增量算法尝试通过执行强大的GC同时最小化暂停来改进旧一代集合。

     

复制/清除收藏

     

使用复制算法,JVM只需通过制作小的清除(用于收集和删除垃圾的Java术语)来回收新生成对象空间(也称为eden)中的大多数对象。寿命较长的对象最终会被复制或保留到旧的对象空间中。

     

Mark-compact collection

     

随着更多物体的使用,旧的物体空间开始达到最大占用率。用于收集旧对象空间中的对象的mark-compact算法与新对象空间中使用的复制收集算法具有不同的要求。

     

mark-compact算法首先扫描所有对象,标记所有可到达的对象。然后它压缩死物体的所有剩余间隙。 mark-compact算法占用比拷贝收集算法更多的时间;但是,它需要更少的内存并消除内存碎片。

     

增量(火车)收集

     

新一代复制/清除和旧一代标记 - 紧凑算法无法消除所有JVM暂停。这种暂停与活动对象的数量成比例。为了满足对无暂停GC的需求,HotSpot JVM还提供增量或训练集合。

     

即使对象区域较大,增量收集也会将旧的对象集合暂停分解为许多微小的暂停。这种算法不仅仅是新一代,而是具有许多小空间的中间代。增量收集会产生一些开销;你可能会看到速度降低10%。

     

-Xincgc和-Xnoincgc参数控制如何使用增量收集。下一版本的HotSpot JVM 1.4版将尝试连续的,无间断的GC,它可能是增量算法的一种变体。我不会讨论增量收集,因为它很快就会改变。

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

答案 4 :(得分:2)

我有一个应用程序可以生成这样的图形,并按照您的描述进行操作。我使用的是CMS收集器(-XX:+ UseConcMarkSweepGC)。以下是我的案例。

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

答案 5 :(得分:0)

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