OutOfMemoryError异常实战
通过前面的内容,了解熟悉了JVM运行时内存区域。
接下来尝试一下看看内粗溢出都是什么样子的。
在Java虚拟机规范的描述中,除了程序计数器之外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError异常的可能。
我将Java虚拟机规范中文版上传了,点击下面链接,即可下载
堆溢出
Java堆用于存储对象实例,我们只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,
就会在对象数量达到最大堆的容量限制之后产生的内存溢出异常。
import java.util.ArrayList;
import java.util.List;
/**
* Java堆溢出
* <p>
* 模拟测试OutOfMemeryError
* <p>
* VM options : -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
* @author CYX
* @create 2017-04-11-18:06
*/
public class Test_OutOfMemoryError {
static class OOM {
}
public static void main(String[] args) {
List<OOM> lists = new ArrayList<OOM>();
while (true) {
lists.add(new OOM());
}
}
}
"-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError":限制堆内存大小为20m,不可扩展;
通过"-XX:+HeapDumpOnOutOfMemoryError"参数可以让虚拟机在出现内存溢出的时候,将dump出当前内存堆转存储快照以便后期分析。
输出结果:
除了异常以外,jvm dump出了 .hprof 存储快照,使用 MemoryAnalyzer 打开这个存储快照。
解决这个区域的异常,一般手段是使用内存映像分析工具堆dump出来的堆 转存储快照进行分析
重点是弄清楚出现的是 "内存泄露" 还是 "内存溢出"
- 内存泄露:对象已死,GC(垃圾搜集器)无法自动回收
查看泄露对象到GC Roots的引用链,可以找到泄露对象通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收;
如果是内存泄露,通过工具查看泄露对象到GC Roots的引用链。
这样就能找到泄露对象是通过怎样的路径与GC Roots相关联并导致垃圾搜集器无法自动回收它们。
- 内存溢出:对象活着,对内存容量小
检查虚拟机堆参数(-Xmx 与 -Xms),与机器物理内存相比较,是否可以扩大。
如果是内存溢出,应当检查虚拟机的堆参数(-Xmx 和 -Xms) 与物理机内存对比看是否还可以调大。
并从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
Java虚拟机栈和本地方法溢出
由于在HotSpot虚拟机中不区分虚拟机栈和本地方法栈,因此对于HotSpot来说,-Xoss参数(设置本地方法栈大小)虽然存在,但实际上是无效的,栈容量只由-Xss参数设定。
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常
- 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常
测试
- 使用-Xss参数减少栈内存容量。结果抛出StackOverflowError异常,异常出现时输出的栈深度相应缩小。
- 定义大量的本地变量,增加此方法帧中本地变量表的长度,结果抛出StackOverflowError异常时,输出的栈深度相应的缩小。
/**
* 使用-Xss参数减少栈内存容量
*
* VM options : -Xss128k
*
* @author CYX
* @create 2017-04-11-18:51
*/
public class Test_StackOverflowError_2 {
private int stackLenght = 1;
public void stackLeak() {
stackLenght++;
stackLeak();
}
public static void main(String[] args) throws Throwable{
Test_StackOverflowError_2 test = new Test_StackOverflowError_2();
try {
test.stackLeak();
} catch (Throwable e) {
System.out.println("stack lenght : " + test.stackLenght);
throw e;
}
}
}
结果:
测试结果:在单个线程下,无论是栈帧太大,还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常
注意:测试这个代码,会造成电脑卡住,甚至死机...
/**
* 通过不断地建立线程,产生内存溢出异常
*
* VM options : -Xss2M
*
* @author CYX
* @create 2017-04-11-19:32
*/
public class Test_StackOverflowError_3 {
private void dontStop() {
while (true) {
}
}
public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
}
public static void main(String[] args) throws Throwable{
Test_StackOverflowError_3 test = new Test_StackOverflowError_3();
test.stackLeakByThread();
}
}
方法区和运行时常量池溢出
由于运行时常量池是方法区的一部分,因此这两个区域的溢出测试就放在一起进行。
前面我们提到过,JDK1.7开始逐步"去永久代"的事情。
String.intern()是一个Native方法,它的作用是:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的string对象。
否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。
(注意:这是jdk1.6之前的做法)
由于常量池在方法区中,通过-XX:PermSize 和-XX:MaxPermSize 限制方法区的大小,从而间接限制其中常量池的容量。
这个例子 使用JDK1.6之后的版本是测不出来的...必须使用JDK1.6之前的版本,jdk1.7开始,将处于方法区中的字符串常量池移走了
/**
* 运行时常量池溢出
* <p>
* VM options : -XX:PermSize=10M -XX:MaxPermSize=10M
*
* @author CYX
* @create 2017-04-11-19:46
*/
public class Test_RuntimeConstantPoolOOM_4 {
public static void main(String[] args) {
//使用list保持常量池引用,避免Full GC回收常量池行为
List<String> list = new ArrayList<String>();
//10M的permSize在integer范围内足够产生OOM了.
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
}
PermGenspace:方法区(永久代Hotspot)
从输出结果来看,运行时常量池溢出,OutOFMemoryError后面跟随的提示信息是PermGen space,说明运行时常量池属于方法区(永久代)的一部分。
使用JDK1.7运行这段代码,会得到不同的结果,while循环将一直进行下去。
-------------------------------------------------------------------------------------------------------------------
关于JDK 1.6之后的版本的区别,我们用一个栗子看下:
String.intern()是一个Native方法,它的作用是:
如果字符串常量池中已经包含了一个等于次对象的字符串,则返回代表这个字符串的String对象;
否则,将此String对象包含的字符串添加到常量池中。
下面这个Demo,使用JDK1.6 和 JDK1.7跑出来的结果是由区别的....
public static void main(String[] args) {
String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuilder("12").append("34").toString();
System.out.println(str2.intern() == str2);
}
jdk1.6:false,false
jdk1.7:true,true
为什么呢?
StringBuilder创建的实例是在Java堆中的
JDK1.6中:
intern()方法会将首次遇到的字符串复制到永久代中,返回的也是永久代中这个字符串实例的引用。
所以,必然不是同一个引用,输出的都是false
JDK1.7中:
intern()实现不会再复制实例,只是在常量池中记录首次出现的实例引用,因此intern()返回的引用和StringBuilder创建的那个字符串实例是同一个。
StringBuilder创建的实例是在Java堆中的。
因此intern()返回的引用和由StringBuilder创建的那个字符串实例是同一个。
对了,在JDK1.7环境下试试这个:
String str3 = new StringBuilder("ja").append("va").toString();
System.out.println(str3.intern() == str3);
你会有不一样的发现(手动斜眼...)
本机直接溢出
DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定则默认与Java堆的最大值(-Xmx)一样。
import sun.misc.Unsafe;
import java.lang.reflect.Field;
/**
* 本机直接内存溢出
* <p>
* VM options : -Xmx20M -XX:MaxDirectMemorySize=10M
*
* @author CYX
* @create 2017-04-12-13:20
*/
public class Test_DirectMemeoryOOM_5 {
private static final int _1M = 1024 * 1024;
public static void main(String[] args) throws Exception {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1M);
}
}
}
运行结果