答案 0 :(得分:90)
好吧,我终于找到了造成这个问题的问题,我发布了一个详细的答案,以防其他人遇到这些问题。
我在进程正在执行时尝试了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查询替换一些hibernate操作(主要是在处理大型集合上的加载和操作的地方 - 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)。 hibernate在内部使用此类作为其主缓存(我已禁用由EHCache支持的第二级和查询缓存)。
这个类用于启用hibernate的大部分功能,所以它不能直接禁用(你可以直接解决它,但是spring不支持无状态会话),如果这个我会很惊讶在成熟的产品中有如此大的内存泄漏。那么为什么它现在泄漏?
嗯,这是一个组合的东西: 石英线程池实例化了某些东西是threadLocal,spring正在注入会话工厂,这是在石英线程生命周期的开始创建一个会话,然后被重用来运行使用hibernate会话的各种石英作业。然后Hibernate在会话中缓存,这是它的预期行为。
问题是线程池永远不会释放会话,所以hibernate保持驻留并维护会话生命周期的缓存。由于这是使用spring hibernate模板支持,没有明确使用会话(我们使用的是dao - > manager - > driver - > quartz-job层次结构,dao通过spring注入了hibernate配置,所以操作直接在模板上完成。
所以会话永远不会被关闭,hibernate正在维护对缓存对象的引用,因此它们永远不会被垃圾收集,因此每次运行新作业时它都会不断填充线程的本地缓存,所以在不同的工作之间甚至没有分享。此外,由于这是一个写密集型作业(读取很少),缓存主要是浪费,因此对象不断被创建。
解决方案:创建一个显式调用session.flush()和session.clear()的dao方法,并在每个作业开始时调用该方法。
该应用程序已运行了几天,没有监控问题,内存错误或重新启动。
感谢大家对此的帮助,这是一个非常棘手的错误,因为一切都正是按预期完成的,但最后一个3线方法设法解决了所有问题。
答案 1 :(得分:4)
您可以在启用JMX的情况下运行生产盒吗?
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=<port>
...
Monitoring and Management Using JMX
然后附上JConsole,VisualVM?
使用jmap进行堆转储是否可以?
如果是,那么您可以使用JProfiler(您已经拥有),jhat,VisualVM,Eclipse MAT分析堆转储是否泄漏。还要比较可能有助于查找泄漏/模式的堆转储。
正如你提到的jakarta-commons。使用与保持类加载器相关的jakarta-commons-logging时出现问题。有关该支票的详细阅读
A day in the life of a memory leak hunter(release(Classloader)
)
答案 2 :(得分:4)
看起来除了堆之外的内存泄漏,你提到堆保持稳定。经典候选者是permgen(永久代),由2个东西组成:加载类对象和实习字符串。由于您报告已与VisualVM连接,因此如果已加载类继续增加,您应该能够看到已加载的类的数量(重要的是,visualvm还显示了已加载的类的总数,没关系,如果这个增加,但加载的类的数量应该在一定时间后稳定。)
如果它确实是一个permgen泄漏,那么调试变得更加棘手,因为与堆相比,permgen分析的工具相当缺乏。你最好的办法是在服务器上重复启动一个小脚本(每小时?)调用:
jmap -permstat <pid> > somefile<timestamp>.txt
带有该参数的jmap将生成已加载类的概述以及它们的大小(以字节为单位)的估计值,此报告可帮助您确定某些类是否未被卸载。 (注意:我的意思是进程ID,应该是一些生成的时间戳来区分文件)
一旦您将某些类标识为已加载且未卸载,您可以在心理上找出可能生成这些类的位置,否则您可以使用jhat来分析使用jmap -dump生成的转储。如果您需要信息,我会保留以供将来更新。
答案 3 :(得分:2)
我会寻找直接分配的ByteBuffer。
来自javadoc。
可以通过调用此类的allocateDirect工厂方法来创建直接字节缓冲区。与非直接缓冲区相比,此方法返回的缓冲区通常具有更高的分配和解除分配成本。直接缓冲区的内容可能位于正常的垃圾收集堆之外,因此它们对应用程序的内存占用量的影响可能并不明显。因此,建议直接缓冲区主要分配给受基础系统本机I / O操作影响的大型长期缓冲区。通常,最好只在它们在程序性能中产生可测量的增益时才分配直接缓冲区。
也许Tomcat代码将此操作用于I / O;配置Tomcat以使用不同的连接器。
如果没有你可以有一个定期执行System.gc()的线程。 “-XX:+ ExplicitGCInvokesConcurrent”可能是一个有趣的选择。
答案 4 :(得分:1)
任何JAXB?我发现JAXB是一个烫发空间填充物。
另外,我发现现在随JDK 6提供的visualgc是查看内存中发生情况的好方法。它精美地显示了伊甸园,代际和烫发空间以及GC的瞬态行为。您所需要的只是过程的PID。当你使用JProfile时,这可能会有所帮助。
那么Spring跟踪/日志记录方面呢?也许你可以写一个简单的方面,以声明方式应用它,并以这种方式做一个穷人的探查器。
答案 5 :(得分:1)
“不幸的是,问题也偶尔会出现,似乎无法预测,它可以运行数天甚至一周而没有任何问题,或者它可能在一天内失败40次,而且我唯一可以看来持续捕获的是垃圾收集正在起作用。“
听起来,这是一个用例,每天最多执行40次,然后几天不再执行。我希望,你不只是追踪症状。这必须是您可以通过跟踪应用程序的参与者(用户,作业,服务)的操作来缩小范围。
如果通过XML导入发生这种情况,您应该将40个崩溃日的XML数据与在零崩溃日导入的数据进行比较。也许这是某种逻辑问题,只在代码中找不到。
答案 6 :(得分:1)
我遇到了同样的问题,有几点不同......
我的技术如下:
grails 2.2.4
tomcat7
quartz-plugin 1.0
我在我的应用程序上使用了两个数据源。这是一个 特殊性决定了bug的原因..
要考虑的另一件事是石英插件,在石英线程中注入休眠会话,就像@liam所说的那样,石英线仍然活着,直到我完成应用。
我的问题是grails ORM上的错误以及插件处理会话和我的两个数据源的方式。
Quartz插件有一个init的监听器并销毁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
。每个数据源一个。
每次操作都会将AggregatePersistenceContextInterceptor
传递给HibernatePersistence,而不进行任何修改或处理。
当我们在init()
上调用HibernatePersistenceContextInterceptor
时,他会增加静态变量
private static ThreadLocal<Integer> nestingCount = new ThreadLocal<Integer>();
我不知道静态计数的讽刺。我知道他增加了两次,每个数据源增加一次,因为AggregatePersistence
实现了。
直到这里我才解释了这个中心。
问题来了......
当我的石英作业完成时,插件会调用侦听器来刷新和销毁hibernate会话,就像您在SessionBinderJobListener
的源代码中看到的那样。
刷新发生得很完美,但是由于HibernatePersistence
,在关闭休眠会话之前进行了一次验证,因此它没有...它会检查nestingCount
以查看该值是否超过1。如果答案是是的,他没有关闭会议。
简化Hibernate所做的事情:
if(--nestingCount.getValue() > 0)
do nothing;
else
close the session;
这是我内存泄漏的基础.. Quartz线程仍然在会话中使用所有对象,因为grails ORM不会关闭会话,因为我有两个数据源导致的bug。
要解决这个问题,我自定义监听器,在销毁之前调用clear,并调用destroy两次(每个数据源一个)。确保我的会话清晰并被破坏,如果破坏失败,他至少是清楚的。