理论教育 MAT使用教程:解析Java代码内存泄漏的根本原因

MAT使用教程:解析Java代码内存泄漏的根本原因

时间:2023-06-19 理论教育 版权反馈
【摘要】:在接下来的内容中,将介绍MAT如何根据heap dump分析泄漏根源。因为绝大多数Android应用程序是用Java语言编写的,所以本小节先用一段Java代码来测试内存泄露。system class loader在没有指定装载器的情况下默认装载用户类,在Sun Java 1.5中即为sun.misc.Launcher$AppClassLoader。图4-18 MAT解析界面由此可见,通过使用MAT工具分析heap dump之后,会在界面上非常直观的展示了一个“饼图”,该图深色区域被怀疑有内存泄漏。在heap dump中没有包含太多的perm gen信息。

MAT使用教程:解析Java代码内存泄漏的根本原因

在接下来的内容中,将介绍MAT如何根据heap dump分析泄漏根源。因为绝大多数Android应用程序是用Java语言编写的,所以本小节先用一段Java代码来测试内存泄露。这段测试代码非常简单,很容易找出问题,希望读者能够借此举一反三。

一开始不得不说说ClassLoader,本质上,它的工作就是把磁盘上的类文件读入内存,然后调用java.lang.ClassLoader.defineClass方法告诉系统把内存镜像处理成合法的字节码。Java提供了抽象类ClassLoader,所有用户自定义类装载器都实例化自ClassLoader的子类。system class loader在没有指定装载器的情况下默认装载用户类,在Sun Java 1.5中即为sun.misc.Launcher$AppClassLoader。

(1)准备heap dump

请看下面的Pilot类的演示代码:

978-7-111-51616-3-Part02-187.jpg

然后再看类OOMHeapTest是如何填满heap dump的。

978-7-111-51616-3-Part02-188.jpg

在上面代码中,构造了很多的Pilot类实例,然后向数组和map中存放。由于是Strong Ref,GC自然不会回收这些对象,所以一直放在heap中直到溢出。当然在运行前,先要在Eclipse中配置VM参数“-XX:+HeapDumpOnOutOfMemoryError”。短时间之后,内存溢出,控制台打出如下信息。

978-7-111-51616-3-Part02-189.jpg

文件java_pid3600.hprof就是我们需要的heap dump,读者可以在OOMHeapTest类所在的工程根目录下找到。

(2)使用MAT

使用MAT解析.hprof文件,在弹出向导后直接单击“Finish”按钮后会看到如图4-18所示的界面。

978-7-111-51616-3-Part02-190.jpg

图4-18 MAT解析界面

由此可见,通过使用MAT工具分析heap dump之后,会在界面上非常直观的展示了一个“饼图”,该图深色区域被怀疑有内存泄漏。我们可以发现整个heap才64M内存,深色区域就占了99.5%。接下来是一个简短的描述,告诉我们main()线程占用了大量内存,并且明确指出system class loader加载的“java.lang.Thread”实例有内存聚集,并建议用关键字“java.lang.Thread”进行检查。MAT通过简单的两句话就说明了问题所在,就算使用者没什么处理内存问题的经验也可找到问题所在。图4-19的下方有一个"Details"链接,在打开之前不妨考虑一个问题:为何对象实例会聚集在内存中,而未被GC?答案是因为Strong Ref,如图4-19所示。

978-7-111-51616-3-Part02-191.jpg

图4-19 “Details”界面

由此可见,打开“Details”链接之后,除了在图4-20中看到的描述外,还有Shortest Paths To the Accumulation Point和Accumulated Objects部分,这里说明了从GC Root到聚集点的最短路径,以及完整的reference chain。观察Accumulated Objects部分,java.util.HashMap和java.lang.Object[1000000]实例的retained heap(size)最大,我们知道retained heap代表从该类实例沿着reference chain往下所能收集到的其他类实例的shallow heap(size)总和,所以明显类实例都聚集在HashMap和Object数组中了。这里我们发现一个有趣的现象,既Object数组的shallow heap和retained heap一样,数组的shallow heap和一般对象(非数组)不同,依赖于数组的长度和里面的元素的类型,对数组求shallow heap,也就是求数组集合内所有对象的shallow heap之和。接下来再来看org.rosenjiang.bo.Pilot对象实例的shallow heap为何是16,因为对象头是8字节,成员变量int是4字节、String引用是4字节,所以总共16字节。

接下来再来看Accumulated Objects by Class区域,如图4-20所示。

978-7-111-51616-3-Part02-192.jpg

图4-20 Accumulated Objects by Class区域

顾名思义,在Accumulated Objects by Class区域能找到被聚集的对象实例的类名。此处的类org.rosenjiang.bo.Pilot是头条,被实例化了290325次,再返回去看程序,其实是编者故意而为之。还有很多有用的报告可用来协助分析问题,只是本文中的例子太简单,所以也用不上。

(3)perm gen

perm gen是一个异类,在里面存储了类和方法数据(与class loader有关)以及interned strings(字符串驻留)。在heap dump中没有包含太多的perm gen信息。那么我们就用这些少量的信息来解决问题,利用interned strings把perm gen填满如下面的代码所示。

