Java 垃圾回收器 - 无法定期正常运行

2022-09-03 07:01:48

我有一个不断运行的程序。通常,它似乎是垃圾回收,并保持在大约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点,当程序由我们的客户重新启动时。但是,此时的内存使用情况图仍在稳步增长。

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

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


答案 1

但是,注意到此问题的唯一原因是因为它实际上在一个周末因内存不足而崩溃,即它必须已达到最大堆大小,并且未运行垃圾回收器。

我认为你的诊断是不正确的。除非你的 JVM 有严重损坏的地方,否则应用程序只会在运行完整的垃圾回收并发现它仍然没有足够的可用堆来继续*之后才会抛出一个 OOME。

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

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

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

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

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

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

随访

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

有了新的证据,还有另外两种可能性:

  • 您的应用程序可能会陷入一个循环,该循环由于时钟时间扭曲而泄漏内存。

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

无论哪种方式,您都应该依靠您的客户,让他们停止像这样调整系统时钟。(32分钟的时间扭曲太多了!!让他们安装系统服务,使时钟每小时(或更频繁)与网络时间保持同步。至关重要的是,让他们使用带有选项的服务,以小增量调整时钟。

(关于第二点:JVM 中有一个 GC 监视机制,用于测量 JVM 相对于执行有用工作所花费的总时间的百分比。这旨在防止 JVM 在应用程序内存确实不足时磨削到停止。

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


答案 2

如果可能的话,我会将进程设置为在内存不足时转储堆 - 这样您就可以分析它是否(何时)再次发生。这不是一个答案,而是一个通往解决方案的潜在途径。

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

-XX:HeapDumpPath=./java_pid.hprof

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

-XX:-堆转储输出内存错误

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