Java虚拟机内存管理(三)—内存异常

Java 与 C++ 之间有一堵由内存动态分配和垃圾收集技术所围成的 “高墙”,墙外面的人想进去,墙里面的人却想出来。——《深入理解Java虚拟机:JVM高级特性与最佳时实践(第二版)》周志明

Java 虚拟机作为运行 Java 程序抽象出来的计算机,具有内存管理的能力,像内存分配、垃圾回收等这些相关的内存管理问题,Java 虚拟机都会帮我们解决,所以作为一个 Java 程序员要比 C++ 程序员幸福,但是内存方面一旦出现问题,如果对虚拟机怎样使用内存不了解,就很难排查错误。

这段时间看周志明先生的《深入理解Java虚拟机:JVM高级特性与最佳时实践(第二版)》,下面就对 Java 虚拟机对内存的管理做一个系统的整理,本篇文章是该专题的第三篇。

3、内存异常

虽然说有 Java 虚拟机帮助我们管理内存,但是在管理过程中仍然有内存异常的发生。除了前面内存划分中说到的程序计数器外,其他区域都有发生 OutOfMemoryError 异常的可能。

我们可以给 Java 虚拟机设置参数来模拟这些异常的发生,不同的 Java 虚拟机运行结果可能也不同,这里使用的是 Oracle 公司的 JDK。

特别说明:下面如果没有特殊说明,默认使用的是 JDK8。

3.1 Java 堆内存异常

Java 堆是用于存储对象实例的,所以只要不断的创建对象把 Java 堆区域填满,并且还要保证牢记垃圾回收机制不能清除这些对象,就可以模拟出 Java 堆内存的异常。

模拟程序代码如下:

import java.util.ArrayList;
import java.util.List;

// 模拟 Java 堆内存异常
public class HeapOOM {
    // 声明类内部静态类,生命周期和外部类 HeapOOM 一样长,使垃圾收集器无法回收这些对象占用的内存空间
    static class OOMObject{
        
    }
    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<>();
        // 死循环不断生成对象,并添加到 list 中, 直到占满 Java堆内存
        while(true) {
            list.add(new OOMObject());
        }
    }
}

这里使用 MAT 内存分析器插件来对内存异常进行分析,IDE 使用免费的 Eclipse,当然 IDEA 也可以安装,Eclipse种的安装教程可以参看这篇文章《mat之一--eclipse安装Memory Analyzer》

在 Debug 的配置页面,设置 JVM 的参数。

Debug设置.jpg

JVM Debug 参数:

-verbose:gc -Xms20M -Xmx20M

-XX:+PrintGCDetails

-XX:+HeapDumpOnOutOfMemoryError

-XX:HeapDumpPath=D:\CodeWorkspace\Java\Dump

-Xms、-Xmx、-Xmn 后面分别是 Java 堆的最小值、Java 堆的最大值都是 20M,-XX后面可以添加一些额外的设置,PrintGCDetails 是打印出垃圾收集的详细信息,HeapDumpOnOutOfMemoryError 是发生OutOfMemoryError 异常时记录内存快照,HeapDumpPath后面是存放内存快照的文件夹位置。

Debug 结果如下:

Java堆异常运行结果.jpg

从上图中可以看到 Java堆区域(Java heap space)出现了 OutOfMemoryError 的异常,并且在我们指定的文件夹生成了内存快照文件。在使用 MAT 内存分析器工具之前,我们还要知道内存泄露和内存溢出的区别,我在前面没有将 OutOfMemoryError 异常翻译成内存泄露异常或内存溢出异常,而是使用原本的英文,内存泄露和内存溢出只是导致出现异常的原因,该事件的结果才是产生 OutOfMemoryError 异常。

内存泄露和内存溢出的区别:

  • 内存泄露是指程序在申请内存后,无法释放已申请的内存空间,内存泄露会导致内存资源耗光,通俗的说就是对象占着内存空间不归还给系统。
  • 内存溢出是指程序申请内存使用时,发现内存空间并不够使用,很常见的例子就是在存一个大数时超过了该数据类型的最大值,通俗的是说就是程序在借内存空间时发现无法满足自己的要求。

