第二章:Java内存区域与内存溢出异常(三)

2.4 实战:OutOfMemoryError异常

2.4.1 虚拟机启动参数设置

在下面的代码实验中,虚拟机参数的设置对于实验的结果具有直接的影响,那么设置虚拟机参数可以直接在Java命令之后书写就可以了,也可以在开发工具中设置,《深入理解Java虚拟机》中,使用的是Eclipse开发工具,可以在Debug/Run页签中设置,博主使用的是IDEA,所以下面介绍一下在IDEA中设置的方法和步骤。

1、选择Help ->Edit Custom VM Options
在这里插入图片描述
2、就可以看到打开虚拟机参数设置的文件,就可以在下面进行更改。
在这里插入图片描述
如果不想要在IDEA中更改,也可以直接在文件中找到上述的虚拟机参数文件,一文件目录一般都是在:C:\Users***.IntelliJIdea2019.3\config 目录下:
在这里插入图片描述

2.4.2 Java堆溢出

Java堆是运行时数据区域中最大的一块内存,用来存储创建的对象实例,在Java堆中没有内存完成实例时,且堆无法扩展的情况之下,Java虚拟机将会抛出OutofMemoryError异常。
那么如何设计使出现Java堆产生内存溢出异常呢?
我们可以不断的进行创建对象,并保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么随着对象数量的增加,总容量触及最大堆的容量限制之后就会产生内溢出异常。
在Java虚拟机参数设置的文件中,将Java堆的大小设置成为20MB,并且不可扩展。设置堆的最小值:-Xms20m,设置堆的最大值:-Xmx20m,大小参数设置一样是为了避免堆的自动扩展。然后通过参数:-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常的时候Dump出当前的内存堆转存储快照以便进行事后分析。

public class Demo01 {
   static class OOMObject{}

    public static void main(String[] args) {
        List<OOMObject> oomObjects = new ArrayList<>();
        while (true){
            oomObjects.add(new OOMObject());
        }
    }
}

运行结果:
在这里插入图片描述
从上面报错信息可以看出来,当出现Java堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟随进一步提示“Java heap space”。

在实际应用中,Java堆出现OutOfMemoryError是最常见的内存溢出异常情况。那么要解决这个异常情况的常规方式是通过内存映像分析工具对Dump出来的堆转存储快照进行分析。

  1. 第一步判断内存中导致内存溢出异常的对象是否都是必须的,即先判断出是出现了内存泄漏还是内存溢出。
  2. 如果是内存泄漏,那么可以通过工具进一步查看泄漏对象到CG Roots的引用链,找到泄漏对象是通过怎样的引用路径、与哪些GC Roots相关联的,才导致垃圾收集机制没有将它门回收,根据泄漏 对象的类型信息以及它到GC Roots引用链的信息,一般可以较为准确的定位到这些对象创建的位置,进而找到产生内存泄漏的代码的具体位置。
  3. 如果不是内存泄漏,那也就是所有的对象都必须要存活,那么就要检查虚拟机的堆参数(-Xmx和-Xms)设置,与机器相比较,是否还有能够调整的内存空间。在从代码上检查是否存在某些对象的生命周期过长,持有状态时间过长,存储结构设计不合理等情况,尽量减少程序运行器的内存消耗。

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

虚拟机栈和本地方法栈是线程所私有的,在虚拟机中通过-Xoss参数来设置本地方法栈的大小。但是在《Java虚拟机规范》中没有明确对本地方法栈进行要求,所以有的虚拟机甚至不区分虚拟机栈和本地方法栈,例如我们这里讨论的HotSpot虚拟机,所以对于HotSpot虚拟机来说,-Xoss参数即便存在,但是不会起到任何效果,栈的容量只能通过-Xss来设定。
对于虚拟机栈和本地方法栈,《Java开发规范》中描述了两种异常:

  1. 当线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
  2. 如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常。

在《Java虚拟机规范》中明确允许Java虚拟机自行决定是否支持栈的动态可扩展,而HotSpot虚拟机的选择是不允许的,所以除非在创建线程的时候内存就不够使用了而出现OutOfMemoryError异常,否则,在线程运行时是不会因为扩展而导致内存溢出的(因为它都不允许扩展),只会因为栈容量无法容纳新的栈帧而导致StackOverflowError异常。

所以我们可以通过以下两种方式来进行实验(实验范围限制在单线程内)

  1. 使用-Xss参数减少栈内存容量。
public class JavaVMStackSOF_1 {
    private int stackLength = 1;

    public void stackLeak() {
        stackLength++;
        stackLeak();
    }

    public static void main(String[] args) {
        JavaVMStackSOF_1 oom = new JavaVMStackSOF_1();
        try {
            oom.stackLeak();
        } catch (Throwable e) {
            System.out.println("stack length:" + oom.stackLength);
            throw e;
        }
    }
}

运行结果:
减少栈内存
结果抛出了StackOverflowError异常,异常出现时输出的堆栈深度相应缩小。

  1. 定义大量的本地变量,增大此方法帧中本地变量表的长度
public class JavaVMStackSOF_2 {
   private static int stackLength = 0;

