使用MemoryAnalyzertoolMAT分析内存泄漏.docx

上传人:b****5 文档编号:6994693 上传时间:2023-01-15 格式:DOCX 页数:15 大小:482.50KB
下载 相关 举报
使用MemoryAnalyzertoolMAT分析内存泄漏.docx_第1页
第1页 / 共15页
使用MemoryAnalyzertoolMAT分析内存泄漏.docx_第2页
第2页 / 共15页
使用MemoryAnalyzertoolMAT分析内存泄漏.docx_第3页
第3页 / 共15页
使用MemoryAnalyzertoolMAT分析内存泄漏.docx_第4页
第4页 / 共15页
使用MemoryAnalyzertoolMAT分析内存泄漏.docx_第5页
第5页 / 共15页
点击查看更多>>
下载资源
资源描述

使用MemoryAnalyzertoolMAT分析内存泄漏.docx

《使用MemoryAnalyzertoolMAT分析内存泄漏.docx》由会员分享,可在线阅读,更多相关《使用MemoryAnalyzertoolMAT分析内存泄漏.docx(15页珍藏版)》请在冰豆网上搜索。

使用MemoryAnalyzertoolMAT分析内存泄漏.docx

使用MemoryAnalyzertoolMAT分析内存泄漏

使用MemoryAnalyzertool(MAT)分析内存泄漏

(一)

 

 

前言

 

在平时工作过程中,有时会遇到OutOfMemoryError,我们知道遇到Error一般表明程序存在着严重问题,可能是灾难性的。

所以找出是什么原因造成OutOfMemoryError非常重要。

现在向大家引荐EclipseMemoryAnalyzertool(MAT),来化解我们遇到的难题。

如未说明,本文均使用Java5.0onWindowsXPSP3环境。

 

为什么用MAT

 

之前的观点,我认为使用实时profiling/monitoring之类的工具,用一种非常实时的方式来分析哪里存在内存泄漏是很正确的。

年初使用了某profiler工具测试消息中间件中存在的内存泄漏,发现在吞吐量很高的时候profiler工具自己也无法响应,这让人很头痛。

后来了解到这样的工具本身就要消耗性能,且在某些条件下还发现不了泄漏。

所以,分析离线数据就非常重要了,MAT正是这样一款工具。

 

为何会内存溢出

 

我们知道JVM根据generation(代)来进行GC,根据下图所示,一共被分为younggeneration(年轻代)、tenuredgeneration(老年代)、permanentgeneration(永久代,permgen),permgen(或称Non-Heap非堆)是个异类,稍后会讲到。

注意,heap空间不包括permgen。

绝大多数的对象都在younggeneration被分配,也在younggeneration被收回,当younggeneration的空间被填满,GC会进行minorcollection(次回收),这次回收不涉及到heap中的其他generation,minorcollection根据weakgenerationalhypothesis(弱年代假设)来假设younggeneration中大量的对象都是垃圾需要回收,minorcollection的过程会非常快。

younggeneration中未被回收的对象被转移到tenuredgeneration,然而tenuredgeneration也会被填满,最终触发majorcollection(主回收),这次回收针对整个heap,由于涉及到大量对象,所以比minorcollection慢得多。

 

JVM有三种垃圾回收器,分别是

throughputcollector,用来做并行younggeneration回收,由参数-XX:

+UseParallelGC启动;concurrentlowpausecollector,用来做tenuredgeneration并发回收,由参数-XX:

+UseConcMarkSweepGC启动;

incrementallowpausecollector,可以认为是默认的垃圾回收器。

不建议直接使用某种垃圾回收器,最好让JVM自己决断,除非自己有足够的把握。

 

Heap中各generation空间是如何划分的?

通过JVM的-Xmx=n参数可指定最大heap空间,而-Xms=n则是指定最小heap空间。

在JVM初始化的时候,如果最小heap空间小于最大heap空间的话,如上图所示JVM会把未用到的空间标注为Virtual。

除了这两个参数还有-XX:

MinHeapFreeRatio=n和-XX:

MaxHeapFreeRatio=n来分别控制最大、最小的剩余空间与活动对象之比例。

在32位SolarisSPARC操作系统下,默认值如下,在32位windowsxp下,默认值也差不多。

参数

默认值

