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

时间:2009-07-01 22:15:19

标签: java memory-leaks garbage-collection profiling

7 个答案:

答案 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 hunterrelease(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两次(每个数据源一个)。确保我的会话清晰并被破坏,如果破坏失败,他至少是清楚的。