Java垃圾回收器 - 未按正常间隔运行

16

我有一个不停运行的程序。通常情况下,它会进行垃圾回收,并保持在大约8MB的内存使用量以下。但是,每个周末,除非我明确调用它,否则它会拒绝进行垃圾回收。但是,如果它接近最大堆大小,它仍然会进行垃圾回收。然而,唯一发现这个问题的原因是,它实际上在一个周末由于内存耗尽而崩溃了,也就是说它必须已经达到了最大堆大小,没有运行垃圾收集器。

以下图片(点击查看)是程序一天内的内存使用情况图。在图表的两侧,您可以看到程序内存使用的正常行为,但第一个大峰似乎是从周末开始的。这张特别的图表是一个奇怪的例子,因为在我明确调用垃圾回收器后,它成功运行,但然后它又攀升到最大堆大小,并成功地自己垃圾回收了两次。

这里到底发生了什么?

编辑:

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

问题在于,这个活动相当稳定,如果说有什么变化,那么在内存使用开始持续攀升的时候,活动应该更少,而不是更多。不过,我上面发布的图表是唯一一张能回溯到之前的图表,因为我只改变了Java Visual VM包装器,以便保留足够长时间的图形数据才能看到这个问题的解决情况,而我无法在周末监视它,因为它在私人网络上,而且周末我不上班。

以下是关于下一天的图表:alt text

这基本上是每周其他日子内存使用情况的样子。程序从未重新启动,我们仅在星期一早上告诉它垃圾回收,因为有这个问题。有一周我们尝试在星期五下午重新启动它,但在周末某个时候它仍然开始攀升,因此我们重新启动它的时间似乎与下周的内存使用情况无关。

事实上,当我们告诉它进行垃圾回收并成功地回收所有这些对象时,这意味着这些对象是可回收的,只是直到达到最大堆大小或我们显式调用垃圾回收器之前才会执行回收。堆转储不会告诉我们任何东西,因为当我们尝试执行一个时,它立即运行垃圾回收器,然后输出堆转储,当然在这一点上看起来完全正常。

所以我想我有两个问题:为什么它突然不像一周的其余时间那样进行垃圾回收,以及为什么在某个情况下,当达到最大堆大小时发生的垃圾回收无法收集所有这些对象(即为什么会有对那么多对象的引用,而每次都不会有引用)?

更新:

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

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

  1. 在01:00,服务器以某种方式重新同步了它的时间,将其设置为00:28。
  2. 根据新的不正确服务器时间,在00:45,程序中的一个消息处理线程抛出了内存不足错误。
  3. 然而,另一个消息处理线程(我们接收到的消息有两种类型,它们稍微不同,但它们都在不断地进来)继续运行,通常情况下,内存使用量会持续增长,没有垃圾回收(从我们一直记录的图表中可以看到,再次出现这种情况)。
  4. 在00:56时,日志停止了,直到早上7点左右,程序才被客户端重新启动。然而,对于此时间段,内存使用量图表仍在稳步增长。

不幸的是,由于服务器时间改变,这使得我们的内存使用量图表上的时间不可靠。然而,似乎它尝试进行垃圾回收,失败了,将堆空间增加到最大可用大小,并立即杀死了那个线程。现在,由于最大堆空间已经增加,它愿意使用所有可用的空间,而不执行主要的垃圾回收。

所以我现在问:如果服务器时间突然改变,是否会影响垃圾回收过程?


4
提供Java厂商、版本和操作系统信息肯定会有所帮助(还有您在运行程序时提供的任何命令行选项)。 - barti_ddu
这张图片显示的是周一,不是周末。无论如何,我认为信息不足。我可以告诉你通常情况下(JLS指定?),如果有足够的可释放内存,就不会发生OOM,因此,如果您看到内存使用量上升并以OOM结束,那么该内存就无法被回收利用。 - Tim Bender
6
你可能想要查看程序在内存不足时创建的转储文件。这在过去对我们很有帮助。请加上“-XX:-HeapDumpOnOutOfMemoryError”和“-XX:HeapDumpPath=/path/to/dump”,它会创建一个hprof文件,你可以使用Eclipse MAT(内存分析工具)进行调试。 - mezzie
此外,如果没有关于您的应用程序的任何知识,猜测原因是不太可能的。它是否创建大量对象?它是否使用外部库(例如,在C库包装器中可能存在内存泄漏)?它是否进行密集的I/O操作?它是在客户端还是服务器模式下运行? - barti_ddu
对我来说,有两个方面:你的程序在做什么?以及:JVM在做什么?我的意思是:JVM在垃圾回收方面是否在周末表现不同?你的UDP数据包流入是否在整个星期内保持稳定吞吐量?这些数据包的内容在周末是否有实质性的不同?在没有更多细节的情况下,我建议尝试使用另一个供应商的JVM,看看它是否会出现类似的情况。将计算机的时间向后调几天,看看它是否认为现在是周末,并且像你想象的那样运行。 - weiji
很遗憾,我们无法更改服务器时间。该程序是更大系统的一部分,当前系统时间必须正确,实际上它在客户端的服务器上,位于他们的私有网络中,因此我们无法将UDP数据包发送到我们自己的服务器进行测试。但是,传入的UDP数据流并不会真正改变,数据包的内容始终意味着相同的事情。实际上,我将编写一个虚拟数据源来模拟真实数据进行本地测试,然后更改本地系统时间以查看发生了什么。 - Ogre
3个回答