知道了内存泄露和内存溢出的区别,我们再来用 MAT 工具分析内存快照,首先调出 MAT 视图,然后在 “File” 选项中选择 “Open Heap Dump” 打开内存快照文件。

调出MAT视图.jpg

打开内存快照文件.jpg

打开后快照文件后可以清晰的看出内存异常的可能出现问题的地方(Problem Suspect)。

内存快照.jpg

点击 “Details” 可以查看具体的细节。

具体细节.jpg

可以看到 OOMObject 占用的内存空间很大,可以查看该对象是否有到 GC roots 的引用链,导致垃圾收集器无法回收对象占用的内存空间,由于是内存空间被占用无法回收,所以 OutOfMemoryError 异常产生的原因是内存泄露。

查看泄露对象到GCRoots的引用链.gif

3.2 栈内存异常

在 HotSpot 虚拟机中并不区分 Java 虚拟机栈和本地方法栈,栈的容量可以通过 -Xss 参数来设定。

在 Java 虚拟机规范中描述了两种栈会出现的异常:

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

栈的深度是由栈的内存空间决定的,请求的栈越深,也即是已使用的栈的空间越大,所以上面 Java 虚拟机规范中的两种异常是有重叠之处的,一种异常也可能会导致另外一种异常的发生,到底是栈的内存空间太小引起的内存异常还是已使用的栈的内存空间太大引起的内存异常?

减少栈内存的容量和定义大量的局部变量来增加栈帧中局部变量表的长度,理论上都是可以产生 StackOverflowError 异常,也可以产生 OutOfMemoryError 异常的。

但是下面的代码只能产生 StackOverflowError 异常。

// 栈 StackOverflowError 异常
public class JVMStackSOF {
    private int stackLength = 1;
    // 递归函数
    public void stackLeak() {
        stackLength++;
        stackLeak();
    }
    public static void main(String[] args) {
        JVMStackSOF stackSOF = new JVMStackSOF();
        try {
            stackSOF.stackLeak();
        } catch (Throwable e) {
            System.out.println("Stack Length:" + stackSOF.stackLength);
            throw e;
        }
    }
}

Debug 的参数为:-verbose:gc -Xss128k -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\CodeWorkspace\Java\Dump

Debug 结果如下,只产生了 StackOverflowError 异常。

栈异常结果1.jpg

而在多线程环境中测试,可以才模拟出 OutOfMemoryError 异常。

特别提醒:此代码运行时会导致系统假死,具有一定的风险性,请在运行前保存好其他文件。

代码如下:

// 栈 OutOfMemoryError 异常
public class JVMStackOOM {
    private void dontStop() {
        while(true) {
            
        }
    }
    // !危险代码请勿随便尝试
    public void stackLeakByTread() {
        // 死循环不断创建线程
        while(true) {
            Thread thread = new Thread(new Runnable() {
                
                @Override
                public void run() {
                    dontStop();
                }
            });
            thread.start();
        }
    }
    
    public static void main(String[] args) {
        JVMStackOOM stackOOM = new JVMStackOOM();
        stackOOM.stackLeakByTread();
    }
}

由于在做这项危险的测试时,系统死掉了,所以笔者并没有得出实际结果,根据《深入理解Java虚拟机:JVM高级特性与最佳时实践(第二版)》,这里给出理论结果,也可以在虚拟机系统中尝试运行此代码,但也可能会出现外部系统假死的情况,读者可以自己尝试。

栈异常结果2.jpg

3.3 方法区内存异常

方法区中有运行时常量池,如果向常量池中添加大量的内容,也可以导致方法区内存异常,可以通过 -XX:Permsize 和 -XX:MaxPermSize 来限制方法区的大小,进而限制常量池的容量。常量池在编译期可以放入常量了,在运行时也可以再添加新的常量,不存在内存被占用无法回收,所以这里的异常不是内存泄露导致的,而是内存溢出。

代码如下:

import java.util.ArrayList;
import java.util.List;

// 模拟方法区中的常量池内存溢出
public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        int i = 0;
        while(true) {
            list.add(String.valueOf(i++).intern());
        }
    }
}

经过实际测试,发现 JDK6 会出现下面内存异常的情况,而在 JDK7 和 JDK8 中,发现垃圾回收器会不断的回收常量池的旧常量所占用的内存,以便新的常量可以进入,从而避免了常量池内存异常的发生。

