JVM内存溢出与排错

来源:互联网 发布:理想相册软件 编辑:程序博客网 时间:2024/06/04 13:58

JVM内存溢出与排错

一、JVM堆内存溢出

Java堆用于存储对象实例,我们只要不断创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制来清除这些对象,就会在对象数量达到最大队的容量限制后产生内存溢出异常。

代码:

/** * Java堆内存溢出 * @author jiangtong * */public class HeapOOM {static class OOMObject{}/** * -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:PermSize=32M -XX:MaxPermSize=64M -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError  */public static void main(String[] args) { List<OOMObject> list = new ArrayList<OOMObject>(); int count = 0; while(true){ try {count++;list.add(new OOMObject());System.out.println("共构造了"+count+"个对象");Thread.sleep(100);} catch (Exception e) {e.printStackTrace();} } }}


 

虚拟机执行参数说明:

Xms20M -Xmx20M:最小堆内存和最大堆内存设置为一样是为了避免堆内存的自动扩展

-Xmn10M:新生代内存分配10M,剩余的交给老年代

-XX:SurvivorRatio=8:新生代中Eden区和Survivor区的内存比例为8:1

-XX:PermSize=32M:永久代最小内存32M

-XX:MaxPermSize=64M:永久代最大扩展内存64M

-XX:+HeapDumpOnOutOfMemoryError:堆内存溢出时Dump出当前的内存堆转储快照以便事后分析

1、控制台输出分析:

[GC [DefNew: 8192K->1024K(9216K), 0.0426563 secs] 8192K->4599K(19456K), 0.0427200 secs]

[GC [DefNew: 6237K->1024K(9216K), 0.0552875 secs] 9813K->9742K(19456K), 0.0553528 secs] [GC [DefNew: 7581K->7581K(9216K), 0.0000467 secs][Tenured: 8718K->10240K(10240K), 0.1228164 secs] 16299K->11923K(19456K), [Perm : 2086K->2086K(32768K)], 0.1229961 secs]

[Full GC [Tenured: 10240K->7996K(10240K),0.1247832 secs] 19456K->15528K(19456K), [Perm : 2086K->2086K(32768K)], 0.1248966 secs]

[Full GC [Tenured: 8598K->8598K(10240K), 0.1296810 secs] 17814K->17814K(19456K), [Perm : 2086K->2086K(32768K)], 0.1297631 secs]

[Full GC [Tenured: 8598K->8595K(10240K), 0.1481811 secs] 17814K->17811K(19456K), [Perm : 2086K->2084K(32768K)], 0.1482582 secs]

java.lang.OutOfMemoryError: Java heap space

Dumping heap to java_pid3504.hprof ...

Heap dump file created [32580683 bytes in 1.553 secs]

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

    at java.util.Arrays.copyOf(Arrays.java:2760)

    at java.util.Arrays.copyOf(Arrays.java:2734)

    at java.util.ArrayList.ensureCapacity(ArrayList.java:167)

    at java.util.ArrayList.add(ArrayList.java:351)

    at biz.HeapOOM.main(HeapOOM.java:25)

Heap

 def new generation   total 9216K, used 9216K [0x029e0000, 0x033e0000, 0x033e0000)

  eden space 8192K, 100% used [0x029e0000, 0x031e0000, 0x031e0000)

  from space 1024K, 100% used [0x031e0000, 0x032e0000, 0x032e0000)

  to   space 1024K,   0% used [0x032e0000, 0x032e0000, 0x033e0000)

 tenured generation   total 10240K, used 8601K [0x033e0000, 0x03de0000, 0x03de0000)

   the space 10240K,  84% used [0x033e0000, 0x03c46780, 0x03c46800, 0x03de0000)

 compacting perm gen  total 32768K, used 2105K [0x03de0000, 0x05de0000, 0x07de0000)

   the space 32768K,   6% used [0x03de0000, 0x03fee770, 0x03fee800, 0x05de0000)

No shared spaces configured.

 

解读第一行代码:

[GC [DefNew: 8192K->1024K(9216K), 0.0426563 secs] 8192K->4599K(19456K), 0.0427200 secs]

其意思是对于本次Minor收集,新生代堆内存从占用8192K--à收集后仅占用1024K,耗时0.0426563秒;而整个堆内存从占用内存8192K--à收集后仅占用4599K,耗时0. 0427200

 

观察整个输出去可以看出以供进行了三次MinorGC,三次FullGC,解释一下:

l  Minor GC[新生代GC]:指发生在新生代的垃圾收集动作,因为Java对象大多具有朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也较快

