Java虚拟机OOM

内存溢出异常 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,099评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,473评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,229评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,570评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,427评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,335评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,737评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,392评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,693评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,730评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,512评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,349评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,750评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,017评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,290评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,706评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,904评论 2 335