跟踪 Java 中的内存泄漏/垃圾回收问题

这是我几个月来一直试图追踪的问题。我有一个运行java应用程序,它处理xml源并将结果存储在数据库中。一直存在难以追踪的间歇性资源问题。

背景:在生产箱上(问题最明显的地方),我没有特别好的访问箱子,并且无法让Jprofiler运行。该盒子是一台64位四核,8gb的机器,运行centos 5.2,tomcat6和java 1.6.0.11。它从这些java-opts开始

JAVA_OPTS="-server -Xmx5g -Xms4g -Xss256k -XX:MaxPermSize=256m -XX:+PrintGCDetails -
XX:+PrintGCTimeStamps -XX:+UseConcMarkSweepGC -XX:+PrintTenuringDistribution -XX:+UseParNewGC"

技术堆栈如下:

  • Centos 64 位 5.2
  • Java 6u11
  • 雄猫 6
  • Spring/WebMVC 2.5
  • 休眠 3
  • 石英 1.6.1
  • DBCP 1.2.1
  • Mysql 5.0.45
  • Ehcache 1.5.0
  • (当然还有许多其他依赖项,特别是雅加达共享资源库)

我能得到的最接近重现问题的是内存要求较低的32位机器。我确实可以控制。我已经用JProfiler探测到死,并修复了许多性能问题(同步问题,预编译/缓存xpath查询,减少线程池,删除不必要的休眠预取,以及在处理过程中过度热心的“缓存预热”)。

在每种情况下,分析器都显示这些由于某种原因占用了大量资源,并且一旦更改进入,这些资源就不再是主要资源占用。

问题:JVM似乎完全忽略了内存使用设置,填满了所有内存并变得无响应。对于面向客户端来说,这是一个问题,他们希望定期进行轮询(5 分钟基准和 1 分钟重试),对于我们的运营团队也是如此,他们不断收到通知,指出某个盒子已变得无响应,必须重新启动它。在这个盒子上没有其他重要的运行。

问题似乎是垃圾回收。我们使用 ConcurrentMarkSweep(如上所述)收集器,因为原始 STW 收集器导致 JDBC 超时并变得越来越慢。日志显示,随着内存使用量的增加,即开始抛出cms故障,并踢回原始的停止世界收集器,然后似乎没有正确收集。

但是,使用jprofiler运行,“运行GC”按钮似乎可以很好地清理内存,而不是显示增加的占用空间,但是由于我无法将jprofiler直接连接到生产盒,并且解决经过验证的热点似乎不起作用,因此我留下了盲目调整垃圾收集的巫毒教。

我尝试过:

  • 分析和修复热点。
  • 使用 STW、并行和 CMS 垃圾回收器。
  • 以最小/最大堆大小以 1/2,2/4,4/5,6/6 为增量运行。
  • 以 256M 为增量以 256M 为增量运行,最高可达 1Gb。
  • 上述的许多组合。
  • 我还查阅了 JVM [调优参考](http://java.sun.com/javase/technologies/hotspot/gc/gc_tuning_6.html),但找不到任何解释此行为的内容,也无法找到任何在此类情况下要使用的 _that_ 调优参数示例。
  • 我也(不成功)在离线模式下尝试了jprofiler,与jconsole,visualvm连接,但我似乎找不到任何可以穿的gc日志数据的东西。

不幸的是,这个问题也偶尔出现,它似乎是不可预测的,它可以运行几天甚至一周而没有任何问题,或者它可以在一天内失败40次,而我唯一能持续抓住的就是垃圾收集正在发挥作用。

任何人都可以给出任何建议:
a)为什么JVM在配置为最大小于6时使用8个物理演出和2 GB的交换空间。
b) 对 GC 调优的引用,它实际上解释或给出了何时以及使用哪种设置的合理示例。
c)对最常见的java内存泄漏的引用(我理解无人认领的引用,但我的意思是在库/框架级别,或者数据结构中更内嵌的东西,如哈希映射)。

感谢您提供的任何和所有见解。

编辑
Emil H:
1)是的,我的开发集群是生产数据的镜像,一直到媒体服务器。主要区别在于32/64位和可用的RAM量,我无法很容易地复制,但代码,查询和设置是相同的。

2)有一些依赖于JaxB的旧代码,但是在重新排序作业以避免调度冲突时,我通常消除了这种执行,因为它每天运行一次。主解析器使用 XPath 查询,这些查询向下调用 java.xml.xpath 包。这是一些热点的来源,其中一个是查询没有预先编译,两个对它们的引用是硬编码的字符串。我创建了一个线程安全缓存(哈希映射),并将对 xpath 查询的引用分解为最终的静态字符串,这大大降低了资源消耗。查询仍然是处理工作的很大一部分,但应该是因为这是应用程序的主要职责。