l  Full GC[老年代GCMajor GC]:指发生在老年代的GC,出现了Full GC,通常会伴随一次的Minor GCFullGC一般比MinorGC10倍以上

 

从上面的日志中我们总结如下:

前提:我们创建的测试对象始终是没有释放内存的

l  第一次的Minor GC和第一次Full GC成效比较显著[相比较第二次手机回收的内存比较多],这是因为他们释放了本次运行程序之前生成的一些对象。

l  从第二次Full GC效果就不那么显著了是因为,老年代的中已经没有多少可以释放的内存【我们创建的对象是一直不会释放的】。

l  由于老年代内存的限制多次GC后无显著成效[老年代已满],最终导致堆内存溢出,并生成了二进制格式的日志文件:java_pid3504.hprof,利用这个文件可以进行JVM内存溢出分析。

l  +HeapDumpOnOutOfMemoryError参数让我们看到了溢出后的堆内存情况,以下解读:

n   Heap

n   def new generation   total 9216K, used 9216K [0x029e0000, 0x033e0000, 0x033e0000)

//以上表示新生代总内存9216K,占用9216K

n   eden space 8192K, 100% used [0x029e0000, 0x031e0000, 0x031e0000)

//以上表示新生代的伊甸区总内存8192K,占用100%

n   from space 1024K, 100% used [0x031e0000, 0x032e0000, 0x032e0000)

//以上表示from space幸存区总内存1024K,占用100%

n   to   space 1024K,   0% used [0x032e0000, 0x032e0000, 0x033e0000)

//以上表示to space幸存区总内存1024K,占用0%

n   tenured generation   total 10240K, used 8601K [0x033e0000, 0x03de0000, 0x03de0000)

n   the space 10240K,  84% used [0x033e0000, 0x03c46780, 0x03c46800, 0x03de0000)

//以上两行表示老年代总内存10240K,占用8601K,占用比例是84%

n   compacting perm gen  total 32768K, used 2105K [0x03de0000, 0x05de0000, 0x07de0000)

n   the space 32768K,   6% used [0x03de0000, 0x03fee770, 0x03fee800, 0x05de0000)

//以上两行表示永久代[方法区]总内存32768K,占用2105K,占用比例6%

No shared spaces configured.

 

2、使用MAT对内存溢出日志“java_pid3504.hprof”进行分析

         2.1、安装MAT

         下载地址:

http://www.eclipse.org/downloads/download.php?file=/mat/1.2.1/MemoryAnalyzer-1.2.1.201211051250.zip

         安装:

         我用的MyEclipse,将包解压到某个目录[无限制],我放到这里:D:\Program Files\Genuitec\Common\MAT1.2

         配置一个插件的链接[这个是必须的],路径:Genuitec\MyEclipse\dropins,在这个目录下建一个文件扩命名为.link,比如:MAT.link,这个文件内容就一句话:

path=插件的解压路径,例如:path=D:/Program Files/Genuitec/Common/MAT1.2

    2.2、获得堆转储文件

你可以采用如下方式取得堆转储文件:

-XX:+HeapDumpOnOutOfMemoryError

JVM 就会在发生内存泄露时抓拍下当时的内存状态,也就是我们想要的堆转储文件。这种方式适合于生产环境。本文采用的这种方式

-XX:+HeapDumpOnCtrlBreak

如果你不想等到发生崩溃性的错误时才获得堆转储文件,也可以通过设置如下 JVM 参数来按需获取堆转储文件。

除此之外,还有很多的工具,例如 JMap,JConsole 都可以帮助我们得到一个堆转储文件。您可以使用命令:JMap -dump:format=b,file=<dumpfile> <pid>

不过,您需要了解到,不同厂家的 JVM 所生成的堆转储文件在数据存储格式以及数据存储内容上有很多区别, MAT 不是一个万能工具,它并不能处理所有类型的堆存储文件。但是比较主流的厂家和格式,例如 Sun, HP, SAP 所采用的 HPROF 二进制堆存储文件,以及 IBM 的 PHD 堆存储文件等都能被很好的解析。


2.3、生成分析报告

选择Eclipse的 File-Open,打开前面获得的堆转储文件,本文是:java_pid4456.hprof,打开这个文件后,如下图:

同时我们注意到项目下面多了几个在打开过程中产生的分析文件:

尤其是这个扩展名为.zip文件非常有意义,它是对分析结果做了一个压缩文件,压缩文件里面是网页格式的分析报告,这样你可以讲这个报告email给你的伙伴,你们一起分析。不得不说MAT想的周到J

