使用Eclipse MAT查找内存泄漏

也许,所有参与商业项目的Java开发人员迟早都会面临内存泄漏的问题,这将导致应用程序性能降低,并且几乎不可避免地导致众所周知的OutOfMemoryError。本文将考虑这种情况的真实示例,以及如何使用Eclipce Memory Analyzer查找原因。



介绍



内存泄漏通常被称为一种情况,即在应用程序的长期运行过程中,堆中的已占用内存量增加,而在垃圾回收器退出后,该数量没有减少。如您所知,jvm内存分为堆和堆栈。堆栈在流的上下文中存储简单类型的变量的值和对对象的引用,而堆则存储对象本身。同样在堆中还有一个称为Metaspace的空间,该空间存储有关加载的类的数据和绑定到类本身的数据,而不是它们的实例,特别是静态变量的值。由Java机器定期启动的垃圾收集器(以下称为GC)在堆中查找不再引用的对象,并释放这些对象占用的内存。 GC工作算法是不同且复杂的,特别是,下次GC启动时,它不会每次都“检查”整个堆以查找未使用的对象,因此不值得依赖这样的事实,即一个GC启动之后,将从内存中删除更多未使用的对象,但是如果应用程序使用的内存量稳定在很长一段时间内没有明显的原因增长,然后是时候考虑可​​能导致这种情况的原因了。



jvm包括多功能实用程序Visual VM(以下称为VM)。 VM使您可以直观地观察图中jvm关键指标的动态,特别是堆中的可用内存和已占用内存量,已加载类,线程的数量等。此外,使用VM,您可以获取并检查内存转储。当然,VM还允许线程转储和应用程序概要分析,但是这些功能的概述不在本文讨论范围之内。在此示例中,我们从VM所需要做的就是连接到虚拟机,并首先查看内存使用情况的概况。我想指出,要将虚拟机连接到远程服务器,必须在其上配置jmxremote参数,因为连接是通过jmx进行的。有关这些参数的描述,您可以参考Oracle官方文档或有关Habré的大量文章。



因此,假设我们已使用VM成功连接到应用程序服务器,并查看了图表。







在堆选项卡上,您可以查看jvm的总内存和已用内存。应当注意,此选项卡还考虑了元空间类型的内存(以及其他原因,因为这也是一个堆)。 “ Metaspace”选项卡仅显示有关元数据占用的内存的信息(通过类本身和绑定到它们的对象)。



查看该图,我们可以看到总堆内存为〜10GB,当前占用的空间为〜5.8GB。图中的隆起对应于GC调用,大约10:18开始的几乎直线(没有隆起)可以(但不一定!)表示从那时起应用程序服务器几乎处于混乱状态,因为没有活动的分配和释放记忆。通常,此图对应于应用程序服务器的正常运行(当然,如果仅从内存判断工作)。问题图将是一条没有脊的水平水平蓝线大约在橙色线处的图,它表示堆中的最大内存量。



现在,让我们看看另一个图形。







在这里,我们直接对示例进行分析,这是本文的主题。 “类”图显示了加载到Metaspace中的类数,该类数约为73000个对象。我想提请您注意以下事实:我们不是在谈论类实例,而是在谈论类本身,即Class <?>类型的对象。从图中不清楚,每个单独类型ClassA或ClassB的实例被加载到内存中的数量。也许出于某种原因,ClassA类型的相同类的数量成倍增加了?我必须说,在下面将描述的示例中,73,000个唯一类是绝对正常的情况。



事实是,在本文作者参与的一个项目中,开发了一种机制来对领域实体进行通用描述(例如在1C中),称为字典系统,而分析师则为特定客户或特定业务领域定制系统,有机会通过特殊的编辑器,通过创建新的和更改的现有实体来模拟业务模型,这些实体不是在表级别上操作,而是具有“文档”,“帐户”,“员工”等概念。系统内核在关系DBMS中为实体数据创建了表,并且可以为每个实体创建多个表,因为通用系统允许历史上存储属性值,并且还需要在数据库中创建其他服务表。



我相信那些需要使用ORM框架的人已经猜到了作者的意思,而通过谈论表而分散了本文的主要主题。该项目使用了Hibernate,并且对于每个表都必须有一个Entity bean类。同时,由于新表是由分析师在系统工作期间动态创建的,因此生成了Hibernate bean类,而不是由开发人员手动编写的。下一代,大约创建了5万至6万个新类。系统中的表数量少得多(约5到6000),但是对于每个表,不仅生成了Entity bean类,而且还生成了许多辅助类,最终导致了一个共同的数字。



工作机制如下。在系统启动时,实体bean类和辅助类(以下简称为bean类)是基于数据库中的元数据生成的。当系统运行时,Hibernate会话工厂创建会话,会话创建Bean类对象的实例。更改结构(添加,更改表)时,将重新生成Bean类并创建一个新的会话工厂。再生后,新工厂创建了使用新bean类的新会话,关闭了旧工厂和会话,并且由于不再从Hibernate基础结构对象中引用旧的bean类,因此GC将其卸载。