11
然而,注意到这个问题的唯一原因是因为在一个周末它实际上由于内存用尽而崩溃,即它必须达到了最大堆大小,没有运行垃圾收集器。
我认为你的诊断是不正确的。除非你的JVM出现了严重问题,否则应用程序只会在刚刚运行完全垃圾回收后抛出OOME,然后发现仍然没有足够的空闲堆来继续执行*。
我怀疑这里发生的事情是以下一种或多种情况:
- 您的应用程序存在缓慢的内存泄漏。每次重新启动应用程序时,泄漏的内存都被回收。因此,如果您在工作日经常重新启动应用程序,则可以解释为仅在周末崩溃。 - 您的应用程序正在执行需要不同量的内存才能完成的计算。那个周末,有人发送了一个需要更多可用内存的请求。
手动运行GC实际上无法解决任何一种情况的问题。您需要调查内存泄漏的可能性,并查看应用程序内存大小是否足够执行正在执行的任务。
如果您可以长时间捕获堆统计信息,则内存泄漏将显示为完整垃圾回收后可用内存量随时间下降的趋势(也就是锯齿形图案的最长“牙齿”的高度)。 与工作负载有关的内存短缺可能会在相对短的时间内显示为同一度量的偶尔急剧下降趋势,然后恢复。您可能会同时看到这两者,那么两种情况都可能发生。
*实际上,决定何时放弃OOME的标准要比这个复杂得多。它们取决于某些JVM调整选项,并且可以包括运行GC所花费的时间百分比。
追问:@Ogre - 我需要更多关于您的应用程序的信息才能具体回答(关于内存泄漏的问题)。

根据你提供的新证据,有两种可能性:

  • 你的应用程序可能因时钟时间扭曲而陷入循环并泄漏内存。

  • 时钟时间扭曲可能会导致GC认为它在运行时占用了过大的比例,并因此触发OOME。这种行为取决于你的JVM设置。

无论哪种情况,你都应该向客户施加压力,让他们停止像那样调整系统时钟。(32分钟的时间扭曲太多了!!)让他们安装一个系统服务,使时钟每小时(或更频繁地)与网络时间保持同步。重要的是,请让他们使用具有逐渐调整时钟选项的服务。

(关于第2个问题:JVM 中有一种 GC 监测机制,它测量 JVM 在执行有用工作相对于运行 GC 所花费的总时间的百分比。这旨在防止 JVM 在应用程序真正耗尽内存时完全停止运行。

该机制通过在各个点上采样墙钟时间来实现。但如果在关键时刻墙钟时间发生扭曲,很容易看出JVM可能会认为某个特定的GC运行花费了比实际所需时间更长...并触发 OOME。)


抱歉,我没有回复你的答案,因为当时我不确定如何回复。但现在我必须问:如果内存使用量直到周一早上1点左右从未超过8mb,那么它怎么可能是内存泄漏呢?该程序在那个时间并没有做任何不同的事情。 - Ogre
我认为你在追踪问题时提到的第二个要点是正确的,尽管它没有解释为什么内存使用量会不断上升,如果它没有触发 OOME,在达到最大堆大小时只有进行垃圾回收。不过,我很高兴能够做些什么来确保服务器时间是正确的,因为由于时间敏感数据,系统的其余部分也需要正确的时间。 - Ogre
@Ogre - 这很容易解释:这只是所有标记/扫描和复制GC工作的方式。当您拥有可收集垃圾的最大量时,GC运行效率最高。当您尝试从中分配空间的堆中没有剩余空间时,就会发生这种情况。结果是内存使用量呈锯齿状模式。 - Stephen C

3
如果可能的话,我会设置进程在内存不足时转储堆 - 这样您可以分析它,以便下次再发生类似情况。这不是答案,但是解决方案的一个潜在途径。
以下是来自Oracle Java HotSpot VM Options页面的JVM选项。(假设您有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)

2

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

似乎在内部某个地方同步时间时,服务器的内存使用率开始稳步上升。我们客户的IT联系人不知道时间从哪里同步过来。显然,无论它来自哪里,都不是一个好的时钟,因为时间落后了半个小时。我们关闭了这个同步功能,现在我今天早上再次检查,问题没有出现。因此,如果您系统上的时间突然改变,显然会对垃圾收集器造成问题。至少这就是我的理解。

至于为什么在服务器上的其他任何部分(也是用Java编写)上都没有发生这种情况,我们可能只是没有注意到,因为它们没有处理如此多的对象,所以它们永远不会达到内存不足的状态。

我觉得这很奇怪,因为我认为调用垃圾收集器完全与内存使用有关,与系统时间无关。显然,我对垃圾收集器的工作原理的理解非常不足。


时间扭曲导致 OOME 是可以解释的;请参考我的回答。 - Stephen C
收集器确实考虑了它运行所花费的时间,但如果它使用挂钟来计算,我会感到惊讶。您是否有在此时间段内的详细GC日志? - SimonJ

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