【问题标题】:Tracking down a memory leak / garbage-collection issue in Java追踪 Java 中的内存泄漏/垃圾收集问题
【发布时间】:2010-11-07 11:35:26
【问题描述】:

这是我几个月来一直试图追查的一个问题。我有一个正在运行的 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
  • 弹簧/WebMVC 2.5
  • 休眠 3
  • 石英 1.6.1
  • DBCP 1.2.1
  • Mysql 5.0.45
  • Ehcache 1.5.0
  • (当然还有许多其他依赖项,尤其是 jakarta-commons 库)

最接近重现该问题的方法是 32 位机器,其内存要求较低。我确实可以控制。我已经用 JProfiler 对它进行了彻底的探索,并修复了许多性能问题(同步问题、预编译/缓存 xpath 查询、减少线程池、删除不必要的休眠预取,以及处理过程中过分热心的“缓存预热”)。

在每种情况下,分析器都显示它们出于某种原因占用了大量资源,并且一旦发生更改,这些就不再是主要的资源消耗。

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

问题似乎是垃圾收集。我们正在使用 ConcurrentMarkSweep(如上所述)收集器,因为原始 STW 收集器导致 JDBC 超时并且变得越来越慢。日志显示,随着内存使用量的增加,即开始引发 cms 故障,并返回到原来的 stop-the-world 收集器,然后似乎无法正确收集。

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

我尝试过的:

  • 分析和修复热点。
  • 使用 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次,而我似乎唯一能做到的持续的问题是垃圾收集正在运行。

谁能给点建议:
a) 为什么 JVM 在配置为最大小于 6 时使用 8 个物理 gig 和 2 GB 交换空间。
b) 对 GC 调整的参考,它实际上解释或给出了合理的示例,说明何时以及使用哪种设置来使用高级集合。
c)对最常见的 java 内存泄漏的引用(我理解无人认领的引用,但我的意思是在库/框架级别,或者在数据结构中更inherenet,如哈希图)。

感谢您提供的所有见解。

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

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

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

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

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

【问题讨论】:

  • 您是否使用真实世界的数据和真实世界的数据库进行测试?最好是生产数据的副本?
  • +1 - 这是我读过的最好的问题之一。我希望我能在帮助方面提供更多帮助。我会回到这个,看看是否有人有聪明的说法。
  • 另外,您使用的是什么 XML 解析器?
  • 您查看分配的 ByteBuffers 的数量以及分配它们的人吗?
  • 查看这个答案:stackoverflow.com/a/35610063,它有关于 Java 原生内存泄漏的详细信息。

标签: java memory-leaks garbage-collection profiling


【解决方案1】:

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

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

问题是 jmap 没有报告应用程序的实际状态,而只是在关闭时捕获类,主要是 tomcat 类。

我又尝试了几次,发现模型对象的数量非常多(实际上是数据库中标记为 public 的 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 内存的盒子上也是如此。幸运的是,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 然后在会话中缓存,这是它的预期行为。

然后问题是线程池从未释放会话,因此休眠保持驻留并为会话的生命周期维护缓存。由于这是使用 springs hibernate 模板支持,因此没有显式使用会话(我们使用的是 dao -> manager -> driver -> quartz-job 层次结构,dao 通过 spring 注入 hibernate 配置,因此操作是直接在模板上完成)。

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

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

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

感谢大家对此的帮助,这是一个很难追踪的错误,因为一切都在按照预期进行,但最终一个 3 行方法设法解决了所有问题。

【讨论】:

  • 您的调试过程很好,感谢您跟进并发布解决方案。
  • 感谢您的精彩解释。我在批量读取 (SELECT) 场景中遇到过类似的问题,导致 StatefulPersistenceContext 变得如此之大。我无法运行 em.clear() 或 em.flush(),因为我的主要循环方法有 @Transactional(propagation = Propagation.NOT_SUPPORTED)。它通过将传播更改为Propagation.REQUIRED 并调用 em.flush/em.clear() 来解决。
  • 我不明白的一件事:如果会话从未刷新,则意味着没有实际数据保存到数据库。这些数据不是在您的应用程序的其他地方检索到的,因此您可以看到它丢失了吗?
  • 为 StatefulPersistenceContext 提供的链接已损坏。现在是docs.jboss.org/hibernate/orm/4.3/javadocs/org/hibernate/engine/… 吗?
  • 利亚姆,非常感谢。我有同样的问题,MAT 指向休眠 statefulPersistentContext。我想通过阅读您的文章,我得到了足够的提示。感谢您提供如此精彩的信息。
【解决方案2】:

您可以在启用 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))

【讨论】:

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

似乎堆以外的内存正在泄漏,您提到堆保持稳定。一个经典的候选者是 permgen(永久代),它由两件事组成:加载的类对象和实习字符串。由于您报告已与 VisualVM 连接,因此您应该能够看到加载的类的数量,如果 加载的 类持续增加(重要的是,visualvm 还显示曾经加载的类的总量,如果这个值上升没关系,但加载的类的数量应该在一段时间后稳定)。

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

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

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

