Java垃圾收集器 – 定期运行不正常

我有一个不断运行的程序。 通常,它似乎是垃圾收集,并保持在大约8MB的内存使用量。 但是,每个周末,它都会拒绝垃圾收集,除非我明确打电话给它。 但是,如果它接近最大堆大小,它仍将是垃圾收集。 然而,注意到这个问题的唯一原因是因为它实际上在一个周末因内存耗尽而崩溃,即它必须达到最大堆大小,而不是运行垃圾收集器。

下图(单击以查看)是程序一天内存使用情况的图表。 在图的两侧,您可以看到程序内存使用情况的正常行为,但第一个大峰似乎是周末开始的。 这个特殊的图是一个奇怪的例子,因为在我对垃圾收集器进行了一次显式调用后,它成功运行了,但随后它又回到了最大堆大小并成功地将垃圾收集到它自己的两次。

这里发生了什么?

编辑:

好的,从评论来看,似乎我没有提供足够的信息。 程序只接收UDP数据包流,这些数据包放在队列中(设置为最大大小为1000个对象),然后对其进行处理,使其数据存储在数据库中。 平均而言,它每秒大约有80个数据包,但可以达到150个。它在Windows Server 2008下运行。

问题是,这个活动是相当一致的,如果有的话,在内存使用开始时它稳定攀升,活动应该更低,而不是更高。 请注意,我上面发布的图表是我唯一一个延伸到目前为止的图表,因为我只更改了Java Visual VM包装器,以便将图形数据保持足够远,以便在本周看到它,所以我不知道它是否完全正确每周的同一时间,因为我不能在周末观看它,因为它是在私人网络上,而且我周末不上class。

这是第二天的图表: 替代文字

这几乎就是每周其他日子的内存使用情况。 程序永远不会重新启动,我们只会在周一早上告诉它垃圾收集因为这个问题。 有一周我们尝试在星期五下午重新启动它,它在周末的某个时间仍然开始爬升,所以我们重新启动它的时间似乎与下周的内存使用没有任何关系。

它成功地垃圾收集所有这些对象的事实,当我们告诉它向我暗示对象是可收集的时,它只是在它达到最大堆大小时才执行它,或者我们显式调用垃圾收集器。 堆转储没有告诉我们什么,因为当我们尝试执行一个时,它突然运行垃圾收集器,然后输出堆转储,这当然看起来完全正常。

所以我想我有两个问题:为什么它突然没有垃圾收集它在本周剩余时间内的方式,为什么在一个场合,当达到最大堆大小时发生的垃圾收集无法收集所有这些对象(也就是说,为什么会有一次这么多对象的引用,而每隔一段时间就不会有这样的对象)?

更新:

今天早上很有趣。 正如我在评论中提到的,该程序正在客户端的系统上运行。 我们在客户组织中的联系人报告说,凌晨1点,该程序失败了,他今天早上上class时必须手动重启,而且服务器时间再次不正确。 这是我们过去曾遇到过的一个问题,但直到现在,这个问题似乎从来没有关系过。

查看我们的程序生成的日志,我们可以推断出以下信息:

  1. 在01:00,服务器以某种方式重新启动它的时间,将其设置为00:28。
  2. 在00:45(根据新的,不正确的服务器时间),程序中的一个消息处理线程抛出了内存不足错误。
  3. 但是,另一个消息处理线程(我们收到两种类型的消息,它们的处理方式略有不同,但它们都不断进入),继续运行,并且像往常一样,内存使用量继续攀升,没有垃圾回收(从我们记录的图表中可以看出,再一次)。
  4. 在00:56,日志停止,直到早上7点,我们的客户重新启动程序。 但是,此时的内存使用情况图仍在稳步增长。

不幸的是,由于服务器时间的变化,这使得我们的内存使用图上的时间不可靠。 但是,它似乎是尝试垃圾收集,失败,将堆空间增加到最大可用大小,并立即杀死该线程。 既然最大堆空间已经增加,它很乐意在不执行主要垃圾收集的情况下使用所有堆空间。

所以现在我问这个问题:如果服务器时间突然像它一样发生变化,那么这会导致垃圾收集过程出现问题吗?

