JVM_3_OutOfMemoryError异常

来源:互联网 发布:sai mac软件下载 编辑:程序博客网 时间:2024/06/11 04:25

OutOfMemoryError异常实战

 

Java虚拟机规范的描述中,除了程序计数器之外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError异常的可能。

 

下面的栗子,都是根据书上敲的...

 

我将Java虚拟机规范中文版上传了,点击下面链接,即可下载

Java虚拟机规范SE7中文版

 



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());        }    }}运行结果:

 

解决这个区域的异常,一般手段是使用内存映像分析工具堆dump出来的堆转存储快照进行分析,重点是弄清楚 出现的是"内存泄露"还是 "内存溢出"

 

  1. 内存泄露对象已死,GC(垃圾搜集器)无法自动回收

查看泄露对象到GC Roots的引用链,可以找到泄露对象通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收

 

  1. 内存溢出对象活着,对内存容量小

检查虚拟机堆参数(-Xmx -Xms),与机器物理内存相比较,是否可以扩大。

 

如果是内存泄露,通过工具查看泄露对象到GC Roots的引用链。这样就能找到泄露对象是通过怎样的路径与GC Roots相关联并导致垃圾搜集器无法自动回收它们。

 

如果是内存溢出,应当检查虚拟机的堆参数(-Xmx -Xms) 与物理机内存对比看是否还可以调大。

并从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。

 

 

 

 

 

 

 

 

Java虚拟机栈和本地方法溢出

 

由于在HotSpot虚拟机中不区分虚拟机栈和本地方法栈,因此对于HotSpot来说,-Xoss参数(设置本地方法栈大小)虽然存在,但实际上是无效的,栈容量只由-Xss参数设定。

  • 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常
  • 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常

 

 

测试

  • 1使用-Xss参数减少栈内存容量。结果抛出StackOverflowError异常,异常出现时输出的栈深度相应缩小。
  • 2定义大量的本地变量,增加此方法帧中本地变量表的长度,结果抛出StackOverflowError异常时,输出的栈深度相应的缩小。
这个例子,对应1/** * 使用-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开始,将处于方法区中的字符串常量池移走了. import java.util.ArrayList;import java.util.List;/** * 运行时常量池溢出 * <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:内存的永久保存区域

 

从输出结果来看,运行时常量池溢出,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,falsejdk1.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);        }    }}运行结果

原创粉丝点击