方法区常量池内存异常.jpg

方法区用于存放类的相关信息,如类名,访问修饰符,常量池,字段描述,方法描述等。使方法区内存异常的大致思路是产生大量的类填满方法区,直到方法区内存溢出。由于实验操作起来比较麻烦,直接操作字节码文件来动态的生成大量的类,所以这里也是使用书中的运行结果。

方法区内存异常.jpg

3.4 直接内存异常

直接内存的大小可以通过 -XX:MaxDirectMemorySize 来指定,如果不指定默认是和 Java 堆的最大值(-Xmx)一样,可以通过使用 Unsafe 类来申请内存,由于该类的使用有限制,只有引导类的加载器才会返回对象实例,所以只能通过反射来获取 Unsafe 类的实例,但是在 Eclipse 中导入该类的包会报错,解决方案见参考文章。

参考文章:

eclipse中解决import sun.misc.Unsafe报错的方法

代码如下:

import java.lang.reflect.Field;
import sun.misc.Unsafe;

// 模拟直接内存异常
public class DirectMemoryOOM {
    private static final int _1MB = 1024 * 1024;
    public static void main(String[] args) throws IllegalArgumentException, IllegalAccessException {
        Field unsafeField = Unsafe.class.getDeclaredFields()[0];
        unsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe)unsafeField.get(null);
        while(true) {
            unsafe.allocateMemory(_1MB); // 申请内存
        }
    }
}

Debug 参数:-verbose:gc -Xmx20M -XX:MaxDirectMemorySize=10M -XX:+PrintGCDetails

由于在 Eclipse 中使用 JDK6 和 JDK7 运行该程序时会直接闪退,无法得到输出的异常,所以直接在控制台中使用 JDK8 编译运行该程序,运行结果如下:

直接内存异常.jpg

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏CSDN技术头条

作为 Java 开发者,你需要了解的堆外内存知识

本文来自作者 应书澜 在 GitChat 上分享 「深入解读 Java 堆外内存(直接内存)」

2243
来自专栏向治洪

JVM体系结构认知

虚拟机 何为虚拟机呢?虚拟机是模拟执行某种指令集体系结构(ISA)的软件,是对操作系统和硬件的一种抽象。其软件模型如下图所示: ? 计算机系统的这种抽象类似于...

2239
来自专栏Java架构沉思录

一文理清Java内存区域

Java虚拟机在执行 Java 程序的过程中会把它管理的内存划分为若干个不同的数据区域。根据《Java 虚拟机规范》将 Java虚拟机所管理的内存分为以下几个运...

1172
来自专栏Android中高级开发

Android并发编程 多线程与锁

该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,如果能给各位看官带来一丝启发或者帮助,那真是极好的。

2662
来自专栏编程坑太多

JVM内存管理与垃圾回收机

2346
来自专栏Java后端技术栈

Java 虚拟机内存区域划分详解(1)

JVM,java virtual machine, 即Java虚拟机,是运行java class文件的程序。

1074
来自专栏精讲JAVA

Java 虚拟机 2 : Java 内存区域及对象

为以后写文章考虑,也为巩固自己的知识和一些基本概念,这里要理清楚几个计算机中的概念。

1242
来自专栏desperate633

深入理解Java中的内存溢出内存溢出内存溢出的几种情况(OOM 异常)导致内存溢出的原因内存溢出的解决方法

OutOfMemoryError 异常: 除了程 序计数器外 , 虚拟机内 存的其他几 个运行时区 域都有发生OutOfMemoryError(OOM)异常的...

1071
来自专栏向治洪

原子性、可见性以及有序性

虚拟机 何为虚拟机呢?虚拟机是模拟执行某种指令集体系结构(ISA)的软件,是对操作系统和硬件的一种抽象。其软件模型如下图所示: ? 计算机系统的这种抽象类似于...

3247
来自专栏Linyb极客之路

java虚拟机知识点简要梳理

首先来看一个java虚拟机的思维导图,下面每个知识点都可以进行展开,本篇只做简要梳理

1853

扫码关注云+社区

领取腾讯云代金券