   public static void test() {
       long unused1, unused2, unused3, unused4, unused5,
               unused6, unused7, unused8, unused9, unused10,
               unused11, unused12, unused13, unused14, unused15,
               unused16, unused17, unused18, unused19, unused20,
               unused21, unused22, unused23, unused24, unused25,
               unused26, unused27, unused28, unused29, unused30,
               unused31, unused32, unused33, unused34, unused35,
               unused36, unused37, unused38, unused39, unused40,
               unused41, unused42, unused43, unused44, unused45,
               unused46, unused47, unused48, unused49, unused50,
               unused51, unused52, unused53, unused54, unused55,
               unused56, unused57, unused58, unused59, unused60,
               unused61, unused62, unused63, unused64, unused65,
               unused66, unused67, unused68, unused69, unused70,
               unused71, unused72, unused73, unused74, unused75,
               unused76, unused77, unused78, unused79, unused80,
               unused81, unused82, unused83, unused84, unused85,
               unused86, unused87, unused88, unused89, unused90,
               unused91, unused92, unused93, unused94, unused95,
               unused96, unused97, unused98, unused99, unused100;

       stackLength ++;
       test();

       unused1 = unused2 = unused3 = unused4 = unused5 =
               unused6 = unused7 = unused8 = unused9 = unused10 =
                       unused11 = unused12 = unused13 = unused14 = unused15 =
                               unused16 = unused17 = unused18 = unused19 = unused20 =
                                       unused21 = unused22 = unused23 = unused24 = unused25 =
                                               unused26 = unused27 = unused28 = unused29 = unused30 =
                                                       unused31 = unused32 = unused33 = unused34 = unused35 =
                                                               unused36 = unused37 = unused38 = unused39 = unused40 =
                                                                       unused41 = unused42 = unused43 = unused44 = unused45 =
                                                                               unused46 = unused47 = unused48 = unused49 = unused50 =
                                                                                       unused51 = unused52 = unused53 = unused54 = unused55 =
                                                                                               unused56 = unused57 = unused58 = unused59 = unused60 =
                                                                                                       unused61 = unused62 = unused63 = unused64 = unused65 =
                                                                                                               unused66 = unused67 = unused68 = unused69 = unused70 =
                                                                                                                       unused71 = unused72 = unused73 = unused74 = unused75 =
                                                                                                                               unused76 = unused77 = unused78 = unused79 = unused80 =
                                                                                                                                       unused81 = unused82 = unused83 = unused84 = unused85 =
                                                                                                                                               unused86 = unused87 = unused88 = unused89 = unused90 =![在这里插入图片描述](https://img-blog.csdnimg.cn/20210125172436120.PNG?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzI4ODQ0Nw==,size_16,color_FFFFFF,t_70#pic_center)

                                                                                                                                                       unused91 = unused92 = unused93 = unused94 = unused95 =
                                                                                                                                                               unused96 = unused97 = unused98 = unused99 = unused100 = 0;
   }

   public static void main(String[] args) {
       try {
           test();
       }catch (Error e){
           System.out.println("stack length:" + stackLength);
           throw e;
       }
   }
}

结果如图:
增加本地变量
结果抛出StackOverflowError异常,异常出现时输出的堆栈深度相应缩小。

那么,通过上面的两个实验中可以看出来,无论是由于栈帧太大(第二个实验)还是虚拟机栈容量太小(第一个实验),当新的栈帧内存无法分配的时候,HotSpot虚拟机都将会抛出StackOverflowError异常。
但是在允许动态扩展的虚拟机中上面的实验可能就会出现不同的结果,如在Classic虚拟机中运行上面第二个实验代码JavaVMStackSOF_2,则会导致java.lang.OutOfMemoryError异常。因为在这个线程中,运行一次stackLeak()方法,就会将该方法的所有变量存放到栈帧中,因为栈帧很大,所以没执行几个就会达到栈容量的最大值,这时开始进行自动扩展栈内存的容量,同样,stackLeak()方法也还在执行,直到没有足够的内存空间可以供栈来扩展,这时就会报出OutOfMemoryError异常。

上面的两个实验实在单线程的基础上进行的,那么如果在多线程的条件下,HotSpot虚拟机是可以出现内存溢出异常的,但是这其实跟栈空间是否足够并没有任何直接的关系,主要取决于操作系统本身内存的使用状态。如下代码:

public class JavaVMStackOOM {

    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 {
        JavaVMStackOOM oom = new JavaVMStackOOM();
        oom.stackLeakByThread();
    }
}

在执行上面程序之前,可以将栈的容量大小调整大一些,因为栈内存被线程所私有,那么每个线程的栈容量大了,线程的容量自然变大,那么在固定的内存中,线程数量就会变少,所以,在这种情况之下,对每个线程的栈空间分配越大,越容易出现内存溢出异常。

在实际应用中,如果出现了StackOverflowError异常时,会有明确的错误对战可以分析,相对而言比较容易定位到问题的所在。对于HotSopt虚拟机来说,栈深度在大说情况之下到达1000-2000是完全没有问题的,对于正常的方法调用,完全够用。对于建立较多的多线程导致的内存溢出,在不能减少线程数量或者更换64为虚拟机的情况之下,就只能通过减少最大堆和减少栈容量来换取更多的线程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值