JVM · Java虚拟机原理 · JVM上语言·框架· 生态系统

Java虚拟机OOM

2019-12-17  本文已影响0人  xuweizhen

内存溢出异常 OOM

我们知道:

  1. JVM的内存模型
  2. 对象的创建和布局

开始面对最终Boss: OOM

我们的目标:

  1. 使用代码验证Java内存模型
  2. 在实际发生OOM时,通过异常信息,瞬间判断:
      1. 那个区域OOM
      1. 定位代码
      1. 异常处理

堆OOM

什么情况下会发生堆OOM

  1. 不断的在堆中创建对象
  2. 垃圾回收机制无法回收对象

不断创建对象通过循环就可以了,但什么情况下垃圾回收机制无法回收对象呢

  1. GC通过GC Roots到对象之间的可达路径来回收对象。
    可作为GC Roots的对象有:
  1. 这里使用第一种方式:虚拟机栈引用,即变量,存放循环创建的对象。
    具体实现:使用List集合,循环添加测试对象。

集合中大量数据很常见呀,也没见到堆OOM

是的,所以需要设置下虚拟机的内存大小,和不可扩展。
JVM 参数:
-Xmx20m : 表示设置虚拟机最大内存20m
-Xms20m : 表示设置虚拟机最小内存20m, 最大内存=最小内存,表示虚拟机不可扩展。

我用的是STS, 这个在虚拟机参数在哪设置

  1. Run Configuration/Debug Configuration 中有VM参数这一项
  2. 设置Java -> Installed JREs选中使用的jdk/jre -> edit按钮 -> 输入VM参数

那报错OOM如何分析呢

一般日志只记录报错堆栈,无法确定某个类占用百分比或GC可达性分析等等。
分析OOM, 需要堆转储快照文件。即发生OOM之前的快照将堆栈中信息以文件信息保存下来

堆转储文件怎么设置?

设置JVM参数即可:-XX:+HeapDumpOnOutOfMemoryError
表示创建堆快照文件,在OOM异常发生时。

上代码

代码:

public class HeapOOM {

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

    }
}

public class OOMObject {

}

报错异常:

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid7740.hprof ...
Heap dump file created [27970781 bytes in 0.088 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Arrays.java:3210)
    at java.util.Arrays.copyOf(Arrays.java:3181)
    at java.util.ArrayList.grow(ArrayList.java:261)
    at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
    at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
    at java.util.ArrayList.add(ArrayList.java:458)
    at jvm.com.oom.heap.HeapOOM.main(HeapOOM.java:12)

使用Memory Analysis 工具分析

可以看到主线程占用15.5M(97%)的空间
而class OOMObject一共有3,241,320个没有释放,占用了97%空间
所以问题就是这个对象无法释放,导致 OOM: Java heap space

虚拟机栈OOM

什么情况下会发生虚拟机栈OOM

虚拟机栈会有两种情况:

  1. 栈空间不可扩展,当前虚拟机栈深度 > 虚拟机规定的栈深度, 会抛出栈溢出错误
  2. 栈空间可扩展,扩展时无法申请到足够内存,会抛出内存溢出异常

测试1:

  1. 本地测试设置栈最大内存参数:-XSs10m
  2. 单线程使用死递归测试,并打印当前栈深度。

新问题:
但栈深度在一定范围内变化,是否表示每次虚拟机规定的栈深度不同?

测试2:

修改栈最大的内存参数,数值缩小一半:-XSs5m

新问题:
既然是通过最大栈空间计算的,如果扩大每个栈帧大小,栈空间在扩展时,可能无法申请到足够内存而抛出内存溢出异常

测试3:

在递归方法添加多个局部变量,扩大栈帧。

新问题:
编译时怎么计算栈深度呢

我们知道:栈帧中的局部变量表在编译时就知道大小,运行时可以直接分配内存

所以编译期就知道栈帧大小,通过最大栈帧,和栈空间最大值,可以知道栈深度最大多少。

新问题:
如何模拟栈空间内存溢出?

这个栈深度是单线程情况下计算出来的,如果多线程情况下,线程越多,占用的栈空间就越多,越可能发生栈空间内存溢出异常。

