JVM OOM异常种类测试和解决方案总结
本文为学习《深入理解java虚拟机》书中2.4节实战部分的整理总结。
jvm中能引起Out of memory Error 的运行时内存存储区域大致可以分为:
- java堆区溢出
- java虚拟机栈和本地方法栈溢出
- 方法区和运行时常量池溢出
- 本机直接内存溢出
下面通过示例代码来观察各个区域出错的状况。
一、java堆区溢出
1 | import java.util.ArrayList; |
出错信息:
1 | java.lang.OutOfMemoryError: **Java heap space** |
基本思路:
java中的对象实例都是储存在堆区中,所以我们通过参数-Xms20m -Xmx20m
固定好java堆的大小,并且保证有可达路径来避免GC回收对象,就可以很快将堆区占满,从而发生内存溢出异常,异常信息中的Java heap space
也可以证明这一点。
另一个参数-XX:+HeapDumpOnOutOfMemoryError
可以在堆溢出时dump出当前内存堆转储快照,我们用Jprofiler打开快照文件查看内存占用情况:
Name | Instance Count | Size |
---|---|---|
com.yzb.HeapTest$OOMObject | 810,326 | 12,965 kB |
char[ ] | 2,294 | 313 kB |
java.lang.String | 2,145 | 51,480 bytes |
java.util.TreeMap$Entry | 791 | 31,640 bytes |
java.lang.Object[ ] | 583 | 3,274 kB |
java.lang.Class | 546 | 174 kB |
可以看到 com.yzb.HeapTest$OOMObject
占用了绝大部分的内存,从而定位出是OOMObject对象实例过多的缘故。
解决方案:
通过内存分析工具分析造成堆溢出的情况。如果是内存泄漏,找到泄漏对象的引用路径,定位对象创建和造成泄漏的位置。如果是内存溢出,检查虚拟机的堆参数-Xmx -Xms
是否还有上调的空间,再从代码上检查是否对象生命周期过长,设计不合理等情况,避免过多的内存消耗。
二、java虚拟机栈和本地方法栈溢出
关于栈溢出,我们可以有两种方法测试:
- 缩小栈内存容量
- 增加栈帧长度
我们先看第一种:
1 | package com.yzb; |
错误信息:
1 | stack length: 983 |
我们通过缩小栈容量,和无止境的递归调用来触发Stack Overflow异常。
这里可以看到,我们在128k的栈容量下,对于stackLeak方法,允许983次递归调用的深度。
再来试试第二种:
1 | package com.yzb; |
错误信息:
1 | stack length: 5209 |
现在使用jvm默认的栈大小,并且在方法内定义了100个本地变量来增加每个方法的调用栈帧。
我们看到该方法的调用深度为5209,并且报了StackOverflowError。
解决方案:
对于StackOverflowError, 通常可以设置增加栈容量或者试着将其栈帧变大的方法内部进行削减本地变量,当然更多时候,栈溢出是递归过深造成的,试着减少递归深度或者改写成迭代形式。
三、方法区和运行时常量池溢出
测试运行时常量池溢出,我们立马会想到String,但由于JDK7之后已经将字符串常量池移到了java堆区,所以只能通过限制堆区容量来观察错误信息,而JDK6之前可以通过限制永久代容量来看到错误情况。
1 | package com.yzb; |
JDK7 的报错信息:
1 | Exception in thread "main" java.lang.OutOfMemoryError: Java heap space |
JDK8之后,元空间替代了永久代,如果到达了元空间初始空间大小,会触发垃圾收集进行类型卸载,并调整空间大小。
四、本机直接内存溢出
通过反射获取Unsafe实例不断进行内存分配来导致本机直接的内存溢出。
1 | package com.yzb; |
报错信息:
1 | allocated 13630MB memory. |
可以看到在我的机器上申请了13GB的内存后出现了OutOfMemoryError。