追踪Java中的内存泄漏和垃圾回收问题

17 浏览
0 Comments

追踪Java中的内存泄漏和垃圾回收问题

这是一个我已经试图解决几个月的问题。我有一个Java应用程序在运行,处理XML源,然后将结果存储在数据库中。存在一些间歇性的资源问题,非常难以跟踪。

背景:

在生产环境中(问题最明显的地方),我无法很好地访问服务器,并且一直未能运行Jprofiler。该服务器是一个64位四核、8GB内存,运行CentOS 5.2、Tomcat6和Java 1.6.0.11的机器。使用以下Java选项启动:

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
  • Tomcat 6
  • Spring/WebMVC 2.5
  • Hibernate 3
  • Quartz 1.6.1
  • DBCP 1.2.1
  • Mysql 5.0.45
  • Ehcache 1.5.0
  • (当然还有许多其他依赖项,特别是Jakarta-commons库)

我能够接近重现这个问题的机器是一个32位机器,内存需求较低。我可以控制这台机器。我已经用JProfiler检查过这个问题,并解决了许多性能问题(同步问题,预编译/缓存XPath查询,减少线程池,去掉不必要的Hibernate预取和过于热衷于“缓存预热”在处理期间)。

在每种情况下,分析器都显示这些操作占用了巨大的资源,原因各不相同,而这些操作在更改后不再是主要资源猪。

问题:

JVM似乎完全忽略了内存使用设置,填满所有内存并变得无响应。这是客户面临的问题,他们期望定期轮询(每5分钟一次,每分钟重试一次),以及我们的运营团队,他们不断收到通知说某台机器已经不响应并且必须重新启动。在这个服务器上没有其他重要的内容运行。

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

然而,使用jprofiler运行时,单击“运行GC”按钮似乎可以很好地清除内存而不是显示逐步增加的占用空间,但由于我无法直接将jprofiler连接到生产服务器,且解决确定的热点问题似乎没有奏效,我只能盲目地调整Garbage Collection。

我尝试过:

- 分析和修复热点。

- 使用STW、Parallel和CMS垃圾收集器。

- 将最小/最大堆大小设置为1/2、2/4、4/5、6/6等级。

- 将permgen空间设置为256MB的增量,最多可达1GB。

- 上述各种组合。

- 我还参考了JVM调优参考资料,但是找不到解释这种行为或任何在这种情况下使用哪些调整参数的示例。

- 我还(不成功地)尝试了离线模式下的jprofiler,连接jconsole、visualvm,但似乎找不到任何可以解释我的GC日志数据的东西。

不幸的是,这个问题也会不定期地出现,似乎是不可预测的,它可以连续运行几天甚至一周而没有任何问题,或者在一天内失败40次,我唯一可以准确捕捉的就是垃圾回收出了问题。

有人能就以下问题提供一些建议吗:

a)为什么JVM在配置为最大不超过6时,使用了8个物理gig和2个GB的交换空间。

b)提供关于GC调优的参考资料,实际上解释或提供什么样的设置和高级集合使用的合理示例。

c)提供最常见的Java内存泄漏的参考资料(我了解未声明的引用,但我指的是在库/框架级别或数据结构中更固有的东西,如哈希映射等)。

谢谢您能提供的任何和所有见解。

编辑

Emil H:

1)是的,我的开发集群是生产数据的镜像,与介质服务器相同。 主要区别在于32/64位和可用的RAM数量,这很难复制,但代码、查询和设置是相同的。

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

3)另一个需要注意的是,其他主要消费者是来自JAI的图像操作(从源重新处理图像)。我不熟悉Java的图形库,但从我找到的资料中,它们并不特别“不漏”。

感谢目前为止的回答!

更新:

我能够使用VisualVM连接到生产实例,但它已禁用了GC可视化/运行GC选项(尽管我可以在本地查看它)。有趣的是:VM的堆分配遵循了JAVA_OPTS,并且实际分配的堆稳定在1-1.5吉字节左右,似乎没有泄漏,但机器级别的监视仍然显示出泄漏的模式,但在VM监视中没有反映出来。此箱中没有其他运行的程序,所以我被难住了。

admin 更改状态以发布 2023年5月24日
0
0 Comments

你能使用启用了JMX的生产环境吗?

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

使用JMX进行监控和管理

然后使用JConsole、VisualVM进行连接?

使用jmap进行堆转储是否可行?

如果是的话,您可以使用JProfiler(您已经有了),jhat,VisualVM,Eclipse MAT等工具来分析堆转储以寻找泄漏。此外,还可以比较堆转储,这可能有助于发现泄漏/模式。

正如您所提到的Jakarta Commons。使用Jakarta Commons Logging时,与持有类加载器相关的问题。有关此内容的详细信息,请查看

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

0
0 Comments

好的,我最终找到了引起这个问题的原因,并且我会发布详细的答案,以防其他人也遇到这些问题。

我在进程出现问题时尝试了 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文件,准确显示了应用程序的状态。具有讽刺意味的是,文件太大了,以至于即使在具有16GB RAM的盒子上,jhat也无法分析它。幸运的是,MAT能够生成一些好看的图形并显示一些更好的数据。

这一次,显眼的是单个quartz线程占用了6GB堆中的4.5GB,其中大部分是由hibernate StatefulPersistenceContext(https://www.hibernate.org/hib_docs/v3/api/org/hibernate/engine/StatefulPersistenceContext.html)所占用。这个类在hibernate内部用作主要缓存(我已经禁用了由EHCache支持的二级和查询缓存)。

这个类用于启用大多数Hibernate的功能,因此它不能直接禁用(你可以直接解决它,但Spring不支持无状态会话),如果这在成熟的产品中有如此重大的内存泄漏,我会非常惊讶。那么它为什么现在泄漏了呢?

嗯,这是一些事情的结合:
quartz线程池实例化时有一些东西被视为threadLocal,Spring注入了一个会话工厂,该工厂在quartz线程生命周期的开始处创建一个会话,然后被重用以运行使用hibernate会话的各种quartz作业。然后Hibernate会在会话中缓存,这是其预期的行为。问题在于线程池从未释放会话,因此Hibernate保持驻留状态并维护缓存的生命周期。由于这是使用Spring的Hibernate模板支持,没有明确使用会话(我们使用dao-> manager-> driver->Quartz-job层次结构,dao通过Spring注入Hibernate配置,因此操作直接在模板上完成)。

因此,会话从未被关闭,Hibernate保持对缓存对象的引用,因此它们从未被垃圾回收,因此每次运行新作业时,它只会继续填充本地线程的缓存,因此甚至没有在不同的作业之间进行共享。此外,由于这是一个写密集型作业(几乎没有读取),缓存大部分是浪费的,因此对象不断被创建。

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

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

感谢大家在此方面的帮助,这是一个相当棘手的错误追踪,因为一切都按照它应该做的那样进行,但最后一个三行代码的方法成功解决了所有问题。

0