OutOfMemoryError异常相信对很多程序员来说一定很头疼,平时不常遇到,但是遇到时却无从下手,其实不论什么样的问题,都有决绝的规律和方法。
OutOfMemoryError异常主要包括:
1、Java堆溢出
2、Java栈溢出
3、方法区和运行时常量池溢出
4、本机直接内存溢出
Java堆溢出
我们都知道,JVM中三大区:堆区、栈区、方法区,其中堆区发生溢出的可能性尤其大,发生溢出大多是代码编写问题,我们现在就模拟一个Java堆溢出的场景。
代码如下:
import java.util.List;
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while(true) {
list.add(new OOMObject());
}
}
}
运行代码之前,需要先修改JVM的参数配置,如下:
-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
-Xms20m -Xmx20m 将堆最大最小内存都设置为20m
-XX:+HeapDumpOnOutOfMemoryError 是在虚拟机发生异常时,dump出当前内存信息,用于分析问题。
修改好JVM运行参数后,执行代码。
报错如下:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid17932.hprof ...
Heap dump file created [28214773 bytes in 0.053 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:265)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
at java.util.ArrayList.add(ArrayList.java:462)
at com.itzhimei.study.jvm.HeapOOM.main(HeapOOM.java:16)
原因很明显,就是堆内存满了,后续新建的对象无法申请到内存空间,所以报了OutOfMemoryError的异常。
如果是平时,遇到这种情况我们可以用工具来分析异常发生时的dump文件,这里使用Memory Analyzer Tools来分析。

从图中可以看出,从根开始,往下有一个持有了大量对象的Objec,里面都是创建的OOMObject类的对象,就是我们代码里定义list所持有的。
这里还需要说明的两个点,就是图中列表里的Shallow Heap和Retained Heap
- Shallow Heap 表示当前对象占用的内存大小
- Retained Heap 表示当前对象及其能直接或间接访问到的对象,也就是发生垃圾回收时,能够释放的空间大小
Java栈溢出
栈溢出分调用超出最大深度溢出和内存不足溢出,分别对应:StackOverflowError和OutOfMemoryError。但这两种异常其实反应的是一个问题,那就是stack内存不足。
栈溢出的问题,有一定编程经验的开发者可能马上想到对应的场景,那就是无限制的递归,无限制递归也就是不停调用同一函数,压入栈中,最终导致栈内存不足,从而抛出StackOverflowError错误。
我们来模拟一下StackOverflowError
代码如下:
/**
* www.itzhimei.com
* 编程技术之美-IT之美
*/
public class StackSOF {
private int len = 0;
public void stackAdd() {
len++;
//调用自身,实现递归效果,只是这个递归没有退出条件,那么肯定会发生栈溢出
stackAdd();
}
public static void main(String[] args) {
StackSOF sof = new StackSOF();
sof.stackAdd();
}
}
JVM参数:
-Xss64k
Exception in thread "main" java.lang.StackOverflowError
at com.itzhimei.study.jvm.StackSOF.stackAdd(StackSOF.java:10)
at com.itzhimei.study.jvm.StackSOF.stackAdd(StackSOF.java:12)
at com.itzhimei.study.jvm.StackSOF.stackAdd(StackSOF.java:12)
at com.itzhimei.study.jvm.StackSOF.stackAdd(StackSOF.java:12)
at com.itzhimei.study.jvm.StackSOF.stackAdd(StackSOF.java:12)
at com.itzhimei.study.jvm.StackSOF.stackAdd(StackSOF.java:12)
at com.itzhimei.study.jvm.StackSOF.stackAdd(StackSOF.java:12)
at com.itzhimei.study.jvm.StackSOF.stackAdd(StackSOF.java:12)
at com.itzhimei.study.jvm.StackSOF.stackAdd(StackSOF.java:12)
at com.itzhimei.study.jvm.StackSOF.stackAdd(StackSOF.java:12)
......
......
at com.itzhimei.study.jvm.StackSOF.main(StackSOF.java:17)
方法区和运行时常量池溢出
要模拟出方法区和运行时常量池的溢出,这里需要区分一下JDK的版本。
JDK1.6及之前的版本,常量池是方法区中的一部分,在JDK1.7开始,常量池被放到的heap中。
代码:
import java.util.ArrayList;
import java.util.List;
/**
* www.itzhimei.com
* 编程技术之美-IT之美
*/
public class ConstantPoolOOM {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
int i=0;
while(true) {
String s = "a"+i++;
list.add(s.intern());
}
}
}
JVM参数:
-Xms10M -Xmx10M -XX:PermSize=5M -XX:MaxPermSize=5M
报错如下:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.itzhimei.study.jvm.ConstantPoolOOM.main(ConstantPoolOOM.java:12)
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=5M; support was removed in 8.0
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=5M; support was removed in 8.0
重点在这里:java.lang.OutOfMemoryError: Java heap space
报错是提示heap内存不足,因为我本地是JDK1.8
如果是JDK1.6或以下,则会提示java.lang.OutOfMemoryError:PermGen space,那么就表示方法区溢出了,有兴趣的可以用1.6试一试。
在JDK1.7及以后的版本中要让PermGen space内存溢出,那么需要使用类似spring的CGLIB这样的功能,在运行时动态生成大量class,编译成字节码,加载到JVM的方法区中,因为class字节码都是存放在方法区的,当动态生成的class过多,那么方法区自然就溢出了。
本机直接内存溢出
本机直接内存–Direct Memory,不是JVM内存中的一部分,但是这部分内存也被频繁的使用,并且可能会产生OutOfMemoryError。
从JDK1.4中新加入了NIO类,引入了基于channel和Buffer的I/O方式,它可以使用Native函数库直接分配堆外内存,也就是在我们系统内存上分配了使用空间,然后使用了Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。为的是提高读写性能。
直接内存可以通过:-XX:MaxDirectMemorySize 来设置大小,如果不设置,默认和堆在最大值-Xmx一样大。
设置本机直接内存的原则就是,各种内存大小+本机直接内存大小<机器物理内存。
代码:
/**
* www.itzhimei.com
* 编程技术之美-IT之美
*/
public class DirectMemoryOOM {
private static final int SIZE_2MB = 1024*1024*2;
public static void main(String[] args) throws Exception {
Field uf = Unsafe.class.getDeclaredFields()[0];
uf.setAccessible(true);
Unsafe unsafe = (Unsafe)uf.get(null);
while(true) {
unsafe.allocateMemory(SIZE_2MB);
}
}
}
JVM参数:
-XX:MaxDirectMemorySize=10M
报错如下:
Exception in thread "main" java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at com.itzhimei.study.jvm.DirectMemoryOOM.main(DirectMemoryOOM.java:19)
上面的代码可能很多人看不太懂,这一对unsafe是什么意思?怎么就产生直接内存溢出了?
Field uf = Unsafe.class.getDeclaredFields()[0];
uf.setAccessible(true);
Unsafe unsafe = (Unsafe)uf.get(null);
while(true) {
unsafe.allocateMemory(SIZE_2MB);
}
这就是我们大部分程序员并不会用到的Unsafe类。
因为要操作的是JVM之外的内存–直接内存,规范的操作是使用Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,但是在NIO出现之前,操作直接内存,用的就是Unsafe类。
Unsafe类是什么?
Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。过度的使用Unsafe类会使得出错的几率变大,因此Java官方并不建议使用的,官方文档也几乎没有。
知道了Unsafe的能力,所以上面的代码就实现直接内存的使用。也就最终导致直接内存溢出。
以上内容学习自周志明老师的《深入理解JVM虚拟机》