2.4、分析

l  首先我们得到的是一个内存泄露推测的分析报告,这一点从标题上[Leak Suspects]可以看出来。

点击LeaksàOverview,我们看到如下:

这个图上告诉我们总共堆内存17.6MB,灰色区域的对象占用了17.3MB,剩下的仅仅307.5KB,灰色区域就是推测存在内存泄露的区域,接下来的“Problem Suspect 1”里面为我们带来了这个区域的解释:

上面的推测告诉我们:

n  线程“main”持有了18,095,992字节大小的本地变量。

n  内存被系统加载器[System class loader]加载的对象数组[java.lang.Object[]]的实例堆积.

点击下面的”Details>>”:

从上图中“Shortest Paths To the Accumulation Point”可以看出是“距离对象堆积点最近的路径”:main线程-----àArrayList

同时下面可以看到堆积的对象[Accumulated Objects],我们可以看出这一样一个引用链:main线程引用Object[],Object[]引用若干个OOMObject,每个OOMObject占用对内存8个字节[对象头是8字节,成员变量int4字节、String引用是4字节,我们这里的对象没有属性故是8字节]。

接下来是:

从名字可以看出意思是:“通过XXX类堆积的对象”,从表格中我们不难看出:程序中biz.HeapOOM$OOMObject这个类的对象在堆内存上堆积了1,507,963个。使用了12,063,704个字节的对内存空间,很明显已经超出了我们定义的老年代的对内存上限[-Xms20M -Xmx20M -Xmn10M,对内存共20M,新生代占用10M,剩下的是老年代的内存10M],由于这1,507,963个对象到GC root都有路径可达因此虚拟机无法释放他们占用的内存,导致内存泄露,进而导致内存溢出。

 

二、JVM方法区内[永久代]存溢出

1、 运行时常量池溢出

在HotSpot虚拟机中运行时常量池是属于方法区的一部分,所以我们可以通过向运行时常量池中放入常量来导致方法区内存溢出。

如果要向运行时常量池中添加内从,最简单的做法就是使用String.intern()这个Native方法。该方法的作用是:如果池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。由于常量池分配在方法区内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小。

代码如下:

/** * 运行时常量池溢出 * -XX:PermSize=10M -XX:MaxPermSize=10M -XX:+HeapDumpOnOutOfMemoryError * @author jiangtong * */public class ConstanOOM {public static void main(String[] args) {List<String> list = new ArrayList<String>();int i=0;while(true){list.add(String.valueOf(i++).intern());System.out.println(i);}}}

通过JVisualVM工具我们观察永久代内存的变化:

永久代占用内存一直往上涨,由于MaxPermSize的限制是10M导致内存溢出:

java.lang.OutOfMemoryError: PermGen space

Dumping heap to java_pid9704.hprof ...

Heap dump file created [11755242 bytes in 0.594 secs]

Exception in thread "main" java.lang.OutOfMemoryError: PermGen space

    at java.lang.String.intern(Native Method)

    at biz.ConstanOOM.main(ConstanOOM.java:19)

通过Eclipse MAT分析:

我们看得出堆积了大量的String对象。

1、 方法区溢出

方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。对这个区域的测试,基本思路是运行时产生大量的类去填充方法区,直到溢出。这里我们借助CGLib在运行时动态生成大量的动态类来达到目的。

我们可以看看来自CGLib官方http://cglib.sourceforge.net/的介绍:

cglib is a powerful, high performance and quality Code Generation Library, It is used to extend JAVA classes and implements interfaces at runtime

示例代码:

/** * 方法区内存溢出 * -XX:PermSize=16M -XX:MaxPermSize=16M * @author jiangtong * */public class MethodAreaOOM {public static void main(String[] args) {while(true){Enhancer enhancer = new Enhancer();enhancer.setInterfaces(new Class[]{IWork.class});enhancer.setUseCache(false);enhancer.setCallback(new NoOp() {});try {enhancer.create();}  catch (Exception e1) {// TODO Auto-generated catch blocke1.printStackTrace();}}}}

 

我们通过jvisualvm观察方法区内存的变化发现这个方法区内存一直是高歌猛进:

当达到我设定的方法区内存上限后抛出:

Caused by: java.lang.OutOfMemoryError: PermGen space

  at java.lang.ClassLoader.defineClass1(Native Method)

  at java.lang.ClassLoader.defineClass(ClassLoader.java:621)

    ... 8 more

           从日志中我们不难看出是方法区[PermGen Space]发生生了内存溢出。

原创粉丝点击