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


79

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

背景: 在生产包装盒(问题最明显的地方)上,我对包装盒的访问不是特别好,并且无法使Jprofiler运行。那个盒子是运行centos 5.2,tomcat6和java 1.6.0.11的64位四核8gb机器。它以这些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
  • 高速缓存1.5.0
  • (当然还有许多其他依赖项,特别是jakarta-commons库)

我最能重现该问题的是内存需求较低的32位计算机。我确实可以控制。我已经使用JProfiler对其进行了探究,并修复了许多性能问题(同步问题,预编译/缓存xpath查询,减少线程池,删除不必要的休眠预取以及处理过程中过度的“缓存变暖”)。

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

问题: JVM似乎完全忽略了内存使用设置,填满了所有内存并且变得无响应。这对于面对最终客户的客户来说是个问题,他们希望定期进行轮询(每5分钟一次,然后重试1分钟),对于我们的运营团队来说,这是不断得到通知的,盒子已变得没有响应,必须重新启动它。此框上没有其他可运行的东西。

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

但是,使用jprofiler运行时,“运行GC”按钮似乎可以很好地清理内存,而不是显示增加的占用空间,但是由于我无法将jprofiler直接连接到生产盒,并且无法解决已证明的热点问题,因此我正在使用剩下的是将Garbage Collection调为盲人的巫毒教。

我试过的

  • 分析和修复热点。
  • 使用STW,Parallel和CMS垃圾收集器。
  • 以最小/最大堆大小以1 / 2、2 / 4、4 / 5、6 / 6增量运行。
  • 以256M的permgen空间运行时,最大增量为1Gb。
  • 以上的许多组合。
  • 我还参考了JVM [调整参考](http://java.sun.com/javase/technologies/hotspot/gc/gc_tuning_6.html),但实际上找不到解释此行为的任何信息或_which_调整的任何示例在这种情况下使用的参数。
  • 我也(未成功)在脱机模式下尝试了jprofiler,并与jconsole,visualvm连接,但是我似乎找不到能影响我的gc日志数据的任何东西。

不幸的是,该问题还偶尔出现,它似乎是无法预测的,它可以运行几天甚至一周,而不会出现任何问题,或者一天可能失败40次,而我唯一能持续发现的问题就是垃圾收集正在起作用。

任何人都可以给予任何意见,以:
一)为什么一个JVM使用8场物理音乐会和2 GB的交换空间时,它被配置为最大出在小于6
b)至GC调谐的引用实际上解释或给出合理的例子与高级集合一起使用的时间和设置类型。
c)对最常见的Java内存泄漏的引用(我理解无人认领的引用,但我的意思是在库/框架级别,或者数据结构中更inherenet的内容,例如哈希映射)。

感谢您提供的所有见解。

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

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

3)另外,另一个主要使用者是来自JAI的图像操作(重新处理来自提要的图像)。我对Java的图形库不熟悉,但是从我发现它们并不是特别泄漏。

(感谢您到目前为止的回答,伙计们!)

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


您是否使用真实世界的数据和真实世界的数据库进行测试?最好是生产数据的副本?
Emil H

4
+1-这是我读过的最好的问题之一。我希望我能提供更多帮助。我将回到这个话题,看看是否有人可以说些聪明的话。
duffymo

另外,您正在使用什么XML解析器?
Emil H

您是否查看了分配的ByteBuffer的数量以及分配它们的人员?
肖恩·麦考夫

检查以下答案:stackoverflow.com/a/35610063,它具有有关Java本机内存泄漏的详细信息。
Lari Hotari

Answers:


92

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

我在进程运行时尝试了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)。此类在内部由hibernate用作其主缓存(我已禁用了由EHCache支持的第二级和查询缓存)。

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

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

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

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

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

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

感谢大家对此的帮助,这是一个非常棘手的错误,因为所有事情都按照预期的方式进行了跟踪,但是最后通过3行方法成功解决了所有问题。


13
调试过程非常顺利,感谢您跟踪并发布解决方案。
鲍里斯·特尔齐克

1
谢谢你的解释。在批处理读取(SELECT)方案中,我遇到了类似的问题,这导致StatefulPersistenceContext变得很大。我无法像主要循环方法那样运行em.clear()或em.flush()@Transactional(propagation = Propagation.NOT_SUPPORTED)。通过将传播更改为Propagation.REQUIRED,并调用em.flush / em.clear()来解决此问题。
Mohsen

3
我不明白的一件事:如果会话从未刷新过,这意味着没有实际数据保存到DB。难道不是在应用程序中其他地方检索了这些数据,以便您可以看到它丢失了吗?
yair

1
为StatefulPersistenceContext提供的链接已断开。现在是docs.jboss.org/hibernate/orm/4.3/javadocs/org/hibernate/engine/…吗?
Victor Stafusa

1
利亚姆,非常感谢 我有同样的问题,并且MAT指向休眠statefulPersistentContext。我想通过阅读您的文章,我可以获得足够的提示。感谢您提供如此精彩的信息。
Reddymails 2014年

4

您可以在启用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)生命中的一天


1)我实际上已经在今天尝试了visualvm和其他一些工具,但是需要正确打开端口。2)我在上一份工作中看到了c-logging问题,实际上,这个问题使我想起了这一问题。公司范围内的服务经常崩溃,并且被跟踪到已知的公地泄漏,我相信这与您所链接的相似。我试图将大多数日志记录保留为log4j,但是对于需要commons包的依赖项目,我没有太多选择。我们也有一些使用simpleFacade的类,我现在在看是否可以使事情更加一致。
liam

4

