转载自:http://www.blogjava.net/rosen/archive/2010/06/13/323522.html
前言
在本文中,将介绍MAT如何根据heap dump分析泄漏根源。由于测试范例可能过于简单,很容易找出问题,但我期待借此举一反三。
一开始不得不说说ClassLoader,本质上,它的工作就是把磁盘上的类文件读入内存,然后调用java.lang.ClassLoader.defineClass方法告诉系统把内存镜像处理成合法的字节码。Java提供了抽象类ClassLoader,所有用户自定义类装载器都实例化自ClassLoader的子类。system class loader在没有指定装载器的情况下默认装载用户类,在Sun Java 1.5中既sun.misc.Launcher$AppClassLoader。更详细的内容请参看下面的资料。
准备heap dump
请看下面的Pilot类,没啥特殊的。
/** * Pilot class * @author rosen jiang */ package org.rosenjiang.bo; public class Pilot{ String name; int age; public Pilot(String a, int b){ name = a; age = b; } }
/** * OOMHeapTest class * @author rosen jiang */ package org.rosenjiang.test; import java.util.Date; import java.util.HashMap; import java.util.Map; import org.rosenjiang.bo.Pilot; public class OOMHeapTest { public static void main(String[] args){ oom(); } private static void oom(){ Map<String, Pilot> map = new HashMap<String, Pilot>(); Object[] array = new Object[1000000]; for(int i=0; i<1000000; i++){ String d = new Date().toString(); Pilot p = new Pilot(d, i); map.put(i+"rosen jiang", p); array[i]=p; } } }
java.lang.OutOfMemoryError: Java heap space Dumping heap to java_pid3600.hprof Heap dump file created [78233961 bytes in 1.995 secs] Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
MAT工具分析了heap dump后在界面上非常直观的展示了一个饼图,该图深色区域被怀疑有内存泄漏,可以发现整个heap才64M内存,深色区域就占了99.5%。接下来是一个简短的描述,告诉我们main线程占用了大量内存,并且明确指出system class loader加载的"java.lang.Thread"实例有内存聚集,并建议用关键字"java.lang.Thread"进行检查。所以,MAT通过简单的两句话就说明了问题所在,就算使用者没什么处理内存问题的经验。在下面还有一个"Details"链接,在点开之前不妨考虑一个问题:为何对象实例会聚集在内存中,为何存活(而未被GC)?是的——Strong Ref,那么再走近一些吧。
点击了"Details"链接之后,除了在上一页看到的描述外,还有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 and retained sizes一文可知,数组的shallow heap和一般对象(非数组)不同,依赖于数组的长度和里面的元素的类型,对数组求shallow heap,也就是求数组集合内所有对象的shallow heap之和。好,再来看org.rosenjiang.bo.Pilot对象实例的shallow heap为何是16,因为对象头是8字节,成员变量int是4字节、String引用是4字节,故总共16字节。
接着往下看,来到了Accumulated Objects by Class区域,顾名思义,这里能找到被聚集的对象实例的类名。org.rosenjiang.bo.Pilot类上头条了,被实例化了290,325次,再返回去看程序,我承认是故意这么干的。还有很多有用的报告可用来协助分析问题,只是本文中的例子太简单,也用不上。以后如有用到,一定撰文详细叙述。
又是perm gen
我们在上一篇文章中知道,perm gen是个异类,里面存储了类和方法数据(与class loader有关)以及interned strings(字符串驻留)。在heap dump中没有包含太多的perm gen信息。那么我们就用这些少量的信息来解决问题吧。
看下面的代码,利用interned strings把perm gen撑破了。
/** * OOMPermTest class * @author rosen jiang */ package org.rosenjiang.test; public class OOMPermTest { public static void main(String[] args){ oom(); } private static void oom(){ Object[] array = new Object[10000000]; for(int i=0; i<10000000; i++){ String d = String.valueOf(i).intern(); array[i]=d; } } }
java.lang.OutOfMemoryError: PermGen space Dumping heap to java_pid1824.hprof Heap dump file created [121273334 bytes in 2.845 secs] Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
/** * @author rosen jiang * MyAbsClass class */ package org.rosenjiang.test; public abstract class MyAbsClass { int LESS = -1; int EQUAL = 0; int GREATER = 1; abstract int absTo(Object o); }
/** * @author rosen jiang * MyClassLoader class */ package org.rosenjiang.test; public class MyClassLoader extends ClassLoader { public Class defineClass(String name, byte[] b) { return defineClass(name, b, 0, b.length); } }
/** * @author rosen jiang * OOMPermTest class */ package org.rosenjiang.test; import java.util.ArrayList; import java.util.List; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Opcodes; public class OOMPermTest { public static void main(String[] args) { OOMPermTest o = new OOMPermTest(); o.oom(); } private void oom() { try { ClassWriter cw = new ClassWriter(0); cw.visit(Opcodes.V1_5, Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT, "org/rosenjiang/test/MyAbsClass", null, "java/lang/Object", new String[] {}); cw.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL + Opcodes.ACC_STATIC, "LESS", "I", null, new Integer(-1)).visitEnd(); cw.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL + Opcodes.ACC_STATIC, "EQUAL", "I", null, new Integer(0)).visitEnd(); cw.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL + Opcodes.ACC_STATIC, "GREATER", "I", null, new Integer(1)).visitEnd(); cw.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT, "absTo", "(Ljava/lang/Object;)I", null, null).visitEnd(); cw.visitEnd(); byte[] b = cw.toByteArray(); List<ClassLoader> classLoaders = new ArrayList<ClassLoader>(); while (true) { MyClassLoader classLoader = new MyClassLoader(); classLoader.defineClass("org.rosenjiang.test.MyAbsClass", b); classLoaders.add(classLoader); } } catch (Exception e) { e.printStackTrace(); } } }
java.lang.OutOfMemoryError: PermGen space Dumping heap to java_pid3023.hprof Heap dump file created [92593641 bytes in 2.405 secs] Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
更进一步分析,点击上图中红框线圈起来的按钮,选择Java Basics——Class Loader Explorer功能。打开后能看到下图所示的界面,第一列是class loader名字;第二列是class loader已定义类(defined classes)的个数,这里要说一下已定义类和已加载类(loaded classes)了,当需要加载类的时候,相应的class loader会首先把请求委派给父class loader,只有当父class loader加载失败后,该class loader才会自己定义并加载类,这就是Java自己的“双亲委派加载链”结构;第三列是class loader所加载的类的实例数目。
在Class Loader Explorer这里,能发现class loader是否加载了过多的类。另外,还有Duplicate Classes功能,也能协助分析重复加载的类,在此就不再截图了,可以肯定的是MyAbsClass被重复加载了N多次。
最后
其实MAT工具非常的强大,上面故弄玄虚的范例代码根本用不上MAT的其他分析功能,所以就不再描述了。其实对于OOM不只我列举的两种溢出错误,还有多种其他错误,但我想说的是,对于perm gen,如果实在找不出问题所在,建议使用JVM的-verbose参数,该参数会在后台打印出日志,可以用来查看哪个class loader加载了什么类,例:“[Loaded org.rosenjiang.test.MyAbsClass from org.rosenjiang.test.MyClassLoader]”。
全文完。