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 */ 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 */ 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 */ 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
/** * 栈溢出模拟 */ 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
/** * 无法创建本地线程模拟 */ 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 */ 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 对应用程序可以分配的最大数组大小有限制,不同平台限制有所不同。
解决方案:检查代码是否有必要创建这么大号的数组,是否可以采用集合、拆分等其它方式处理。