第二章 Java内存区域与内存溢出异常 | part 3

这一部分属于实践啦,将会模拟各种 OOM 和 SOF 的情况,以此帮助大家在工作中遇到相关错误时,能够知道如何去处理。

基础知识

由上一 part 的内容我们知道,在 Java 的运行时数据区中,除了程序计数器 PC,其他区域都定义了相关的内存泄漏 / 内存溢出错误,主要包括 OutOfMemoryError 和 StackOverFlowError 两种,具体可以查看上一篇文章。

本文将对各个数据区的内存泄漏 / 内存溢出进行模拟,因此必不可少的需要对 JVM 的参数进行修改。以 IDEA 为例,具体的修改方式如下:

然后点击 Apply 确认就好啦!(注意不要把 VM 参数写到 Program arguments 里噢!)

Java 堆溢出

Java 堆用于存储对象实例,因此要模拟堆溢出,只需要用一个死循环不断生成新的对象即可。在实验之前,我们通过加入参数 -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError 将堆的大小限制在 20MB 且不可扩展,另外在内存溢出时生成 Dump 文件以便分析。

java 复制代码
public class HeapOOM {
    static class OOMObject {}
    public static void main(String[] args) throws Exception {
        // VM options:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
        List<OOMObject> list = new ArrayList<>();
        while(true) {
            list.add(new OOMObject());
        }
    }
}

运行后可以得到下面这样的结果:

说明发生了 OOM 异常,且 OOM 区域是 Java 堆区。此时我们可以对 Dump 文件进行分析,分辨出是内存泄漏还是内存溢出。如果是内存泄漏,则可进一步对泄漏对象的 GC Roots 引用链进行分析,从而定位到泄漏代码的具体位置;如果是内存溢出,说明内存中的对象都是必须存活的,那就可以尝试修改 JVM 参数( -Xms-Xmx) ,并检查代码是否存在设计不合理的地方,如某些对象的生命周期过长等。具体的处理过程将在第三章结合垃圾回收进行分析。

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

对于栈区, JVM 规范中定义了两种异常:

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

由于 HotSpot 虚拟机不支持栈的动态扩展,接下来的实验将只会模拟第一种异常。

要模拟第一种异常有两种方式:一是不定义额外变量,仅通过无限递归申请无穷的栈帧,这种情况下每个栈帧占用空间少,异常时的堆栈深度大;二是通过定义大量本地变量,增大每个栈帧的长度,这种情况下每个栈帧占用空间大,异常时的堆栈深度小。下面通过加入参数 -Xss128k 限制栈的大小来模拟两种情况下的 SOF 异常。

  • 情况一:
java 复制代码
public class JavaVMStackSOF {
    private int stackLength = 1;
    public void stackLeak() {
        stackLength++;
        stackLeak();
    }

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

运行后可以得到下面这样的结果:

  • 情况二:
java 复制代码
public class JavaVMStackSOF {
    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 =
        unused91 = unused92 = unused93 = unused94 = unused95 =
        unused96 = unused97 = unused98 = unused99 = unused100 = 0;
    }

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

运行后可以得到下面这样的结果:
如果栈的大小允许动态扩展,那上面两个示例均将产生 OOM 异常,而非 SOF。

另外在多线程下,HotSpot 也会产生 OOM 异常。因为操作系统为 JVM 进程分配的内存是有限的,去掉堆区、方法区和其他一些资源后,剩下的就由不同线程的栈区瓜分。因此当线程数量太多,导致操作系统分配给虚拟机的内存被使用完时,会产生 OOM 异常。由上述描述可以得知,在这种情况下,为每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。

加入参数 -Xss2M 模拟上述情况。

java 复制代码
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();
    }
}

需在 32 位操作系统下模拟,结果将是 OutOfMemoryError: unable to create native thread,本文不做展示。

对于这种多线程导致的内存溢出,在不能减少线程数量或者更换 64 位虚拟机的情况下,只能通过减少最大堆和减少栈容量来获取更多的线程。

方法区和运行时常量池溢出

在 JDK 8 之后,字符串常量池被保留在了堆中,而永久代则被元空间取代,退出了历史舞台,因此方法区和运行时常量池的溢出变得不再那么常见。

这里简单介绍一下控制元空间的几个参数:

  • -XX:MaxMetaspaceSize:设置元空间最大值,默认为 -1,即仅受限于本地内存大小。
  • -XX:MetaspaceSize:指定元空间的初始大小,到达该值就会触发垃圾回收进行类卸载,同时收集器会对该值进行调整:如果释放了大量空间则适当降低该值;如果释放了很少的空间,则在不超过 MaxMetaspaceSize 的情况下适当提高该值。
  • -XX:MinMetaspaceFreeRatio:在垃圾回收之后控制最小的元空间剩余容量的百分比,即空闲比小于该值时会适当扩容,可减少因为元空间不足导致的垃圾回收频率。

本机直接内存溢出

直接内存的容量大小可通过 -XX:MaxDirectMemorySize 指定,默认与 Java 堆最大值一致。

下面通过加入参数 -XX:MaxDirectMemorySize=10M 来模拟直接内存溢出。模拟原理是通过 Unsafe 类的 allocateMemory() 方法来分配直接内存。Unsafe 类的对象需要通过反射获取。

java 复制代码
public class DirectMemoryOOM {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) throws Throwable {
        // -XX:MaxDirectMemorySize=10M
        // 通过反射获取Unsafe类的第一个成员变量,即"Unsafe theUnsafe"
        Field unsafeField = Unsafe.class.getDeclaredFields()[0];
        // 取消安全检查
        unsafeField.setAccessible(true);
        // 通过get得到一个Unsafe对象,geet(null)是因为这是一个stati成员变量
        Unsafe unsafe = (Unsafe) unsafeField.get(null);
        while(true) {
            unsafe.allocateMemory(_1MB);
        }
    }
} 

运行后可以得到下面这样的结果:
由直接内存导致的内存溢出,一个很明显的特征是在 Dump 文件中不会看到上面明显的异常情况。因此当 Dump 文件很小,程序中又使用了 DirectMemory 时,可以重点检查这方面。

第二章的内容到这里就结束啦!第三章将是关于垃圾回收和内存分配,下次见!

相关推荐
liu_chunhai10 分钟前
设计模式(3)builder
java·开发语言·设计模式
ya888g1 小时前
GESP C++四级样题卷
java·c++·算法
【D'accumulation】1 小时前
令牌主动失效机制范例(利用redis)注释分析
java·spring boot·redis·后端
小叶学C++1 小时前
【C++】类与对象(下)
java·开发语言·c++
2401_854391081 小时前
高效开发:SpringBoot网上租赁系统实现细节
java·spring boot·后端
Cikiss1 小时前
微服务实战——SpringCache 整合 Redis
java·redis·后端·微服务
wxin_VXbishe1 小时前
springboot合肥师范学院实习实训管理系统-计算机毕业设计源码31290
java·spring boot·python·spring·servlet·django·php
Cikiss1 小时前
微服务实战——平台属性
java·数据库·后端·微服务
无敌の星仔1 小时前
一个月学会Java 第2天 认识类与对象
java·开发语言
OEC小胖胖1 小时前
Spring Boot + MyBatis 项目中常用注解详解(万字长篇解读)
java·spring boot·后端·spring·mybatis·web