3)另外一个注意事项,另一个主要消费者是来自JAI的图像操作(从源重新处理图像)。我不熟悉java的图形库,但从我发现它们并不是特别泄漏。

(感谢到目前为止的答案,伙计们!

更新:
我能够使用VisualVM连接到生产实例,但它已经禁用了GC可视化/运行GC选项(尽管我可以在本地查看它)。有趣的是:VM 的堆分配服从JAVA_OPTS,实际分配的堆舒适地位于 1-1.5 gigs,似乎没有泄漏,但机箱级别的监控仍然显示泄漏模式,但它没有反映在 VM 监控中。这个盒子上没有其他东西在运行,所以我很困惑。


答案 1

好吧,我终于找到了导致此问题的问题,并且我正在发布一个详细的答案,以防其他人遇到这些问题。

我在进程运行时尝试了jmap,但这通常会导致jvm进一步挂起,我必须用--force运行它。这导致堆转储似乎缺少大量数据,或者至少缺少它们之间的引用。为了进行分析,我尝试了jhat,它提供了很多数据,但在如何解释它方面却没有太多。其次,我尝试了基于eclipse的内存分析工具(http://www.eclipse.org/mat/),它表明堆主要是与tomcat相关的类。

问题在于 jmap 没有报告应用程序的实际状态,只是在关闭时捕获类,其中大部分是 tomcat 类。

我又尝试了几次,注意到模型对象的数量非常高(实际上比数据库中标记为公共的多2-3倍)。

使用此功能,我分析了慢速查询日志和一些不相关的性能问题。我尝试了超延迟加载(http://docs.jboss.org/hibernate/core/3.3/reference/en/html/performance.html),以及用直接jdbc查询替换一些休眠操作(主要是在处理加载和操作大型集合的地方 - jdbc替换只是直接在连接表上工作),并替换了mysql正在记录的其他一些低效查询。

这些步骤提高了前端性能,但仍然没有解决泄漏问题,应用程序仍然不稳定并且行为不可预测。

最后,我找到了选项:-XX:+HeapDumpOnOutOfMemoryError。这最终产生了一个非常大的(~6.5GB)hprof文件,可以准确地显示应用程序的状态。具有讽刺意味的是,该文件是如此之大,以至于jhat无法对其进行分析,即使在具有16gb RAM的盒子上也是如此。幸运的是,MAT能够生成一些漂亮的图表,并显示一些更好的数据。

这一次,一个石英线程占据了6GB堆中的4.5GB,其中大部分是休眠的StatefulPersistenceContext(https://www.hibernate.org/hib_docs/v3/api/org/hibernate/engine/StatefulPersistenceContext.html)。这个类被休眠内部用作其主缓存(我已经禁用了EHCache支持的二级和查询缓存)。

这个类用于启用休眠的大多数功能,因此无法直接禁用它(您可以直接解决它,但spring不支持无状态会话),如果这在成熟产品中有如此严重的内存泄漏,我会感到非常惊讶。那么为什么它现在会泄漏呢?

嗯,这是一个组合:石英线程池实例化,某些东西是threadLocal,spring注入了一个会话工厂,在石英线程生命周期开始时创建一个会话,然后重用该会话来运行使用休眠会话的各种quartz作业。然后,休眠在会话中缓存,这是其预期行为。

问题在于线程池从未释放会话,因此休眠在会话的生命周期内保持驻留并维护缓存。由于这是使用弹簧休眠模板支持,因此没有显式使用会话(我们使用的是dao ->管理器->驱动程序->石英作业层次结构,dao通过spring注入休眠配置,因此操作直接在模板上完成)。

因此,会话从未被关闭,休眠状态维护对缓存对象的引用,因此它们永远不会被垃圾回收,因此每次运行新作业时,它只会继续填充线程本地的缓存,因此不同作业之间甚至没有任何共享。此外,由于这是一个写入密集型作业(很少读取),缓存大部分被浪费,因此不断创建对象。

解决方案:创建一个显式调用 session.flush() 和 session.clear() 的 dao 方法,并在每个作业开始时调用该方法。

该应用程序已经运行了几天,没有监视问题,内存错误或重新启动。

感谢大家在这方面的帮助,这是一个非常棘手的错误,因为一切都在做它应该做的事情,但最终一个3行的方法设法解决了所有问题。


答案 2

能否在启用 JMX 的情况下运行生产机箱?

-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=<port>
...

使用 JMX 进行监控和管理

然后附加JConsole,VisualVM

可以使用jmap进行堆转储吗?

如果是,你可以使用JProfiler(你已经有),jhat,VisualVM,Eclipse MAT来分析堆转储中的泄漏。还要比较可能有助于查找泄漏/模式的堆转储。

正如你提到的雅加达公地。使用 jakarta-commons-logging 时,存在一个与保持类装入器相关的问题。为了好好阅读该检查

内存泄漏猎人生活中的一天release(Classloader))


推荐