MinHeapFreeRatio

40

MaxHeapFreeRatio

70

-Xms

3670k

-Xmx

64m

由于tenuredgeneration的majorcollection较慢,所以tenuredgeneration空间小于younggeneration的话,会造成频繁的majorcollection,影响效率。

ServerJVM默认的younggeneration和tenuredgeneration空间比例为1:

2,也就是说younggeneration的eden和survivor空间之和是整个heap(当然不包括permgen)的三分之一,该比例可以通过-XX:

NewRatio=n参数来控制,而ClientJVM默认的-XX:

NewRatio是8。

至于调整younggeneration空间大小的NewSize=n和MaxNewSize=n参数就不讲了,请参考后面的资料。

 

younggeneration中幸存的对象被转移到tenuredgeneration,但不幸的是concurrentcollector线程在这里进行majorcollection,而在回收任务结束前空间被耗尽了,这时将会发生FullCollections(FullGC),整个应用程序都会停止下来直到回收完成。

FullGC是高负载生产环境的噩梦……

 

现在来说说异类permgen,它是JVM用来存储无法在Java语言级描述的对象,这些对象分别是类和方法数据(与classloader有关)以及internedstrings(字符串驻留)。

一般32位OS下permgen默认64m,可通过参数-XX:

MaxPermSize=n指定,JVMMemoryStructure一文说,对于这块区域,没有更详细的文献了,神秘。

 

回到问题“为何会内存溢出?

”。

要回答这个问题又要引出另外一个话题,既什么样的对象GC才会回收?

当然是GC发现通过任何referencechain(引用链)无法访问某个对象的时候,该对象即被回收。

名词GCRoots正是分析这一过程的起点,例如JVM自己确保了对象的可到达性(那么JVM就是GCRoots),所以GCRoots就是这样在内存中保持对象可到达性的,一旦不可到达,即被回收。

通常GCRoots是一个在currentthread(当前线程)的callstack(调用栈)上的对象(例如方法参数和局部变量),或者是线程自身或者是systemclassloader(系统类加载器)加载的类以及nativecode(本地代码)保留的活动对象。

所以GCRoots是分析对象为何还存活于内存中的利器。

知道了什么样的对象GC才会回收后,再来学习下对象引用都包含哪些吧。

 

从最强到最弱,不同的引用(可到达性)级别反映了对象的生命周期。

● StrongRef(强引用):

通常我们编写的代码都是StrongRef,于此对应的是强可达性,只有去掉强可达,对象才被回收。

● SoftRef(软引用):

对应软可达性,只要有足够的内存,就一直保持对象,直到发现内存吃紧且没有StrongRef时才回收对象。

一般可用来实现缓存,通过java.lang.ref.SoftReference类实现。

● WeakRef(弱引用):

比SoftRef更弱,当发现不存在StrongRef时,立刻回收对象而不必等到内存吃紧的时候。

通过java.lang.ref.WeakReference和java.util.WeakHashMap类实现。

● PhantomRef(虚引用):

根本不会在内存中保持任何对象,你只能使用PhantomRef本身。

一般用于在进入finalize()方法后进行特殊的清理过程,通过java.lang.ref.PhantomReference实现。

 

有了上面的种种我相信很容易就能把heap和permgen撑破了吧,是的利用StrongRef,存储大量数据,直到heap撑破;利用internedstrings(或者classloader加载大量的类)把permgen撑破。

 

关于shallowsize、retainedsize

 

Shallowsize就是对象本身占用内存的大小,不包含对其他对象的引用,也就是对象头加成员变量(不是成员变量的值)的总和。

在32位系统上,对象头占用8字节,int占用4字节,不管成员变量(对象或数组)是否引用了其他对象(实例)或者赋值为null它始终占用4字节。

故此,对于String对象实例来说,它有三个int成员(3*4=12字节)、一个char[]成员(1*4=4字节)以及一个对象头(8字节),总共3*4+1*4+8=24字节。

根据这一原则,对Stringa=”rosenjiang”来说,实例a的shallowsize也是24字节(很多人对此有争议,请看官甄别并留言给我)。

 

Retainedsize是该对象自己的shallowsize,加上从该对象能直接或间接访问到对象的shallowsize之和。

换句话说,retainedsize是该对象被GC之后所能回收到内存的总和。

