前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java虚拟机内存管理(三)—内存异常

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

作者头像
Wizey
发布2018-09-29 09:45:10
6900
发布2018-09-29 09:45:10
举报
文章被收录于专栏:编程心路

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 堆内存的异常。

模拟程序代码如下:

代码语言:javascript
复制
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 异常。

代码语言:javascript
复制
// 栈 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 异常。

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

代码如下:

代码语言:javascript
复制
// 栈 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 来限制方法区的大小,进而限制常量池的容量。常量池在编译期可以放入常量了,在运行时也可以再添加新的常量,不存在内存被占用无法回收,所以这里的异常不是内存泄露导致的,而是内存溢出。

代码如下:

代码语言:javascript
复制
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报错的方法

代码如下:

代码语言:javascript
复制
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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018.08.30 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 3、内存异常
    • 3.1 Java 堆内存异常
      • 3.2 栈内存异常
        • 3.3 方法区内存异常
          • 3.4 直接内存异常
          相关产品与服务
          对象存储
          对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档