似乎堆以外的内存正在泄漏,您提到堆保持稳定。经典的候选者是permgen(永久代),它由两部分组成:加载的类对象和插入的字符串。既然你报告已经与VisualVM的连接,你应该能够显得装入类的数量,如果有继续增加装载的类(重要的VisualVM也说明以往的类加载总量,这没关系,如果这个上升,但加载的类的数量应在一定时间后稳定下来)。

如果结果证明确实是一个permgen泄漏,则调试会变得更加棘手,因为与堆相比,permgen分析的工具相当缺乏。最好的选择是在服务器上启动一个反复执行(每个小时?)的小脚本:

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

带有该参数的jmap会生成已加载类的概述以及它们的大小(以字节为单位)的估计值,此报告可以帮助您确定某些类是否未卸载。(注意:我的意思是进程ID,应该是一些生成的时间戳以区分文件)

一旦确定了某些类正在加载而不是卸载,就可以在脑海中弄清楚它们的生成位置,否则可以使用jhat分析由jmap -dump生成的转储。如果您需要此信息,我会保留以供将来更新。


好建议。今天下午我会尝试的。
liam

jmap没有帮助,但是很接近。查看完整答案以获取解释。
liam

2

我会寻找直接分配的ByteBuffer。

来自javadoc。

可以通过调用此类的allocateDirect工厂方法来创建直接字节缓冲区。这种方法返回的缓冲区通常比非直接缓冲区具有更高的分配和释放成本。直接缓冲区的内容可能驻留在普通垃圾回收堆之外,因此它们对应用程序内存占用的影响可能并不明显。因此,建议直接缓冲区主要分配给大型,寿命长的缓冲区,这些缓冲区要受基础系统的本机I / O操作的约束。通常,最好仅在直接缓冲区产生可衡量的程序性能提升时才分配它们。

也许Tomcat代码使用此方法进行I / O;将Tomcat配置为使用其他连接器。

失败的话,您可能会有一个线程定期执行System.gc()。尝试使用“ -XX:+ ExplicitGCInvokesConcurrent”可能是一个有趣的选择。


1)当您说连接器时,是指数据库连接器还是其他IO绑定类?就个人而言,即使c3p0非常匹配,我也不愿花费精力来引入新的连接池,但是有可能将其放入。2)我没有遇到显式GC标志,但是我一定会考虑的。但是,这感觉有点骇人听闻,并且使用这种大小的遗留代码库,我正在尝试摆脱这种方法。(例如:几个月前,我不得不追踪只是产生线程的副作用的几个位置。现在对线程进行合并)。
liam

1)自从我配置了tomcat已经有一段时间了。它确实有一个称为连接器的概念,因此您可以将其配置为侦听来自Apache httpd的请求或直接侦听HTTP。在某个时候,有一个NIO http连接器和一个基本的HTTP连接器。您可能会看到NIO HTTP连接器有哪些配置选项,或者是否只有唯一的基本连接器可用。2)您只需要定期调用System.gc()的线程,也可以重用时间线程。是的,这完全是骇人听闻的。
肖恩·麦考夫

请参阅stackoverflow.com/questions/26041117/…以调试本机内存泄漏。
拉里·霍塔里

1

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

另外,我发现JDK 6附带的visualgc是查看内存中发生的事情的好方法。它很好地显示了GC的eden,generation和perm空间以及瞬态行为。您需要的只是过程的PID。在您使用JProfile时,这可能会有所帮助。

那么Spring跟踪/记录方面又如何呢?也许您可以编写一个简单的方面,声明式地应用它,然后以这种方式进行穷人的剖析。


1)我正在与SA一起尝试打开远程端口,并且我将尝试基于本机java / jmx的工具(我尝试了一些工具,包括jprofiler,这是个很棒的工具!适当的系统级库在那里)。2)即使从春季开始,我也非常警惕面向方面的事物。以我的经验,即使依赖于此也会使事情变得更加混乱并且更难以配置。不过,如果没有其他办法,我会牢记这一点。
liam

1

“不幸的是,这个问题还偶尔出现,它似乎是无法预测的,它可以运行几天甚至一周,而不会出现任何问题,或者一天可以失败40次,而我唯一能始终抓住的问题是垃圾收集正在发挥作用。”

听起来,这与一个用例绑定,该用例每天最多执行40次,然后几天不再执行。希望您不仅跟踪症状。这一定是可以通过跟踪应用程序参与者(用户,工作,服务)的行为来缩小的范围。

如果通过XML导入发生这种情况,则应将40个崩溃日的XML数据与在零崩溃日导入的数据进行比较。也许这是某种逻辑问题,您只能在代码内部找不到。


1

我遇到了相同的问题,但有几个区别。

我的技术如下:

grails 2.2.4

雄猫7

石英插件1.0

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

还要考虑的另一件事是,石英插件会像@liam所说的那样在石英线程中注入休眠会话,并且石英线程仍然存在,直到我完成应用程序。

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

Quartz插件具有一个监听器,用于初始化和销毁​​休眠会话

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实施而增加了两倍,每个数据源增加了一倍。

在此之前,我只介绍这种情况。

问题来了...

完成我的石英作业后,该插件将调用侦听器刷新并销毁休眠会话,就像在的源代码中可以看到的那样SessionBinderJobListener

刷新发生得很好,但是销毁没有,因为HibernatePersistence,请在关闭休眠会话之前进行一次验证...,它会检查nestingCount该值是否大于1。如果答案为是,则他不会关闭会话。

简化Hibernate所做的工作:

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

那是我内存泄漏的基础。Quartz线程对于会话中使用的所有对象仍然有效,因为Grails ORM无法关闭会话,这是由于我有两个数据源而导致的错误。

为了解决这个问题,我自定义了侦听器,在销毁之前调用clear,并两次调用destroy(每个数据源一个)。确保我的会议是明确的并被销毁,如果销毁失败,至少他是明确的。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.