一旦你确定某些类被加载而不是被卸载,你就可以在脑海中弄清楚这些类可能在哪里生成,否则你可以使用 jhat 来分析使用 jmap -dump 生成的转储。如果您需要这些信息,我会保留它以备将来更新。

【讨论】:

  • 好建议。我今天下午试试。
  • jmap 没有帮助,但很接近。查看完整答案以获得解释。
【解决方案4】:

我会寻找直接分配的 ByteBuffer。

来自 javadoc。

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

也许 Tomcat 代码对 I/O 使用了这种做法;配置 Tomcat 以使用不同的连接器。

如果你不能有一个定期执行 System.gc() 的线程。 "-XX:+ExplicitGCInvokesConcurrent" 可能是一个值得尝试的有趣选项。

【讨论】:

  • 1) 当您说连接器时,您指的是 DB 连接器,还是不同的 IO 绑定类?就我个人而言,我宁愿不努力引入新的连接池,即使 c3p0 很接近,但我还是把它作为一种可能性。 2)我没有遇到过明确的GC标志,但我一定会考虑的。不过,这感觉有点骇人听闻,而且由于有这么大的遗留代码库,我正试图摆脱这种方法。 (例如:几个月前,我不得不追踪几个只是产生线程作为副作用的点。线程现在已合并)。
  • 1) 我已经有一段时间没有配置tomcat了。它确实有一个称为连接器的概念,因此您可以将其配置为侦听来自 Apache httpd 的请求或直接侦听 HTTP。在某些时候,有一个 NIO http 连接器和一个基本的 HTTP 连接器。您可能会看到哪些配置选项可用于 NIO HTTP 连接器,或者查看唯一的基本连接器是否可用。 2)您只需要定期调用 System.gc() 的线程,或者您可以重用时间线程。是的,这完全是 hackish。
  • 请参阅stackoverflow.com/questions/26041117/… 以了解调试本机内存泄漏。
【解决方案5】:

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

另外,我发现visualgc(现在随 JDK 6 一起提供)是查看内存中发生情况的好方法。它精美地展示了 eden、generational 和 perm 空间以及 GC 的瞬态行为。您只需要进程的PID。也许这会在您处理 JProfile 时有所帮助。

那么 Spring 跟踪/日志记录方面呢?也许你可以写一个简单的方面,以声明的方式应用它,然后用这种方式做一个穷人的探查器。

【讨论】:

  • 1) 我正在与 SA 合作以尝试打开远程端口,并且我将尝试基于本地 java/jmx 的工具(我尝试了一些,包括 jprofiler - 很棒的工具! - 但是在那里获得适当的系统级库太难了)。 2)我对面向方面的任何事情都非常警惕,即使是从春天开始。根据我的经验,即使依赖它也会使事情变得更加混乱和难以配置。不过,如果没有其他方法,我会记住这一点。
【解决方案6】:

“很遗憾,这个问题也是零星出现的,似乎无法预料,它可以运行几天甚至一周没有任何问题,或者一天可以失败40次,而我唯一能看到的持续捕获是垃圾收集正在发挥作用。”

听起来,这与一个每天最多执行 40 次的用例绑定,然后几天内不再执行。我希望,你不只是跟踪症状。这一定是一些东西,您可以通过跟踪应用程序参与者(用户、作业、服务)的操作来缩小范围。

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

【讨论】:

    【解决方案7】:

    我有同样的问题,但有几个不同..

    我的技术如下:

    grails 2.2.4

    tomcat7

    quartz-plugin1.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,无需任何修改或处理。

    当我们在HibernatePersistenceContextInterceptor 上调用init() 时,他会增加下面的静态变量

    private static ThreadLocal&lt;Integer&gt; nestingCount = new ThreadLocal&lt;Integer&gt;();

    我不知道静态计数的作用。我只知道由于AggregatePersistence 实现,它增加了两次,每个数据源一次。

    直到这里我只是解释了场景。

    问题来了……

    当我的quartz工作完成后,插件会调用监听器来刷新和销毁休眠会话,就像你在SessionBinderJobListener的源代码中看到的那样。

    刷新完美发生,但破坏没有,因为HibernatePersistence,在关闭休眠会话之前进行一次验证...它检查nestingCount以查看值是否大于1。如果答案是肯定的,他不关闭会话。

    简化 Hibernate 所做的工作:

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

    这是我的内存泄漏的基础.. Quartz 线程仍然存在会话中使用的所有对象,因为 grails ORM 没有关闭会话,因为我有两个数据源导致的错误。

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

    【讨论】:

      猜你喜欢
      • 2012-06-28
      • 2018-05-10
      • 1970-01-01
      • 1970-01-01
      • 2016-11-02
      • 1970-01-01
      • 1970-01-01
      • 2012-01-03
      • 1970-01-01
      相关资源
      最近更新 更多