专栏首页一猿小讲程序员进阶系列:OOM 都搞不定,还敢妄称自己Java高级攻城狮?

程序员进阶系列:OOM 都搞不定,还敢妄称自己Java高级攻城狮?

1

这些异常你是否还记得?

正式开讲之前,先罗列一下所知的 OutOfMemoryError (简称 OOM)异常,看看这些异常工作中你是否也遇到过?

Java 堆内存溢出:java.lang.OutOfMemoryError: Java heap space

垃圾回收内存溢出:java.lang.OutOfMemoryError: GC overhead limit exceeded

方法区溢出:java.lang.OutOfMemoryError: PermGen space

Metaspace 内存溢出:java.lang.OutOfMemoryError: Metaspace

直接内存内存溢出:java.lang.OutOfMemoryError: Direct buffer memory

栈内存溢出:java.lang.StackOverflowError

创建本地线程内存溢出:java.lang.OutOfMemoryError: Unable to create new native thread

数组超限内存溢出:java.lang.OutOfMemoryError:Requested array size exceeds VM limit

在实际工作中,若真遇到了上面罗列的这些内存溢出的异常,你是否能够根据异常提示迅速定位是哪儿出了幺蛾子,并是否能够铲除这些幺蛾子呢?

希望通过此篇分享,尽量能够让大家了解每个异常发生的场景,并能够掌握每个异常场景的应对之策。

如上图示意,按照内存共享来划分 JVM 内存,主要划分为线程共享内存区域(堆、方法区)、线程私有内存区域(程序计数器、虚拟机栈、本地方法栈)、直接内存。而在《Java 虚拟机规范》的规定里,除了程序计数器外,虚拟机内存的其它几个运行时区域都可能发生 OOM 异常,接下来通过代码来剖析一下各种 OutOfMemoryError(OOM)的场景。

2

实战:OutOfMemoryError 异常

场景一

java.lang.OutOfMemoryError: Java heap space

/**
 * VM options:-Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError
 * @author 一猿小讲
 */
public class HeapOOM {
    public static void main(String[] args) {
        byte[] bytes = new byte[20 * 1024 * 1024];
        System.out.println(bytes);
    }
}

理论且不谈,直接抛代码,代码很简单,创建一个字节数组对象,要分配 20M 的空间。

若在运行程序时指定 VM 参数:

  • 通过参数 -Xms10m -Xmx10m 将堆的最小值与最大值都设置为 10M,即限制 Java 堆的大小为 10MB,并且避免堆自动扩展;
  • 通过参数 -XX:+HeapDumpOnOutOf-MemoryError 让虚拟机在出现内存溢出异常的时候 Dump 出当前的内存堆转储快照以便进行事后分析。

