如果您是一名新手或中级Java开发人员,还不知道如何使用Java虚拟机(JVM)生产环境,那么在Java应用程序中查找内存泄漏可能是大海捞针。但是,根据您的评测工具,您可以轻松地分析Java内存消耗,同时获得对Java生产应用程序中堆的即时洞察。但是,在详细介绍如何在java web应用程序中发现内存泄漏之前,让我们先了解一下什么是java内存泄漏,这种泄漏的可能原因以及处理此问题的修复过程。
Java内存泄漏
内存泄漏只是由PermGen中保留的引用链接引起的,不能被垃圾回收。
听起来像胡言乱语,对吧?好吧,保持冷静,我继续解释。因为web容器使用类到类装入器映射系统来隔离web应用程序,而且因为类是由其名称和加载它的类装入器唯一标识的。因此,可以有一个同名的类,在一个JVM中加载多次,每个类都有一个不同的类装入器。
java.lang.Class
从长远来看,这可能会成为一个非常大的参考图。别忘了这些类直接加载到PermGen中。因此,从web应用程序中保留对特定对象的引用会将web应用程序加载到PermGen中的每个类固定下来。即使web应用程序被重新加载后,这些引用通常仍会保留,每次重新加载时,更多的类会被固定或卡在PermGen中,而PermGen在适当的时候会被填满。
什么是PermGen?
PermGen是永久生成(PermGen)的缩写,是JVM中的一个堆,专门用于存储JVM的Java类的内部表示形式,以及被扣留的字符串实例。简单地说,它是一个独立于主Java堆的独占堆位置,JVM在那里注册与已加载类相关的元数据。
不过,大多数java servlet容器和WebSocket技术都支持 org.apache.catalina.core.JreMemoryLeakPreventionListener
通过扩展(如ApacheTomcat7.0及更高版本)初始化。尽管如此,如果出现更复杂的情况,比如重新加载时的PermGen错误和应用程序本身引起的bug干扰,那么包含这个内存泄漏处理程序是不够的。当tomcat servlet没有导致泄漏时,这会变得更有趣,应用程序也不是(至少不是直接的),而是JRE代码中由第三方库触发的bug。
随着Java开发工具包JDK6(更新7或更新版本)的出现,JDK附带了一个方便的工具,它使我们的生活变得更加轻松。这个工具称为 VisualVM ;它是一个图形化工具,可以无缝地连接到任何JVM,除了允许您导航该堆之外,还允许您从JVM堆中排出不需要的垃圾。在tomcat servlet中,web应用程序的类加载器也是一个名为 org.apache.catalina.loader.WebappClassLoader
. 因此,如果我们的Tomcat实例只部署了一个web应用程序,那么堆中应该只有一个此类的实例。但如果有更多的话,我们就有漏洞了。
如何在java web应用程序中查找内存泄漏的步骤
现在我们已经解决了这个问题,让我们快速深入了解如何检测和避免Java内存泄漏的步骤。让我们立即深入研究如何使用 VisualVM 来解决这个问题。
步骤:
1. 打开命令提示符终端,输入下面的命令启动visualvm;
${javahome}/bin/jvisualvm
将弹出一个类似于图1的窗口。
如何在java web应用程序中发现内存泄漏
图1:JavaVisualVM
右键单击左侧边栏中的Tomcat,然后选择“Heap Dump”。
如何在java web应用程序中发现内存泄漏
图2:堆转储:单击“OQL控制台”按钮。
点击控制台顶部的“转储”按钮。这将打开一个控制台,允许您查询堆转储。对于本练习,我们希望找到 org.apache.catalina.loader.WebappClassLoader.So
在结果控制台中输入以下命令;
select x from org.apache.catalina.loader.WebappClassLoader x
图3:“OQL查询编辑器控制台”:输入上面的命令并单击execute
在本例中,VisualVM发现了两个web应用程序类装入器实例;一个用于web应用程序本身,另一个用于Tomcat管理器应用程序。
使用Tomcat管理器应用程序在重新启动web应用程序 http://localhost:8080/manager/html
并获取另一个Tomcat进程的堆转储。
图4:重新启动web应用程序并导航回“OQL控制台”以再次重复步骤3
注意上面步骤中包含了一个额外的实例。这是因为这3个实例中的一个应该由垃圾回收器收集,但事实并非如此。多亏了Tomcat,我们可以很容易地分辨出哪个实例没有被垃圾回收,因为所有活动类装入器都设置了字段名:“started”设置为“true”。
为了找到无效的实例,请单击每个类装入器实例,直到找到“started”字段设置为“false”的实例。
图5:单击堆转储中的每个类装入器实例,找出有问题的实例-“started”字段设置为false
现在我们已经找到了导致泄漏的类装入器,我们需要确定哪个对象持有对类装入器的引用。显然,大量的对象将被许多其他对象引用,但从本质上讲,只有有限数量的这些对象会构成引用图的根。这些是我们特别感兴趣的对象。
因此,在instances选项卡的底部窗格中,右键单击构成引用图根的对象实例,然后选择“Show Nearent GC root”。
结果窗口应该是这样的;
图6:右键单击构成引用图根的对象实例,然后选择“Show Nearent GC root”。
右键单击实例并选择“显示实例”。
图7:右键单击实例并选择“Show Instance”。
由此,我们可以推断这是 sun.awt.AppContext
类型。我们还可以看到AppContext中的contextClassLoader字段包含对WebappClassLoader的引用。因此,这是导致内存泄漏的错误引用。现在我们来看看是什么实例化了 sun.awt.AppContext
类型,对于初学者。
首先,我们用以下代码在调试模式下重新启动Tomcat;
export JPDA_SUSPEND=y
${TOMCAT_HOME}/bin/catalina.sh jpda
然后,我们继续远程调试类加载序列——在这种情况下,我将使用Eclipse来完成这项工作。另外,我们需要在 sun.awt.AppContext
使用Open Type命令(Shift+Control+T)导航到 sun.awt.AppContext
类型。
右键单击大纲窗格中的类名并选择 Toggle Class Load Breakpoint “切换类加载断点”。
此后,我们需要通过将调试器连接到Tomcat实例并让调试器在 sun.awt.AppContext
装载;
将调试器连接到Tomcat实例,并将调试器设置为在 sun.awt.AppContext
已加载。
就这样!它已经被JavaBeans框架实例化,在这个实例中,Oracle Universal Connection Pool (UCP)正在使用它。因此,我们可以注意到contextClassLoader是一个最终的字段,它看起来像是一个单独的字段;所以我们可以假设这个字段只在AppContext的实例化过程中设置一次。
所以我们可以推断这个字段只在实例化期间设置了一次。
我很方便地将上面的代码添加到我的servlet上下文侦听器中,使它在应用程序启动期间执行,并达到了修复这个特定内存泄漏的预期效果。
如何在Java Web应用程序中发现内存泄漏-小结
总而言之,JEE应用程序的“PermGen”内存不足错误通常驻留在应用程序本身(或应用程序使用的库)中,并且通常由JRE库中的类组成,这些类包含对web应用程序类加载器的引用或web应用程序类加载器实例化的对象的引用。
查找泄漏原因的整个过程是利用Java应用程序性能监视( APM )解决方案。就像Fusion Reactor的堆分析器为未收集的web应用程序类装入器实例提供源代码,然后为直接或间接保留类装入器的根GC对象提供源。当您找到这个对象时,您现在可以使用APM的内置调试器来发现这个对象是如何被实例化的,然后设计一种方法来修改它的常规行为。这样做是为了使它不会永远保持类装入器引用。