Java虚拟机OOM

作者: xuweizhen | 来源:发表于2019-12-17 18:43 被阅读0次

    内存溢出异常 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. 虚拟机栈引用的对象
      1. 方法区中类静态属性引用的对象
      1. 方法区中常量引用的对象
      1. 本地方法栈中JNI引用的对象
    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

    相关文章

      网友评论

        本文标题:Java虚拟机OOM

        本文链接:https://www.haomeiwen.com/subject/ggrenctx.html