使用Memory Analyzer tool分析JAVA虚拟机的内存溢出问题

来源:互联网 发布:大数据理论知识 编辑:程序博客网 时间:2024/06/16 01:04

有一次项目外场反馈了一个失败观察的离线工具的问题,打开了一个含有1W条失败观察离线数据文件,离线工具界面很快假死了,无法操作,同时CMD界面出现OutOfMemoryError的错误。

 

2.1   为何会内存溢出

JAVA内存空间中的堆区域(用于存放JAVA的对象)发生了内存溢出,有两个原因:

Ø  你的应用比较消耗内存空间,需要较大的堆空间,而你设置的内存不够。

Ø  你的程序有隐患,new了大量的不能及时释放对象,最终消耗了过多的内存。需要排查。

这两种情况对应的解决方案有两种:

Ø  优化调整JVM内存

Ø  使用专门的内存溢出测试工具进行检测Java的内存溢出,找出问题后修改代码。这里我们主要介绍EclipseMemory Analyzer tool(MAT)

在绝大多数情况下,如果出现内存溢出的错误,单靠调整JVM内存大小一般都不能从根本上解决问题,因此本文主要描述第二种解决方案。

2.2   JAVA虚拟机的垃圾回收原理

JVM根据generation()来进行GC,根据下图所示,一共被分为young generation(年轻代)tenured generation(老年代)permanent generation(永久代, perm gen)。注意,堆内存被分成新生代和年老代两个部分,heap空间不包括perm gen

现在的Java虚拟机就联合使用了分代复制、标记-清除和标记-整理算法,JAVA虚拟机垃圾收集器关注的内存结构如下:

2.2.1  新生代

新生代使用复制和标记-清除垃圾收集算法,研究表明,新生代中98%的对象是朝生夕死的短生命周期对象,所以不需要将新生代划分为容量大小相等的两部分内存,而是将新生代分为Eden区,Survivor fromSurvivor to三部分,其占新生代内存容量默认比例分别为811,其中Survivor fromSurvivor to总有一个区域是空白,只有Eden和其中一个Survivor总共90%的新生代容量用于为新创建的对象分配内存,只有10%Survivor内存浪费,当新生代内存空间不足需要进行垃圾回收时,仍然存活的对象被复制到空白的Survivor内存区域中,Eden和非空白的Survivor进行标记-清理回收,两个Survivor区域是轮换的。

新生代中98%情况下空白Survivor都可以存放垃圾回收时仍然存活的对象,2%的极端情况下,如果空白Survivor空间无法存放下仍然存活的对象时,使用内存分配担保机制,直接将新生代依然存活的对象复制到年老代内存中,同时对于创建大对象时,如果新生代中无足够的连续内存时,也直接在年老代中分配内存空间。

Java虚拟机对新生代的垃圾回收称为Minor GC,次数比较频繁,每次回收时间也比较短。

使用java虚拟机-Xmn参数可以指定新生代内存大小。

2.2.2  年老代

年老代中的对象一般都是长生命周期对象,对象的存活率比较高,因此在年老代中使用标记-整理垃圾回收算法。

Java虚拟机对年老代的垃圾回收称为MajorGC/Full GC,次数相对比较少,每次回收的时间也比较长。

当新生代中无足够空间为对象创建分配内存,年老代中内存回收也无法回收到足够的内存空间,并且新生代和年老代空间无法在扩展时,堆就会产生OutOfMemoryError异常。

java虚拟机-Xms参数可以指定最小内存大小,-Xmx参数可以指定最大内存大小,这两个参数分别减去Xmn参数指定的新生代内存大小,可以计算出年老代最小和最大内存容量。

2.2.3  永久代

java虚拟机内存中的方法区在Sun HotSpot虚拟机中被称为永久代,是被各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。永久代垃圾回收比较少,效率也比较低,但是也必须进行垃圾回收,否则会永久代内存不够用时仍然会抛出OutOfMemoryError异常。

永久代也使用标记-整理算法进行垃圾回收,java虚拟机参数-XX:PermSize-XX:MaxPermSize可以设置永久代的初始大小和最大容量。

2.3   解决办法

