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

这是我一直试图追查几个月的问题。 我有一个Java应用程序运行该进程的XML饲料并将结果存储在数据库中。 有间歇性的资源问题,很难追查。

背景:在生产箱(问题最明显的地方),我没有特别好的访问权限,并且无法运行Jprofiler。 那个盒子是一个64位四核,运行centos 5.2的8gb机器,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
  • hibernate3
  • 石英1.6.1
  • DBCP 1.2.1
  • Mysql 5.0.45
  • Ehcache 1.5.0
  • (当然还有一些其他的依赖项,特别是雅加达公共图书馆)

最近我可以重现这个问题是一个32位的内存要求较低的机器。 我有控制权。 我已经用JProfiler探测死了,并修复了许多性能问题(同步问题,预编译/cachingxpath查询,减less线程池,并消除不必要的hibernate预取,以及在处理过程中overzealous“高速caching预热”)。

在每种情况下,剖析者都将这些资源视为占用了大量资源,并且一旦变化进入,这些资源已经不再是主要资源。

问题: JVM似乎完全忽略了内存使用情况设置,填满了所有的内存并且没有响应。 对于面向客户的客户来说,这是一个问题,他们期望定期进行投票(5分钟和1分钟重试),同样也适用于我们的运营团队,他们经常被告知一个箱子没有响应,必须重新启动。 这个盒子上没有别的重要的东西了。

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

然而,使用jprofiler运行,“运行GC”button似乎很好地清理内存,而不是显示增加的足迹,但由于我不能直接连接到生产框jprofiler,并解决经certificate的热点似乎没有工作我是留下了调整垃圾收集盲目的巫术。

我曾经尝试过:

  • 分析和修复热点。
  • 使用STW,并行和CMS垃圾收集器。
  • 以最小/最大堆大小以1 / 2,2 / 4,4 / 5,6 / 6增量运行。
  • 以256M的增长空间运行,最高可达1Gb。
  • 以上的许多组合。
  • 我也咨询了JVM [调优参考](http://java.sun.com/javase/technologies/hotspot/gc/gc_tuning_6.html),但是不能真正find任何解释这种行为的东西或者_which_ tuning的任何例子在这种情况下使用的参数。
  • 我也(不成功)尝试jprofiler离线模式,连接jconsole,visualvm,但我似乎无法find任何将干扰我的gc日志数据。

不幸的是,这个问题也偶尔出现,似乎是不可预知的,它可以运行几天,甚至一个星期没有任何问题,或者一天可能会失败40次,唯一我能看到的一贯是垃圾收集正在起作用。

任何人都可以提供任何build议:
a)为什么JVM使用8个物理演出和2GB交换空间,当configuration为最大输出小于6时。
b)对GC调整的参考,实际解释或给出合理的例子,说明什么时候使用高级集合以及使用什么样的设置。
c)对最常见的java内存泄漏的引用(我理解未声明的引用,但是我的意思是在库/框架级别,或者是像Hashmaps那样在数据结构中更多的内联网)。

感谢您提供的所有洞察力。

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

2)有一些依赖于JaxB的遗留代码,但在重新sorting作业以避免调度冲突时,我通常会因为每天运行一次就消除执行。 主分析器使用调用java.xml.xpath包的XPath查询。 这是一些热点的来源,因为一个查询没有被预编译,而且两个引用是硬编码的string。 我创build了一个线程安全caching(hashmap),并将对xpath查询的引用分解为最终的静态string,从而显着降低了资源消耗。 查询仍然是处理的一大部分,但应该是因为这是应用程序的主要责任。

3)另外,另一个主要消费者是来自JAI的图像操作(重新处理来自饲料的图像)。 我不熟悉java的graphics库,但从我发现他们不是特别泄漏。

(感谢迄今为止的答案,伙计们!)

更新:
我可以使用VisualVM连接到生产实例,但是它禁用了GC可视化/运行GC选项(尽pipe我可以在本地查看它)。 有趣的是:VM的堆分配是服从JAVA_OPTS的,实际分配的堆坐在1-1.5 gigs,看起来没有泄漏,但是箱级监控仍然显示泄漏模式,但它是没有体现在虚拟机监控中。 这个盒子上没有别的东西在跑,所以我很难过。

那么,我终于find了这个问题,我发布了一个详细的答案,以防别人有这些问题。