但是测试案例无法模拟,因为创建很多进程在window环境下直接导致操作系统假死,Java的线程是映射到操作系统的内核线程上。

理论上: 多线程中为每个线程分配越大的内存空间,越容易出现内存溢出

原因:

  1. 操作系统分配给每个进程的内存是有限制的,如32位windows是2G
  2. 虚拟机会设置Java堆内存和方法区内存最大值,即还剩下:2G - 最大堆内存 - 最大方法区内存
  3. 剩下内存由虚拟机栈和本地方法栈瓜分,每个线程分配到的栈容量越大,可建立的线程数量越少。
    建立新的线程时,就容易发生内存溢出异常。

以上结论待测试验证!

方法区内存溢出异常

方法区什么时候出现内存溢出异常

方法区在不同jdk版本中实现不同

  1. jdk1.7之前,使用永生代实现
  2. jdk1.8之后,使用元空间实现

由于我现在使用的是jdk1.8, 无法模拟出永生代的内存溢出,但原理基本一致。

测试步骤:

  1. 设置虚拟机参数,方法区空间最大值,且无法扩展
    永生代虚拟机参数:-XX:PermSize=10M -XX:MaxPermSize=10M
    元空间虚拟机参数:-XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M

  2. 循环创建大量不同的类,直到内存溢出。
    使用CGlib字节码动态代理方式,可以在运行时动态创建不同的类。
    CGlib字节码动态代理在框架中经常遇到,如Spring框架的AOP就是使用CGlib字节码动态代理实现的。

测试代码:

public class PermOOM {
    public static void main(String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(OOMObject.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                
                @Override
                public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                    
                    return proxy.invokeSuper(obj, args);
                }
            });
        }
    }
    
    static class OOMObject {
    }
}

测试结果:

Error occurred during initialization of VM
OutOfMemoryError: Metaspace

测试结论:

可以看到,当元空间内存不够时,大量的类就会造成元空间的内存溢出

所以在Spring等框架运用大量的CGlib字节码动态代理技术时,需要保证有大容量的元空间。

关于永久代有个字符串常量池的问题

String 有个intern()方法。
在jdk1.6中,会把首次遇到的字符串实例复制到永久代中,返回的也是这个永久代中这个字符串的实例.
在jdk1.7之后,不会再复制字符串实例,只是在字符串常量池中记录首次出现的实例引用。

所以会有下面代码中情况:

public class ContantsOOM {
    public static void main(String[] args) {
        // 指向字符串常量池中字符串
        String str1 = "xuweizhen";
        // str1在字符串常量池中已存在,str1.intern返回字符串常量值中首次出现的实例引用,一致
        System.out.println("1 :" + (str1.intern() == str1));
        // 指向堆中字符串对象
        String str2 = new StringBuilder("xuwei").append("zhen").toString();
        // str2.intern()在字符串常量池中已存在,不是首次出现,所以返回的是str1的字符串常量池常量,与str2不一致。
        System.out.println("2 :" + (str2.intern() == str2)); 
        // 指向堆中字符串对象
        String str22 = new StringBuilder("aaa").append("bbb").toString();
        /**
         *  str22指向堆中字符串对象引用
         *  str22.intern方法判断str22在字符串常量池中是否存在,str22不在字符串常量池中
         *  将str22放入字符串常量池中,并返回该字符串常量池引用,所以一致。
         */
        System.out.println("3 :" + (str22.intern() == str22));      
        // 指向堆中字符串对象
        String str222 = new StringBuilder("a").append("aabbb").toString();
        System.out.println("4 :" + (str222.intern() == str222));    
        // 指向堆中字符串对象
        String str3 = new String("cccddd");
        // str的new String()方法返回的是一个字符串副本,和原字符串引用并不一致
        System.out.println(str3.intern() == str3); 
    }
}

直接内存的内存溢出异常

直接内存在什么情况下出现内存溢出异常

直接内存容量通过参数:-XX:MaxDirectMemorySize指定

可以通过Unsafe的allocateMemory方法分配直接内存,但Unsafe类只有引导类加载器才会返回实例。这里无法实现。

直接内存测试待补充!!!

想共同学习jvm的可以加我微信:1832162841,或者进QQ群:982523529

上一篇下一篇

猜你喜欢

热点阅读