为了更好的理解retainedsize,不妨看个例子。

 

把内存中的对象看成下图中的节点,并且对象和对象之间互相引用。

这里有一个特殊的节点GCRoots,正解!

这就是referencechain的起点。

从obj1入手,上图中蓝色节点代表仅仅只有通过obj1才能直接或间接访问的对象。

因为可以通过GCRoots访问,所以左图的obj3不是蓝色节点;而在右图却是蓝色,因为它已经被包含在retained集合内。

所以对于左图,obj1的retainedsize是obj1、obj2、obj4的shallowsize总和;右图的retainedsize是obj1、obj2、obj3、obj4的shallowsize总和。

obj2的retainedsize可以通过相同的方式计算。

 

HeapDump

 

heapdump是特定时间点,java进程的内存快照。

有不同的格式来存储这些数据,总的来说包含了快照被触发时java对象和类在heap中的情况。

由于快照只是一瞬间的事情,所以heapdump中无法包含一个对象在何时、何地(哪个方法中)被分配这样的信息。

 

在不同平台和不同java版本有不同的方式获取heapdump,而MAT需要的是HPROF格式的heapdump二进制文件。

想无需人工干预的话,要这样配置JVM参数:

-XX:

-HeapDumpOnOutOfMemoryError,当错误发生时,会自动生成heapdump,在生产环境中,只有用这种方式。

如果你想自己控制什么时候生成heapdump,在Windows+JDK6环境中可利用JConsole工具,而在Linux或者MacOSX环境下均可使用JDK5、6自带的jmap工具。

当然,还可以配置JVM参数:

-XX:

+HeapDumpOnCtrlBreak,也就是在控制台使用Ctrl+Break键来生成heapdump。

由于我是windows+JDK5,所以选择了-XX:

-HeapDumpOnOutOfMemoryError这种方式,更多配置请参考MATWiki。

使用MemoryAnalyzertool(MAT)分析内存泄漏

(二)

前言

在使用MemoryAnalyzertool(MAT)分析内存泄漏

(一)中,我介绍了内存泄漏的前因后果。

在本文中,将介绍MAT如何根据heapdump分析泄漏根源。

由于测试范例可能过于简单,很容易找出问题,但我期待借此举一反三。

一开始不得不说说ClassLoader,本质上,它的工作就是把磁盘上的类文件读入内存,然后调用java.lang.ClassLoader.defineClass方法告诉系统把内存镜像处理成合法的字节码。

Java提供了抽象类ClassLoader,所有用户自定义类装载器都实例化自ClassLoader的子类。

systemclassloader在没有指定装载器的情况下默认装载用户类,在SunJava1.5中既sun.misc.Launcher$AppClassLoader。

更详细的内容请参看下面的资料。

准备heapdump

请看下面的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类,它是如何撑破heapdump的。