978-7-111-51616-3-Part02-193.jpg

控制台会打印如下的信息,然后把java_pid1824.hprof文件导入到MAT。其实在MAT里,看到的状况应该和“OutOfMemoryError:Java heap space”差不多,因为heap dump并没有包含interned strings方面的任何信息。只是在这里需要强调,使用intern()方法的时候应该多加注意。

978-7-111-51616-3-Part02-194.jpg

开始思考如何把class loader填满,经过尝试会发现使用ASM(JavaScript的一个新领域)来动态生成类才能达到目的。ASM(http://asm.objectweb.org)的主要作用是处理已编译类(compiled class),能对已编译类进行生成、转换、分析(功能之一是实现动态代理),而且它运行起来足够的快和小巧,文档也全面。ASM提供了core API和tree API,前者是基于事件的方式,后者是基于对象的方式,类似于XML的SAX、DOM解析,但是使用tree API性能会有损失。到此为止,我们已编译类的结构有:(www.daowen.com)

修饰符(如public、private)、类名、父类名、接口和annotation部分。

类成员变量声明,包括每个成员的修饰符、名字、类型和annotation。

方法和构造函数描述,包括修饰符、名字、返回和传入参数类型,以及annotation。当然还包括这些方法或构造函数的具体Java字节码。

常量池(constant pool)部分,constant pool是一个包含类中出现的数字、字符串、类型常量的数组。

已编译类和原来的类源码区别在于,已编译类只包含类本身,内部类不会在已编译类中出现,而是生成另外一个已编译类文件;其二,已编译类中没有注释;其三,已编译类没有package和import部分。已编译类对Java类型的描述:对于原始类型由单个大写字母表示,Z代表boolean、C代表char、B代表byte、S代表short、I代表int、F代表float、J代表long、D代表double;而对类类型的描述使用内部名(internal name)外加前缀L和后面的分号共同来表示,所谓内部名就是带全包路径的表示法,例如String的内部名是java/lang/String;对于数组类型,使用单方括号加上数据元素类型的方式描述;最后对于方法的描述;用圆括号来表示,如果返回是void用V表示,具体参照如图4-21所示。

978-7-111-51616-3-Part02-195.jpg

图4-21 Java类型的描述

而在下面的代码中会使用ASM core API,在此需要注意接口ClassVisitor是核心,FieldVisitor、MethodVisitor都是辅助接口。ClassVisitor应该按照这样的方式来调用:

978-7-111-51616-3-Part02-196.jpg

也就是说方法visit必须首先调用,再调用最多一次的visitSource,再调用最多一次的visitOuterClass方法,接下来再多次调用visitAnnotation和visitAttribute方法,最后是多次调用visitInnerClass、visitField和visitMethod方法。调用完后再调用visitEnd方法作为结尾。

另外还需要注意ClassWriter类,该类实现了ClassVisitor接口,通过toByteArray方法可以把已编译类直接构建成二进制形式。由于我们要动态生成子类,所以这里只对ClassWriter感兴趣。首先是抽象类原型:

978-7-111-51616-3-Part02-197.jpg

其次是自定义类加载器,因为ClassLoader的defineClass方法都是受保护的,所以要想加载字节数组形式的类,只有通过继承自己后再实现。

978-7-111-51616-3-Part02-198.jpg

最后看测试类的演示代码:

978-7-111-51616-3-Part02-199.jpg

978-7-111-51616-3-Part02-200.jpg

运行后控制台会报错,输出:

978-7-111-51616-3-Part02-201.jpg

打开文件java_pid3023.hprof,如图4-22所示。我们着重看图中的Classes:88.1k和Class Loader:87.7k部分,可看出class loader加载了大量的类。

978-7-111-51616-3-Part02-202.jpg

图4-22 打开文件java_pid3023.hprof

更进一步分析,需要单击图4-22中的按钮978-7-111-51616-3-Part02-203.jpg,然后选择“Java Basics”→“Class Loader Explorer”功能。打开后能看到图4-23所示的界面,第一列是class loader名字;第二列是class loader已定义类(defined classes)的个数,这里要说一下已定义类和已加载类(loaded classes)了,当需要加载类的时候,相应的class loader会首先把请求委派给父class loader,只有当父class loader加载失败后,该class loader才会自己定义并加载类;第三列是class loader所加载的类的实例数目。

978-7-111-51616-3-Part02-204.jpg

图4-23 Class Loader Explorer功能

在Class Loader Explorer面板会发现class loader是否加载了过多的类。另外,还有Duplicate Classes功能,也能协助分析重复加载的类。在此可以肯定的是,MyAbsClass被重复加载了很多次。

注意:其实MAT工具已经非常强大了,我们的上述演示根本用不到MAT的其他分析功能。在上述演示中,对于OOM只列举了两种溢出错误,其实还有多种其他错误,但对于perm gen来说,如果实在找不出问题所在,建议使用JVM的-verbose参数,该参数会在后台打印出日志,可以用来查看哪个class loader加载了什么类,例如,“[Loaded org.rosenjiang.test.My AbsClass from org.rosenjiang.test.MyClassLoader]”。

免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。

我要反馈