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 参数:
指定 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。
面对此种异常,常规解决思路:
场景二
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 参数:
指定 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 异常。
面对此种异常,常规解决思路:
场景三
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(元空间的溢出),为什么会出现这个异常?元空间大小的要求取决于加载的类的数量以及这种类声明的大小,所以主要原因很可能是太多的类或太大的类加载到元空间导致的。
解决方案:
场景五
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)
解决方案:
场景六
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)
解决方案:
场景七
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
寄语
一个人想步行穿过大陆,但道路布满了荆棘,这时候他有两种选择:铺一条路,征服大自然,或者准备一双草鞋。
好了,本次就谈到这里,一起聊技术、谈业务、喷架构,少走弯路,不踩大坑。会持续输出原创精彩分享,敬请期待!