/**

 * 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 map = new HashMap();

        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;

        }

    }

}

是的,上面构造了很多的Pilot类实例,向数组和map中放。

由于是StrongRef,GC自然不会回收这些对象,一直放在heap中直到溢出。

当然在运行前,先要在Eclipse中配置VM参数-XX:

+HeapDumpOnOutOfMemoryError。

好了,一会儿功夫内存溢出,控制台打出如下信息。

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

java_pid3600.hprof既是heapdump,可以在OOMHeapTest类所在的工程根目录下找到。

MAT安装

话分两头说,有了heapdump还得安装MAT。

可以在http:

//www.eclipse.org/mat/downloads.php选择合适的方式安装。

安装完成后切换到MemoryAnalyzer视图。

在Eclipse的左上角有OpenHeapDump按钮,按照刚才说的路径找到java_pid3600.hprof文件并打开。

解析hprof文件会花些时间,然后会弹出向导,直接Finish即可。

稍后会看到下图所示的界面。

MAT工具分析了heapdump后在界面上非常直观的展示了一个饼图,该图深色区域被怀疑有内存泄漏,可以发现整个heap才64M内存,深色区域就占了99.5%。

接下来是一个简短的描述,告诉我们main线程占用了大量内存,并且明确指出systemclassloader加载的"java.lang.Thread"实例有内存聚集,并建议用关键字"java.lang.Thread"进行检查。

所以,MAT通过简单的两句话就说明了问题所在,就算使用者没什么处理内存问题的经验。

在下面还有一个"Details"链接,在点开之前不妨考虑一个问题:

为何对象实例会聚集在内存中,为何存活(而未被GC)?

是的——StrongRef,那么再走近一些吧。

点击了"Details"链接之后,除了在上一页看到的描述外,还有ShortestPathsTotheAccumulationPoint和AccumulatedObjects部分,这里说明了从GCroot到聚集点的最短路径,以及完整的referencechain。

观察AccumulatedObjects部分,java.util.HashMap和java.lang.Object[1000000]实例的retainedheap(size)最大,在上一篇文章中我们知道retainedheap代表从该类实例沿着referencechain往下所能收集到的其他类实例的shallowheap(size)总和,所以明显类实例都聚集在HashMap和Object数组中了。

这里我们发现一个有趣的现象,既Object数组的shallowheap和retainedheap竟然一样,通过Shallowandretainedsizes一文可知,数组的shallowheap和一般对象(非数组)不同,依赖于数组的长度和里面的元素的类型,对数组求shallowheap,也就是求数组集合内所有对象的shallowheap之和。

好,再来看org.rosenjiang.bo.Pilot对象实例的shallowheap为何是16,因为对象头是8字节,成员变量int是4字节、String引用是4字节,故总共16字节。

接着往下看,来到了AccumulatedObjectsbyClass区域,顾名思义,这里能找到被聚集的对象实例的类名。

org.rosenjiang.bo.Pilot类上头条了,被实例化了290,325次,再返回去看程序,我承认是故意这么干的。

还有很多有用的报告可用来协助分析问题,只是本文中的例子太简单,也用不上。

以后如有用到,一定撰文详细叙述。

又是permgen

我们在上一篇文章中知道,permgen是个异类,里面存储了类和方法数据(与classloader有关)以及internedstrings(字符串驻留)。

在heapdump中没有包含太多的permgen信息。

那么我们就用这些少量的信息来解决问题吧。

看下面的代码,利用internedstrings把permgen撑破了。

/**

 * 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_pid1824.hprof文件导入到MAT。

其实在MAT里,看到的状况应该和“OutOfMemoryError:

Javaheapspace”差不多(用了数组),因为heapdump并没有包含internedstrings方面的任何信息。

只是在这里需要强调,使用intern()方法的时候应该多加注意。

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

倒是在思考如何把classloader撑破废了些心思。

经过尝试,发现使用ASM来动态生成类才能达到目的。

ASM(http:

//asm.objectweb.org)的主要作用是处理已编译类(compiledclass),能对已编译类进行生成、转换、分析(功能之一是实现动态代理),而且它运行起来足够的快和小巧,文档也全面,实属居家必备之良品。

ASM提供了coreAPI和treeAPI,前者是基于事件的方式,后者是基于对象的方式,类似于XML的SAX、DOM解析,但是使用treeAPI性能会有损失。

既然下面要用到ASM,这里不得不啰嗦下已编译类的结构,包括:

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

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

   3、方法和构造函数描述,包括修饰符、名字、返回和传入参数类型,以及annotation。

当然还包括这些方法或构造函数的具体Java字节码。

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

已编译类和原来的类源码区别在于,已编译类只包含类本身,内部类不会在已编译类中出现,而是生成另外一个已编译类文件;其二,已编译类中没有注释;其三,已编译类没有package和import部分。

这里还得说说已编译类对Java类型的描述,对于原始类型由单个大写字母表示,Z代表boolean、C代表char、B代表byte、S代表short、I代表int、F代表float、J代表long、D代表double;而对类类型的描述使用内部名(internalname)外加前缀L和后面的分号共同表示来表示,所谓内部名就是带全包路径的表示法,例如String的内部名是java/lang/String;对于数组类型,使用单方括号加上数据元素类型的方式描述。

最后对于方法的描述,用圆括号来表示,如果返回是void用V表示,具体参考下图。

下面的代码中会使用ASMcoreAPI,注意接口ClassVisitor是核心,FieldVisitor、MethodVisitor都是辅助接口。

ClassVisitor应该按照这样的方式来调用:

visitvisitSource?

visitOuterClass?

(visitAnnotation|visitAttribu

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 工程科技 > 信息与通信

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1