要定位出应用程序中是什么造成了JVM内存被快速消耗殆尽,首先要对JVM内存回收原理和机制有一定的了解,把应用程序的JVM内存信息导出,然后通过MAT(Memory Analyzer Tool)工具来分析,找出消耗完内存的最大对象,然后分析代码,找出是程序哪里出了问题。

MAT是一个基于Eclipse的内存分析工具,是一个快速、功能丰富的JAVA heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗。使用内存分析工具从众多的对象中进行分析,快速的计算出在内存中对象的占用大小,看看是谁阻止了垃圾收集器的回收工作,并可以通过报表直观的查看到可能造成这种结果的对象。

3    实践情况

JAVA程序出现了内存溢出,最简单的做法是调大JVM的内存,但是一般并不能从根本上解决问题。如果确认Java应用程序出现了内存泄露的现象,那么我们一般采用下面的步骤分析:

1.      安装MAT工具。

2.      Java应用程序使用的heap dump下来。

3.      使用Java heap分析工具,结合程序的源代码找出内存占用超出预期(一般是因为数量太多)的嫌疑对象。

4.      修改代码,验证修改情况。

3.1   尝试调大JVM内存

本例中,我们把信令跟踪离线工具的JVM内存由原先的512M调整到1G,把程序启动脚本中的set JVM_MX=-Xmx512m修改为set JVM_MX=-Xmx1024m

使用信令跟踪离线工具重新打开内有10000条数据的失败观察的离线数据文件进行测试,发现工具能够正常打开。

但是如果选择离线工具的导出XLS文件功能时,工具再次无响应,N分钟后出现OutOfMemoryError错误。可见调大JVM内存不能从根本上解决内存溢出的问题;程序中一定存在不合理的地方,使得JVM无法及时回收内存,导致OutOfMemoryError错误。

 

3.2   安装MAT

http://www.eclipse.org/mat/downloads.php中根据当前的操作系统版本下载相应安装包

 

安装完成后,为了更有效率的使用 MAT,我们可以配置一些环境参数。因为通常而言,分析一个堆转储文件需要消耗很多的堆空间,为了保证分析的效率和性能,在有条件的情况下,我们会建议分配给 MAT尽可能多的内存资源。你可以采用如下两种方式来分配内存更多的内存资源给 MAT

Ø  一种是修改启动参数 MemoryAnalyzer.exe -vmargs -Xmx4g

Ø  另一种是编辑文件 MemoryAnalyzer.ini,在里面添加类似信息 -vmargs Xmx4g

 

说明:

1. MemoryAnalyzer.ini中的参数一般默认为-vmargs Xmx1024m,这就够用了。假如你机器的内存不大,改大该参数的值,会导致MemoryAnalyzer启动时,报错:Failed to create the Java Virtual Machine

2.当你导出的dump文件的大小大于你配置的1024m(说明1中,提到的配置:-vmargs Xmx1024m),MAT输出分析报告的时候,会报错:An internal error occurred during: "Parsing heap dump from XXX”。适当调大说明1中的参数即可。

3.3   导出JVM堆文件

3.3.1  设置JVM在内存不足时自动dump

如果使用Oracle JVM也就是标准的SUN JVMSUN已被oracle收购)当内存溢出时生成heapdump文件配置如下:

 

1.      -Xloggc:${目录}/temp_gc.log           GC日志文件)

2.      -XX:+HeapDumpOnOutOfMemoryError       (内存溢出时生成heapdump文件)

3.      -XX:HeapDumpPath=${目录}              heapdump文件存放位置)

 

如果不配置13参数的话,默认生成的对文件在工具当前目录下,离线工具的配置方法是在启动脚本中增加2这个配置项。

 

注:导出JVM堆文件也可以使用JDK中提供的jmap命令。

jmap -dump:format=b,file=heap.bin <pid>format=b的含义是,dump出来的文件时二进制格式。file-heap.bin的含义是,dump出来的文件名是heap.bin<pid>就是JVM的进程号。(在linux下)先执行ps aux | grep java,找到JVMpid;然后再执行jmap -dump:format=b,file=heap.bin <pid>,得到heap dump文件。

3.3.2  操作离线工具,自动生成对转储文件

使用离线工具打开失败观察离线数据文件,等待离线工具自动生成堆转储文件。在离线工具的目录下生成了一个名为java_pid4812.hprof的文件。

