【问题标题】:Very strange OutOfMemoryError很奇怪的OutOfMemoryError
【发布时间】:2012-05-03 17:08:25
【问题描述】:

和往常一样,问题描述很长。

我们目前正在对我们的产品进行压力测试 - 现在我们面临一个奇怪的问题。一到两个小时后,堆空间开始增长,应用程序稍后会死掉。

分析应用程序显示大量的 Finalizer 对象,填充堆。好吧,我们认为“可能是奇怪的终结器线程减慢”问题,并审查了减少需要终结的对象数量(在这种情况下为 JNA 原生句柄)。无论如何都是好主意,减少了数千个新对象...

接下来的测试显示了相同的模式,仅在一小时后并且没有那么陡峭。这次 Finalizer 源自在测试平台中大量使用的 FileInput- 和 FileOutput 流。所有资源都已关闭,但终结器不再清理。

我不知道为什么在 1 或 2 小时后(无例外)FinalizerThread 似乎突然停止工作。如果我们在某些线程中手动强制 System.runFinalization(),则分析器显示终结器已被清理。立即恢复测试会为终结器分配新的堆。

FinalizerThread 还在,询问 jConsole 他在等待。

编辑

首先,用 HeapAnalyzer 检查堆没有发现任何新的/奇怪的东西。 HeapAnalyzer 有一些不错的功能,但一开始我遇到了困难。我正在使用 jProfiler,它带有很好的堆检查工具,并且会一直使用它。

也许我错过了 HeapAnalyzer 中的一些杀手级功能?

其次,今天我们使用调试连接而不是分析器来设置测试 - 系统现在稳定了近 5 个小时。这似乎是太多终结器(在第一次审查中已减少)、分析器和 VM GC 策略的非常奇怪的组合。由于目前一切正常,没有真正的见解......

到目前为止,感谢您的意见 - 也许您会继续关注并感兴趣(现在您可能有更多理由相信我们不会谈论简单的编程错误)。

【问题讨论】:

标签: java out-of-memory finalizer


【解决方案1】:

我想用当前状态的摘要来结束这个问题。

最后一次测试现在已经超过 60 小时,没有任何问题。这导致我们得出以下总结/结论:

  • 我们有一个使用大量对象的高吞吐量服务器,这些对象最终实现了“终结”。这些对象主要是 JNA 内存句柄和文件流。构建终结器的速度比 GC 和终结器线程能够清理的速度快,这个过程在大约 3 小时后失败。这是众所周知的现象(-> google)。
  • 我们进行了一些优化,使服务器摆脱了几乎所有的 JNA 终结器。此版本已通过附加的 jProfiler 进行测试。
  • 服务器死机比我们最初的尝试晚了几个小时...
  • 分析器显示了大量的终结器,这一次主要是由文件流引起的。即使在服务器暂停一段时间后,此队列也没有清理。
  • 只有在手动触发“System.runFinalization()”后,队列才会被清空。正在恢复服务器开始重新填充...
  • 这仍然是莫名其妙的。我们现在猜测这是一些分析器与 GC/finalization 的交互。
  • 为了调试可能导致非活动终结器线程的原因,我们这次分离了探查器并附加了调试器。
  • 系统运行时没有明显缺陷... FinalizerThread 和 GC 都“绿色”。
  • 我们恢复了测试(现在是第一次在没有附加 jConsole 之外的任何代理的情况下再次进行测试)并且它现在已经正常运行了 60 多个小时。显然,最初的 JNA 重构解决了这个问题,只是分析会话增加了一些不确定性(来自 Heisenberg 的问候)。

管理终结器的其他策略例如在http://cleversoft.wordpress.com/2011/05/14/out-of-memory-exception-from-finalizer-object-overflow/ 中讨论(除了不太聪明的“不要使用终结器”..)。

感谢您的所有意见。