指定 VM options 后的运行结果:

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid35115.hprof ...
Heap dump file created [1033561 bytes in 0.005 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
  at HeapOOM.main(HeapOOM.java:7)

为什么呢?简单解释原因,-Xms10m -Xmx10m 限制了堆的最大值为 10M,而 new byte[20 * 1024 * 1024] 需要 20M 的空间,则堆内存明显不够,则直接导致 OOM。

面对此种异常,常规解决思路:

  • 要检查一下代码是否存在优化的空间;
  • 依据内存溢出时的快照文件 xx.hprof 来判断是否存在内存泄露,不需要的对象有没有被回收掉;
  • 调节虚拟机的堆参数(-Xms -Xmx),适当调大堆内存。

场景二

java.lang.OutOfMemoryError: GC overhead limit exceeded

/**
 * VM options:-Xmx6m -XX:+HeapDumpOnOutOfMemoryError
 * @author 一猿小讲
 */
public class HeapOOM {
    static class GirlFriend {
    }

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

理论且不谈,直接抛代码,代码很简单,一直往集合中加入新创建的对象(虚妄的单身狗生活:一直创建女朋友对象。)

若在运行程序时指定 VM 参数:

  • 通过参数 -Xmx6m 将堆的最大值设置为 6M;
  • 通过参数 -XX:+HeapDumpOnOutOf-MemoryError 让虚拟机在出现内存溢出异常的时候 Dump 出当前的内存堆转储快照以便进行事后分析。

指定 VM options 后的运行结果:

java.lang.OutOfMemoryError: GC overhead limit exceeded
Dumping heap to java_pid35304.hprof ...
Heap dump file created [12557270 bytes in 0.082 secs]
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
  at HeapOOM.main(HeapOOM.java:16)

为什么呢?来段洋文,尝试解读一下。

The parallel(concurrent) collector will throw an OutOfMemoryError if too much time is being spent in garbage collection: if more than 98% of the total time is spent in garbage collection and less than 2% of the heap is recovered, an OutOfMemoryError will be thrown.

大概意思应用程序在垃圾收集上花费了太多时间,但是却没有什么卵用,默认超过 98% 的时间用来做GC却回收了不到2%的内存时将会抛出 OutOfMemoryError 异常。

面对此种异常,常规解决思路:

  • 程序启动添加 JVM 参数 -XX:-UseGCOverheadLimit 推迟异常报出,而并非彻底解决了问题;
  • 好好分析快照文件 xx.hprof,排除代码问题,若确实堆内存不足,通过参数 -Xmx 适度调整堆内存大小。

场景三

java.lang.OutOfMemoryError: PermGen space

首先来解释一下 PermGen space 的用处,主要用来存储每个类的信息,例如:类加载器引用、运行时常量池(所有常量、字段引用、方法引用、属性)、字段(Field)数据、方法(Method)数据、方法代码、方法字节码等等。

当出现 java.lang.OutOfMemoryError: PermGen space 异常时,要能够知道可能是由于太多的类或者太大的类被加载到方法区导致的。

解决方案:可以根据具体情况采用 -XX:MaxPermSize=64m 参数来加大分配的内存进行解决。

场景四

java.lang.OutOfMemoryError: Metaspace

在 JDK6、7 还能够见到java.lang.OutOfMemoryError: PermGen space异常的踪影,而在 JDK8 以后,永久代便完全退出了历史舞台,元空间作为其替代者登场,在默认参数设置下,已经很难再迫使虚拟机产生上面所描述的异常了。不过 java.lang.OutOfMemoryError: Metaspace 异常偶尔就会碰到了。

java.lang.OutOfMemoryError: Metaspace(元空间的溢出),为什么会出现这个异常?元空间大小的要求取决于加载的类的数量以及这种类声明的大小,所以主要原因很可能是太多的类或太大的类加载到元空间导致的。

解决方案:

  • 优化参数配置,适度调大该值 -XX:MaxMetaspaceSize;
  • 着重关注代码生成以及依赖的三方包。

场景五

java.lang.OutOfMemoryError: Direct buffer memory

/**
 * VM Args:-XX:MaxDirectMemorySize=4m
 * @author 一猿小讲
 */
public class DirectMemoryOOM {
    private static final int _5MB = 5 * 1024 * 1024;

    public static void main(String[] args) throws Exception {
        //-XX:MaxDirectMemorySize=4m 本地内存配置的是4MB,这里实际使用的是5MB
        ByteBuffer.allocateDirect(_5MB);
    }
}

理论且不谈,直接看代码,代码很简单,分配一个 5M 的直接字节缓冲区。

若在运行程序时指定直接内存的容量大小 -XX:MaxDirectMemorySize 为 4M,则程序运行会出现以下效果:

Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
  at java.nio.Bits.reserveMemory(Bits.java:694)
  at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
  at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
  at DirectMemoryOOM.main(DirectMemoryOOM.java:11)

解决方案:

  • 可以通过参数 -XX:MaxDirectMemorySize 适度调整直接内存的容量大小;
  • 考虑代码是否有优化空间。

场景六

java.lang.StackOverflowError

/**
 * 栈溢出模拟
 * @author 一猿小讲
 */
public class StackOOM {
    public static void main(String[] args) {
        love1024();
    }
    
    public static void love1024() {
        // 递归调用
        love1024();
    }
}

直接看代码,代码很简单,模拟了一下方法递归调用,程序运行效果如下:

Exception in thread "main" java.lang.StackOverflowError
  at StackOOM.love1024(StackOOM.java:12)
  at StackOOM.love1024(StackOOM.java:12)

解决方案:

  • StackOverflowError 属于比较好排查的一种错误,有错误栈可以阅读,大部分出现这种错误,都是程序出现了递归调用的问题;
  • 如果真需要递归调用的存在,可以适度调整参数 -Xss 的大小来解决。

场景七

java.lang.OutOfMemoryError: Unable to create new native thread

/**
 * 无法创建本地线程模拟
 * @author 一猿小讲
 */
public class ThreadUnableCreateOOM {
    public static void main(String[] args) {
        while(true) {
            new Thread(){
                @Override
                public void run() {
                    System.out.println("1024 节日快乐");
                    try {
                        Thread.sleep(10000);
                    } catch (InterruptedException e) {
                    }
                }
            }.start();
        }
    }
}

直接看代码,代码很简单,模拟了一下业务研发中若一直启动新的线程去执行任务而带来的效果,运行如下:

Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
  at java.lang.Thread.start0(Native Method)
  at java.lang.Thread.start(Thread.java:717)
  at ThreadUnableCreateOOM.main(ThreadUnableCreateOOM.java:18)

为什么呢?因为当 JVM 向操作系统请求创建一个新线程时,然而操作系统也无法创建新的 native 线程时就会抛出 Unable to create new native thread 错误。

解决方案:

  • 优化代码,考虑使用线程池及线程池的数量设置是否合适;
  • 检查操作系统本身的线程数是否可以适度调整。

场景八

java.lang.OutOfMemoryError:Requested array size exceeds VM limit

/**
 * OutOfMemoryError: Requested array size exceeds VM limit
 * @author 一猿小讲
 */
public class ArrayLimitOOM {
    public static void main(String[] args) {
        int[] ary = new int[Integer.MAX_VALUE];
    }
}

直接看代码,代码很简单,创建一个大小为 Integer.MAX_VALUE 的 int 数组,代码看起来没毛病,程序运行起来很诧异:

Exception in thread "main" java.lang.OutOfMemoryError: Requested array size exceeds VM limit
  at ArrayLimitOOM.main(ArrayLimitOOM.java:3)

为什么?当你编写的 Java 程序试图要分配大于 Java 虚拟机可以支持的数组时就会报 OOM,Java 对应用程序可以分配的最大数组大小有限制,不同平台限制有所不同。

解决方案:检查代码是否有必要创建这么大号的数组,是否可以采用集合、拆分等其它方式处理。

3

寄语

一个人想步行穿过大陆,但道路布满了荆棘,这时候他有两种选择:铺一条路,征服大自然,或者准备一双草鞋。

好了,本次就谈到这里,一起聊技术、谈业务、喷架构,少走弯路,不踩大坑。会持续输出原创精彩分享,敬请期待!

本文分享自微信公众号 - 一猿小讲(yiyuanxiaojiangV5),作者:一猿小讲

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-10-24

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

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 外国程序猿是什么水平?

    这里所说的人肯定不是美国人,日本人和德国人,在上述国家招聘,根部不用操心人的问题,你能招聘到甩清华北大数条街的人才。我们目前海外扩张主要还是去第三世界国家,例如...

    netkiller old
  • WEB前端开发成长指南

    小 编注:相比起网页射击狮,操纵代码的前端攻城狮凭着双手在键盘巴拉巴拉敲出的字符,就能赋予二次元的静态页面生命,各种lovely 的~~fabulous的~~e...

    shirayner
  • 开发者技能修炼的五个等级

    第一阶梯:Typer,打字员 每一位开发者在正式踏上开发道路之前,都需要经过毫无编程经验的“第一阶段”。 这时他们对于程序的理解仅限于照着书本或记忆进行有规律的...

    ThoughtWorks
  • 天天写代码,觉得自己特别苦逼?嗯,还有20年AI就来解放你

    Root 编译整理 量子位 出品 | 公众号 QbitAI “年轻人呐,别想着写代码写到老啊喂。时代在变啦!” 美国能源部橡树岭国家实验室研究委员会苦口婆心地劝...

    量子位
  • 逻辑更严谨,你需要这样做

    现在是北京时间7点,这会儿已经把粥老师的语音+文字看了两遍,从中不断地在思考,为什么我之前的吹水文章会如此的水,原因之一就是在逻辑上。

    程序员小跃
  • Java大学问——优雅地处理异常

    你有没有这样的印象,当你想要更新一款 APP 的时候,它的更新日志里总有这么一两句描述:

    本人秃顶程序员
  • 还未成为优秀的安卓工程师,是差在时代还是败在机遇?看了这份进阶指南后,我茅塞顿开

    移动研发火热不停,越来越多人开始学习Android开发。但很多人感觉入门容易成长很难,对未来比较迷茫,不知道自己技能该怎么提升,到达下一阶段需要补充哪些内容。市...

    Android技术干货分享
  • 特斯拉车主要注意了:你的车子极易被盗!

    镁客网
  • Debug 是门艺术

    上期的分享《Coding 是门技术》主要通过引入身边 Code farmer 撸码的一些真实故事,掰扯了一下开发规范以及重构可以改变代码的设计的理念,并且文末我...

    一猿小讲
  • 2018年产品设计协作领域最强黑马居然是它?

    我发了一条朋友圈“感谢池子的秘密法宝,我今天终于吃上了女朋友做的晚饭了”并配上香香的绿豆汤,瞬间获得好几十条评论。

    奔跑的小鹿
  • 一位在魔都奋斗的7年老码农有感而发:职场转变要从思维改变开始

    因为作者本人的求职经历真的只能用“丰富+坎坷”来形容:二流本科院校的三流专业,首次南下深圳的经历让他坚定地走上了“技术”这条路。先后在华为外包公司、第三方支付公...

    养码场
  • Android 架构师研发技术进阶之路:不同阶段需要掌握的那些技术及软技能

    移动研发火热不停,越来越多人开始学习android开发。但很多人感觉入门容易成长很难,对未来比较迷茫,不知道自己技能该怎么提升,到达下一阶段需要补充哪些内容。市...

    Android技术干货分享
  • 【作业3.0】HansBug的第三次博客规格总结

    早在上世纪50年代,就已经有早期的编程语言出现,也开始有一些程序编写者出现(多为资深电子工程师,和半路出家的数学家)。

    HansBug
  • 程序员带你十天快速入门Python,玩转电脑软件开发(一)

    关注今日头条-做全栈攻城狮,学代码也要读书,爱全栈,更爱生活。提供程序员技术及生活指导干货。

    做全栈攻城狮
  • 装逼神器:现在游戏这么火,你也可以做到,带你制作一款小游戏4

    本教程致力于.Net程序员可以利用unity技术快速学习和入门游戏开发。一方面通过自己的总结希望可以帮助更多热衷与游戏开发或者编程技术开发的同仁。另一方面可以总...

    做全栈攻城狮
  • 程序员带你学习安卓开发-安卓基础之网络编程 大汇总

    本系列教程致力于可以快速的进行学习安卓开发,按照项目式的方法,通常一篇文章会做一个小程序。提高学习的兴趣。

    做全栈攻城狮
  • 程序员带你学习安卓开发-安卓基础之网络编程 大汇总

    本系列教程致力于可以快速的进行学习安卓开发,按照项目式的方法,通常一篇文章会做一个小程序。提高学习的兴趣。

    做全栈攻城狮
  • 零基础开发高大上精美网站,非技术人员建站宝典 第一课

    对于程序员,写代码是日常工作。作为他们眼中的电脑通,时常会有亲朋好友,找你给做个网站或者公众号。盛情难却。自己不得不搞。“子非鱼焉知鱼之乐”。按照要求去做,浪费...

    做全栈攻城狮
  • 注意!进阶Android高级开发这些坑不得不避免,相对的技巧要会用,量变到质量的过程

    程序员这个行业,日新月异,技术体系更新速度快,新技术新框架层出不穷,所有的技术都像是一个无底洞,当你学得越多就会发现不懂的越多,不懂的越多,需要学习的就更多。

    Android技术干货分享

扫码关注云+社区

领取腾讯云代金券