3.4   使用MAT分析Dump文件

打开MAT,选择菜单:FileOpen Heap Dump,选择打开失败观察离线数据文件,等待Heap Dump dump文件进行解析,最终生成一个Overview视图,这个图是一个概要图,显示了一些统计信息,包括整个size大小,class数量,以及对象的数量,同时还将生成一个大对象的top图,并线显示大对象占用内存的百分比。图中我们可以看到Class name                                                                                                    java.awt.EventQueue的对象占了431.6 MB的内存,EventQueueJDK标准API里的类,这里看不出导致内存溢出的罪魁祸首,接下来再看看别的方式来定位。

接下来的工作就是要找出溢出源。在MAT的工具栏上,有两个按钮,Histogram视图(截图里柱子那个)和Dominator Tree视图。HistogramDominator Tree的区别是站的角度不一样,Histogram是从类的角度上去看,Dominator Tree是以对象实例的角度来看,Dominator Tree可以更方便的看出其引用关系。

打开Histogram试图,Objects列表示每個class产生了多少個实例,Shallow Size列表示对象自身占用的内存大小,不包括它引用的对象。Retained Size表示当前对象大小+当前对象可直接或间接引用到的对象的大小总和。根据Retained Heap排序,可以很容易找出占内存最多的几个类,最大的是java.util.HashMap

接下来需要查看HashMap中是哪些内容,占了这么多的内存。在java.util.HashMap项的右键菜单里选择List objects---with incoming references

打开后可以看到从HashMap开始的引用树,Map里的对象都是NTLVObjectExtendNTLVObjectExtendcom.zte.appmodule.trace.entity.DescObject引用到了,DescObject又被javax.management.Attribute引用,最终以上对象都被com.zte.zxwomc.tools.trace.offline.table.TraceTable引用,而TraceTable就是界面上显示消息列表的表格,问题应该就在这里了。

3.5   分析修改代码

DescObject的代码如下, NTLVObjectExtend作为一个类变量被引用,这里可能存在一个问题,因为DescObject的实例同时被javax.management.Attribute引用,所以NTLVObjectExtend作为DescObject的一个类变量,也会一直被引用,而无法被垃圾回收,恰恰NTLVObjectExtend是一个比较大的对象。那这里完全可以把NTLVObjectExtend data的声明放到方法public DescObject(Object value, String name)中去,作为一个局部变量,每次构造函数执行完之后data变量不再被引用,就可以被垃圾回收了。

public class DescObject{

    String skey;/* NTLV 字段值 */

    String strentityName;/* 实体名称 */

    String sname;/* NLTV 字段名称 */

    String country = Locale.getDefault().getCountry();

    NTLVObjectExtend data = null;

    public DescObject(Object value, String name) {

        TraceDataObj dataObj = (TraceDataObj) value;

        strentityName = dataObj.getEntityName();

        sname = name;

        byte[] dataBuf = dataObj.getNTLVDataBuffer();

        // NTLV对象

        data = new NTLVObjectExtend(dataBuf);

        data.parser(dataObj.isHbo);

        skey = data.getNtlvValueByString(name);

    }

    private String getValue()

    {  ……  }

    public String toString(){

        return getValue();

    }

}

 

修改后的代码:

public class DescObject{

    String skey;/* NTLV 字段值 */

    String strentityName;/* 实体名称 */

    String sname;/* NLTV 字段名称 */

    String country = Locale.getDefault().getCountry();

   

    public DescObject(Object value, String name) {

        TraceDataObj dataObj = (TraceDataObj) value;

        strentityName = dataObj.getEntityName();

        sname = name;

        byte[] dataBuf = dataObj.getNTLVDataBuffer();

        // NTLV对象

        NTLVObjectExtend data = new NTLVObjectExtend(dataBuf);

        data.parser(dataObj.isHbo);

        skey = data.getNtlvValueByString(name);

    }

    private String getValue()

    {  ……  }

    public String toString(){

        return getValue();

    }

}

编译成jar包然后替换掉对应的jar文件,重新用信令跟踪离线工具打开离线用户失败观察文件,迅速就可以打开消息列表,再选择导出所有消息到Excel文件功能进行测试,可以快速而稳定的导出结果文件。

0 0
原创粉丝点击