在某个时候,出现了一个问题,即在每次下一次再生之后,箱类的数量开始增加。显然,这是由于没有从内存中卸载出于某些原因不再使用的旧类集这一事实。为了了解系统出现这种行为的原因,我们使用了Eclipse Memory Analizer(MAT)。



查找内存泄漏



MAT能够处理内存转储,发现其中的潜在问题,但是首先您需要获取此内存转储,但是在实际环境中,获取转储有某些细微差别。



删除内存转储



如上所述,可以通过按







But按钮直接从VM中删除内存转储,由于转储的大小很大,VM可能根本无法处理此任务,因此在按下Heap Dump按钮后冻结了一段时间。此外,根本不可能通过jmx连接到VM所需的产品应用程序服务器。在这种情况下,另一个名为jMap的jvm实用程序将为我们提供帮助。它直接在运行jvm的服务器上在命令行上运行,并允许您设置其他转储参数:



jmap -dump:live,format = b,file = / tmp / heapdump.bin 14616 –dump:live



参数非常重要,因为允许您显着减小其大小,排除不再引用的对象。



另一个常见情况是,由于jvm本身因OutOfMemoryError崩溃而无法进行手动转储。在这种情况下,将提供-XX:+ HeapDumpOnOutOfMemoryError选项,此外,还有-XX:HeapDumpPath,它允许您指定捕获的转储的路径。



接下来,使用Eclipse Memory Analizer打开捕获的转储。该文件的大小可能很大(几个GB),因此您需要在MemoryAnalyzer.ini文件中提供足够的内存:



-Xmx4096m



使用MAT定位问题



因此,让我们考虑一种情况,即已加载的类的数量与初始级别相比增加了几倍,即使在强制调用垃圾回收之后也不会减少(这可以通过按VM中的相应按钮来完成)。



上面,从概念上描述了重新生成bean类的过程及其使用。从技术上讲,它看起来像这样:

 

  • 所有Hibernate会话均已关闭(SessionImpl类)
  • 关闭了旧的会话工厂(SessionFactoryImpl),并重置了LocalSessionFactoryBean对它的引用
  • 重新创建ClassLoader
  • 在生成器类中对旧Bean类的引用将无效
  • Bean类被重新生成


在没有对旧bean类的引用的情况下,垃圾收集后类的数量不应增加。



启动MAT并打开以前获得的内存转储文件。打开转储后,MAT将显示内存中最大的对象链。







单击“ Leak Suspects”之后,我们将看到详细信息:



 265 M的圆圈的2个段分别是SessionFactoryImpl的2个实例。目前尚不清楚为什么有两个实例,并且很可能每个实例都包含对实体Bean类的完整集合的引用。 MAT会如下告知我们潜在的问题。





 

我立即注意到问题可疑3并不是一个真正的问题。该项目已实现了自己的语言解析器,该解析器是SQL上的多平台附加组件,它不支持对表进行操作,而可以对系统实体进行操作,并且121M占用其查询缓存。



让我们回到SessionFactoryImpl的两个实例。单击Duplicate Classes,然后查看每个Entity Bean类确实有2个实例。也就是说,到Entity Bean的旧类的链接仍然保留,并且很可能是来自SesssionFactoryImpl的链接。基于此类的源代码,对Bean类的引用应存储在classMetaData字段中。



单击“问题可疑对象1”,然后在SessionFactoryImpl类上,从上下文菜单中选择“列表对象”->“带有出站引用”。这样,我们可以看到SessionFactoryImpl引用的所有对象。







展开classMetaData对象,并确保它实际上存储了Entity bean类的数组。







现在,我们需要了解阻止垃圾收集器处置SessionFactoryImpl单个实例的原因。如果返回“泄漏可疑”->“泄漏->问题可疑1”,我们将看到一堆链接,这些链接都指向SessionFactoryImpl。



 



我们看到包含HTTP会话上下文的SessionInfoImpl bean的entityManager变量包含一个dbTransactionListeners数组,该数组使用SessionImpl Hibernate会话对象作为键,并且这些会话引用SessionFactoryImpl。



事实是,出于某些目的,会话对象已缓存在dbTransactionListeners中,并且在重新生成Bean类之前,对其的引用可以保留在此数组中。会话依次引用了会话工厂,该工厂存储了对所有bean类的引用的数组。另外,会话保留对实体类实例的引用,并且它们本身引用Bean类。



因此,找到了问题的切入点。原来是从dbTransactionListeners引用旧会话的,在修复了错误并开始清除dbTransactionListeners数组后,此问题已得到解决。



Eclipse内存分析器功能



 

因此,Eclipse Memory Analyzer允许您:



  • 找出哪些对象链占用了最大的内存,并确定这些链的入口点(泄漏嫌疑人)
  • 查看所有传入对象引用的树(到累积点的最短路径)
  • 查看对象的所有出站引用的树(“对象”->“列表对象”->“有出站引用”)
  • 查看由不同的ClassLoader加载的重复类(重复的类)



All Articles