【讨论】:

    【解决方案2】:

    很难对您的困境给出具体的答案,但需要进行堆转储并通过 IBM 的 HeapAnalyzer 运行它。搜索“堆分析器:http://www.ibm.com/developerworks(直接链接不断变化)。如果您没有覆盖 finalize,那么终结器线程“突然停止工作”似乎极不可能。

    【讨论】:

    • “如果你排除了不可能,剩下的,无论多么不可能,都必须是真相”......我应该找到什么结论?
    • 您是在说“我不知道如何使用 HeapAnalyzer”。 ?
    • 不 - 我只是绝望..当你说“这不太可能..”时,这让我想起了 Spock 的伟大引用:-) 我明天会试一试..
    • 同意——我想你最终会发现你的产品存在错误。我只是建议您查看堆上的对象,以帮助您识别分配未清理对象的代码。 HeapAnalyzer 是识别可疑泄漏的绝佳工具。
    • 我真的不这么认为,但我会试一试。请记住,手动触发时队列是清​​空的。从某种意义上说,某些对象被某个晦涩的路径引用,这没有泄漏。代码检查和分析器都没有显示其他 GC 根,但 Finalizer...
    【解决方案3】:

    Finalizer 有可能被阻止,但我不知道它怎么会死。

    如果您有很多 FileInputStream 和 FileOutputStream finalize() 方法,这表明您没有正确关闭文件。确保这些流始终在 finally 块中关闭或使用 Java 7 的 ARM。 (自动资源管理)

    jConsole 他正在等待。

    要等待它必须等待一个对象。

    【讨论】:

    • 这似乎很明显,但我可以向您保证,事实并非如此。测试台使用成熟的工具方法,所有方法都在 finally 块中关闭。测试在退化前数小时绝对无泄漏。此外,我没有正确理解“如果你有 ... finalize() 方法”的说法。我没有终结方法 - 流确实有.. 为它们创建的终结器没有清理.. 在分析器中时,来自流的描述符引用全部重置为 -1(因此它们被关闭)
    • 我的意思是,如果这些类 finalize 方法显示为重要。根据您的建议,如果他们出现是因为其他原因导致队列延迟或终结器线程不再是问题。
    • 如果它在等待队列,这表明它是空的。
    • 这是人们应该假设的。但是,当 - 队列中什么都没有 - 有很多终结器 - 他们引用了其他未引用的对象,谁应该受到责备?
    • 对象引用的对象需要先finalize,只能在下一次GC循环时清理。
    【解决方案4】:

    FileInputStream 和 FileOutputStream 在它们的 finalize() 方法中有相同的注释:

    . . .
    /*
     * Finalizer should not release the FileDescriptor if another
     * stream is still using it. If the user directly invokes
     * close() then the FileDescriptor is also released.
     */
         runningFinalize.set(Boolean.TRUE); 
    . . . 
    

    这意味着您的终结器可能正在等待流被释放。这意味着,正如上面 Joop Eggen 所提到的,您的应用在关闭其中一个流时可能会做错事。

    【讨论】:

    • 当我查看我的 JDK 1.6.21 时,情况并非如此。你用的是什么版本?
    • 我会再看一遍。但我不明白这如何解释最终确定的饥饿。请记住,手动调用 runFinalization 会清理队列。
    【解决方案5】:

    我的猜测:它是您自己的流(包装器)类中的覆盖关闭。由于流类通常是包装器并委托给其他人,我可以想象这样的嵌套new A(new B(new C())) 可能会在关闭时导致一些错误的逻辑。您应该寻找两次关闭,委托关闭。也许还有一些被遗忘的关闭(关闭错误的对象?)。

    【讨论】:

    • 很奇怪,也许是完全不同的东西,比如多次访问/删除/创建同一个文件名?这也可能导致一些不稳定,尤其是在 Windows 下。
    【解决方案6】:

    随着堆的缓慢增长,当 Java 垃圾收集器尝试在内存不足的情况下进行迟到的垃圾收集时,它可能会耗尽内存。尝试使用 -XX:+UseConcMarkSweepGC 打开并发标记和清除垃圾收集,看看您的问题是否消失。

    【讨论】:

    • 非常活跃 - 增长不慢。运行时有很多垃圾收集。只有一个小时后,终结者才不再收集。
    • 即使在活动堆中,Java 也会经常推迟某些对象的垃圾回收,这可能会导致终结器代码出现问题。
    猜你喜欢
    • 2014-12-02
    • 2019-01-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-03-26
    相关资源
    最近更新 更多