然而,注意到这个问题的唯一原因是因为它实际上在一个周末因内存耗尽而崩溃,即它必须达到最大堆大小,而不是运行垃圾收集器。

我认为您的诊断不正确。 除非你的JVM出现严重问题,否则应用程序只会运行完全垃圾收集抛出一个OOME,并发现它仍然没有足够的空闲堆来继续*

我怀疑这里发生的是以下一种或多种情况:

  • 您的应用程序内存泄漏缓慢。 每次重新启动应用程序时,都会回收泄漏的内存。 因此,如果您在一周内定期重新启动应用程序,这可以解释为什么它只会在周末崩溃。

  • 您的应用程序正在进行需要不同内存量才能完成的计算。 在那个周末,有人向它发送了一个需要更多可用内存的请求。

在任何一种情况下,手动运行GC实际上都无法解决问题。 您需要做的是调查内存泄漏的可能性,并查看应用程序内存大小,以查看它是否足够大以执行正在执行的任务。

如果您可以长时间捕获堆统计信息,则内存泄漏将显示为完全垃圾回收后可用内存量随时间的下降趋势。 (这是锯齿模式中最长的“牙齿”的高度。)与工作负荷相关的记忆短缺可能会在相对较短的时间内出现在同一测量中的偶然急剧下降趋势,随后是恢复。 你可能会看到两者,然后你可以发生这两件事。

*实际上,决定何时放弃OOME的标准比这复杂一点。 它们依赖于某些JVM调优选项,并且可以包括运行GC所花费的时间百分比。

跟进

@Ogre – 我需要更多关于你的应用程序的信息,以便能够以任何特异性回答这个问题(关于内存泄漏)。

根据您的新证据,还有两种可能性:

  • 由于时钟时间扭曲,您的应用程序可能会卡在一个泄漏内存的循环中。

  • 时钟时间扭曲可能会导致GC认为它占用了太大的运行时间百分比并因此触发了OOME。 此行为取决于您的JVM设置。

无论哪种方式,你都应该在客户端上努力让他们停止像那样调整系统时钟。 (32分钟的时间扭曲太多了!)。 让他们安装系统服务,使时钟与网络时间保持同步(或更频繁)。 重要的是,让他们使用带有选项的服务以小幅度调整时钟。

(重新说明第2篇:JVM中有一个GC监视机制,用于衡量JVM运行GC所花费的总时间百分比,相对于执行有用的工作而言。这是为了防止JVM在停止运行时停止运行。你的应用程序真的没有内存。

该机制将通过在不同点对壁钟时间进行采样来实现。 但是如果挂钟时间在关键时刻扭曲,很容易看出JVM如何认为特定GC运行花费的时间比它实际上要长得多……并触发OOME。)

如果可能的话,我会设置进程,以便在内存不足时转储堆 – 所以如果(再次)它再次发生,你可以分析它。 不是答案,而是解决方案的潜在途径。

以下是JVM选项,取自Oracle的Java HotSpot VM Options页面。 (这假设您有一个Oracle JVM):

-XX:HeapDumpPath = / java_pid.hprof

堆转储的目录或文件名的路径。 管理。 (在1.4.2更新12,5.0更新7中引入。)

-XX:-HeapDumpOnOutOfMemoryError

抛出java.lang.OutOfMemoryError时转储堆到文件。 管理。 (在1.4.2更新12,5.0更新7中引入。)

好的,谢谢你的帮助。 然而,正确的答案与程序本身无关。

似乎当内存使用率开始稳步攀升时,服务器正在从内部某处同步它的时间,尽管我们客户的IT联系人不知道在哪里。 显然,无论它来自哪里,都不是一个好时钟,因为时间落后半小时。 我们关闭了这个同步,现在我今天早上再次检查了它,问题没有发生。 因此,如果系统上的时间突然改变,显然这会导致垃圾收集器出现问题。 至少这对我来说意味着什么。

至于为什么这个系统的任何其他部分都没有出现在这个服务器上(也用Java编写),我们可能根本就没有注意到,因为它们没有处理大量的对象,所以他们永远不会遇到记忆状态。

我发现这很奇怪,因为我会认为调用垃圾收集器与内存使用完全相关,而不是系统时间。 显然,我对垃圾收集器如何工作的理解是非常不合适的。