我尝试了jmap,但是这通常会导致jvm进一步挂起,我将不得不使用–force来运行它。 这导致堆转储似乎缺less大量的数据,或者至less错过了它们之间的引用。 为了分析,我尝试了jhat,它提供了大量的数据,但没有太多解释它的方式。 其次,我尝试了基于eclipse的内存分析工具( http://www.eclipse.org/mat/ ),它显示堆大部分是与tomcat相关的类。

问题是jmap没有报告应用程序的实际状态,只是捕获closures的类,这主要是tomcat类。

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

使用这个我分析了慢速查询日志和一些无关的性能问题。 我尝试了额外的懒加载( http://docs.jboss.org/hibernate/core/3.3/reference/en/html/performance.html ),以及用直接的jdbc查询replace一些hibernate操作正在处理大集合的加载和操作 – jdbcreplace只是直接在连接表上工作),并replace了一些mysql正在logging的低效查询。

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

最后,我find了选项:-XX:+ HeapDumpOnOutOfMemoryError。 这最终产生了一个非常大的(〜6.5GB)hprof文件,准确地显示了应用程序的状态。 具有讽刺意味的是,这个文件太大了,甚至连一个16gb的文件夹也不能分割。 幸运的是,MAT能够产生一些漂亮的图表,并显示出一些更好的数据。

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

这个类用来启用大部分的hibernate特性,所以不能直接禁用(可以直接解决,但是spring不支持无状态会话),如果这样做的话,我会很惊讶重要的内存泄漏在一个成熟的产品。 那么为什么现在在泄漏呢?

嗯,这是一个事情的组合:石英线程池实例化某些东西是threadLocal,spring是注入一个会话工厂,这是石英线程生命周期开始时创build一个会话,然后被重用来运行使用hibernate会话的各种石英作业。 然后Hibernate在会话中caching,这是它的预期行为。

问题在于线程池从来没有释放会话,所以hibernate保持驻留并保持会话生命周期的caching。 由于这是使用spring hibernate模板的支持,所以没有明确的使用会话(我们使用的是dao – > manager – > driver – > quartz-job hierarchy,dao是通过spring注入hibernateconfiguration的,所以操作是直接在模板上完成)。

所以会话从来没有被closures,hibernate保持对caching对象的引用,所以他们从来没有被垃圾收集,所以每当一个新的作业运行,它只会继续填充caching本地的线程,所以没有任何不同的工作之间的分享。 另外,由于这是一个写入密集型工作(很less阅读),caching大部分被浪费了,所以对象不断被创build。

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

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

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

你能运行JMX启用生产框?

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

使用JMX监视和pipe理

然后附加JConsole, VisualVM ?

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

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

正如你所提到的雅加达 – 普通人。 在使用jakarta-commons-logging相关的类加载器时存在一个问题。 对于这个检查很好的阅读

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

看起来像堆以外的内存在泄漏,你提到堆是保持稳定的。 一个经典的候选人是permgen(永久代),它由2个东西组成:加载的类对象和实际的string。 既然你报告与VisualVM连接,你应该能够看到加载的类的数量,如果加载的类继续增加(重要的是,visualvm也显示了加载的类的总数量,如果这种情况好转,在一定的时间之后,加载的类的数量应该稳定)。

如果它确实是一个permgen泄漏,然后debugging变得更加棘手,因为permgen分析的工具相比堆是相当缺乏。 最好的办法是在服务器上重复启动一个小脚本(每小时?)调用:

 jmap -permstat <pid> > somefile<timestamp>.txt 

具有该参数的jmap将生成一个加载类的概述,以及以字节为单位的估计大小,这个报告可以帮助您识别某些类是否不被卸载。 (注:我的意思是进程ID,应该是一些生成的时间戳来区分这些文件)

一旦你确定某些类被加载并且不被卸载,你可以从心理上找出可能产生这些类的地方,否则你可以使用jhat来分析使用jmap -dump生成的转储。 如果您需要这些信息,我会保留这个以备将来更新。

我会寻找直接分配的ByteBuffer。

从javadoc。

通过调用此类的allocateDirect工厂方法可以创build直接字节缓冲区。 这种方法返回的缓冲区通常比非直接缓冲区具有更高的分配和解除分配成本。 直接缓冲区的内容可能位于正常的垃圾收集堆之外,因此它们对应用程序内存占用的影响可能不明显。 因此,build议将直接缓冲区主要分配给受底层系统本地I / O操作影响的大型,长寿命缓冲区。 一般来说,只有在程序性能得到可测量的增益时才最好分配直接缓冲区。

也许Tomcat代码使用这个做I / O; configurationTomcat以使用不同的连接器。

如果没有,你可以有一个定期执行System.gc()的线程。 “-XX:+ ExplicitGCInvokesConcurrent”可能是一个有趣的select。

任何JAXB? 我发现JAXB是一个烫发空间填充器。

另外,我发现现在随JDK 6一起提供的visualgc是查看内存中发生的事情的好方法。 它精美地显示了伊甸园,世代和烫发空间以及GC的瞬态行为。 所有你需要的是过程的PID。 也许这将有助于你在JProfile上工作。

那么Spring的跟踪/日志方面呢? 也许你可以写一个简单的方面,声明性地应用它,并以这种方式做一个穷人的分析器。

“不幸的是,这个问题也偶尔出现,似乎是不可预知的,它可以运行几天,甚至一个星期,没有任何问题,或者一天可能失败40次,而且我唯一能够一致的是垃圾收集行动起来。“

听起来,这是一个用例,每天执行40次,然后不再是几天。 我希望,你不要只跟踪症状。 这必须是一些东西,可以通过跟踪应用程序的angular色(用户,作业,服务)的行为来缩小范围。

如果XML导入发生这种情况,则应该将40天崩溃date的XML数据与零日崩溃时导入的数据进行比较。 也许这是某种逻辑问题,你不能在你的代码里面find。

我有同样的问题,有几个分歧..

我的技术如下:

grails 2.2.4

tomcat7

石英插件 1.0

我在我的应用程序上使用两个数据源。 这是错误原因的特殊性决定因素。

另一件要考虑的事情是石英插件,像石英线一样注入hibernate会话,就像@liam说的,石英线仍然活着,直到我完成应用程序。

我的问题是grails ORM与插件处理会话和我的两个数据源的方式相结合的错误。

Quartz插件有一个侦听器来初始化并销毁hibernate会话

 public class SessionBinderJobListener extends JobListenerSupport { public static final String NAME = "sessionBinderListener"; private PersistenceContextInterceptor persistenceInterceptor; public String getName() { return NAME; } public PersistenceContextInterceptor getPersistenceInterceptor() { return persistenceInterceptor; } public void setPersistenceInterceptor(PersistenceContextInterceptor persistenceInterceptor) { this.persistenceInterceptor = persistenceInterceptor; } public void jobToBeExecuted(JobExecutionContext context) { if (persistenceInterceptor != null) { persistenceInterceptor.init(); } } public void jobWasExecuted(JobExecutionContext context, JobExecutionException exception) { if (persistenceInterceptor != null) { persistenceInterceptor.flush(); persistenceInterceptor.destroy(); } } } 

在我的例子中, persistenceInterceptor实例AggregatePersistenceContextInterceptor ,它有一个HibernatePersistenceContextInterceptor列表。 每个数据源一个。

每个Opertion都使用AggregatePersistenceContextInterceptor其传递给HibernatePersistence,而不做任何修改或处理。

当我们调用HibernatePersistenceContextInterceptor上的init() ,会增加下面的静态variables

private static ThreadLocal<Integer> nestingCount = new ThreadLocal<Integer>();

我不知道静态计数的倾向。 我只知道他是因为AggregatePersistence实现而增加了两次,每个数据源一次。

在此之前,我只是解释一下这个问题。

现在的问题来了…

当我的石英工作完成后,插件调用侦听器刷新和销毁hibernate会话,就像在SessionBinderJobListener源代码中看到的一样。

刷新完全发生,但是破坏不是,因为HibernatePersistence在closureshibernate会话之前做了一个validation。它检查nestingCount ,看看这个值是否超过1.如果答案是肯定的,他不closures会话。

简化Hibernate做了什么:

 if(--nestingCount.getValue() > 0) do nothing; else close the session; 

这是我的内存泄漏的基础。石英线程仍然活着与会话中使用的所有对象,因为grails ORM不会closures会话,因为由于我有两个数据源造成的错误。

为了解决这个问题,我定制了监听器,在destroy之前调用clear,并且调用destroy两次(每个数据源一次)。 确保我的会议是清楚的,销毁的,如果破坏失败,至less他是清楚的。