JVM_2.0_OutOfMemoryError异常

 

OutOfMemoryError异常实战

通过前面的内容,了解熟悉了JVM运行时内存区域。

接下来尝试一下看看内粗溢出都是什么样子的。

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

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

Java虚拟机规范SE7中文版

 

堆溢出

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);
        }
    }
}

运行结果

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值