内存溢出(OOM)通常出现在某一块内存空间耗尽的时候,导致内存溢出的原因有很多,常见的有堆溢出、直接内存溢出、永久区溢出等。
堆溢出
堆是Java程序中最为重要的内存空间,由于大量的对象都直接分配在堆上,因此它也成为最有可能发生溢出的区间。一般来说,绝大部分Java的内存溢出都属于这种情况。其原因是因为大量对象占据了堆空间,而这些对象都持有强引用,导致无法回收,当对象大小之和大于由Xmx参数指定的堆空间大小时,溢出错误就自然而然地发生了。
【示例】ArrayList对象持有byte数组的强引用,导致数据无法回收。 -Xms5m -Xmx20m
public class SimpleHeapOOM {
public static void main(String[] args) {
List list = new ArrayList();
for (int i = 0; i < 1024; i++) {
list.add(new byte[1024 * 1024]);
}
}
}
运行结果:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.example.demo.jvm.SimpleHeapOOM.main(SimpleHeapOOM.java:16)
为了缓解堆溢出错误,一方面可以使用-Xmx参数指定一个更大的堆空间,另一方面,由于堆空间不可能无限增长,通过下文提到的MAT或者Visual VM等工具,分析找到大量占用堆空间的对象,并在应用程序上做出合理的优化也是十分必要的。
直接内存溢出
在Java的NIO中,支持直接内存的使用,可以使用Java代码获得一块堆外内存空间,这块空间是直接向操作系统申请的。直接内存的申请速度比堆内存慢,但是其访问速度要快于堆内存。因此,对于那些可复用的,并且会被经常访问的空间,使用直接内存是可以提高系统性能的。但由于直接内存没有被Java虚拟机完全托管,若使用不当,也容易触发直接内存溢出,导致宕机。
【示例】下面的代码不断地申请直接内存,并最终可能导致内存溢出
public class DirectBufferOOM {
public static void main(String[] args) {
for (int i = 0; i < 10240; i++) {
ByteBuffer.allocateDirect(1024 * 1024);
System.out.println(i);
// System.gc();
}
}
}
注意代码第6行,System.gc(暂时被注释掉,也就是不会显式触发GC。接着,在Windows平台上,使用32位Java虚拟机,根据以下参数运行上述代码:-Xmx1G -XX:+PrintGCDetails
不用多久,程序就会因为内存溢出而退出,部分打印信息如下:
Exception in thread "main" java. lang . OutOfMemoryError
at java.nio. DirectByteBuffer. <init> (DirectByteBuffer. java:127)
at java.nio. ByteBuffer . allocateDirect (ByteBuffer.java:306)
at geym. zbase. ch7. oom. Di rectBuf ferOOM. main (DirectBuffer0OM. java:14)
可以看到,在大约733次循环时,发生OutOfMemoryError 错误。从堆栈可以看到,发生OOM时,正在进行DirectByteBuffer的分配。
读者也许还会有一个疑问,就是在这里为什么Java的垃圾回收机制没有发挥作用?程序第12行分配的直接内存并没有被任何对象所引用,为何没有被回收呢?从程序的输入日志中也可以看到,虽然打开了-XX:+PrintGCDetails开关,但是并没有一-次GC日志,这说明在整个执行过程中,GC并没有进行。事实上,直接内存不一- 定能够触发GC (除非直接内存使用量达到了-XX:MaxDirectMemorySize的设置),所以保证直接内存不溢出的方法是合理地进行Full GC的执行,或者设定一- 个系统实际可达的-XX:MaxDirectMemorySize值(默认情况下等于-Xmx的设置)。因此,如果系统的堆内存少有GC发生,而直接内存申请频繁,会比较容易导致直接内存溢出(这个问题在32位虚拟机上尤为明显)。如果将上述代码中第6行的System.gc()的注释去掉,使显式GC生效,那么程序将可以正常结束,这说明GC可以回收直接内存。另一个让该程序正常执行的方法是设置一个较 小的堆,在不指定-XX:MaxDirectMemorySize的情况下,最大可用直接内存等于-Xmx的值。
- Xmx512m -XX: +PrintGCDetails
这里将最大堆限制在512MB,而非1GB,这种情况下,最大可用直接内存也为512MB, 操作系统可以同时为堆和直接内存提供足够的空间,当直接内存使用量达到512MB时,也会进行GC释放无用内存空间。
此外,显式设置-XX:MaxDirectMemorySize也是解决这一问题的方法。 只要设置一个系统实际可达的最大直接内存值,那么像这种实际上不应该触发的内存溢出就不会发生了。
综上所述,为避免直接内存溢出,在确保空间不浪费的基础上,合理得执行显式GC,可以降低直接内存溢出的概率,设置合理的-XX:MaxDrectMemorySize也可以避免意外的内存溢出发生,而设置一个较小的堆在32位虚拟机上可以使得更多的内存用于直接内存。
过多线程导致OOM
由于每一个线程的开启都要占用系统内存,因此当线程数量太多时,也有可能导致OOM。由于线程的栈空间也是在堆外分配的,因此和直接内存非常相似,如果想让系统支持更多的线程,那么应该使用一个较小的堆空间。
【示例】需要在32位操作系统下运行 jdk1.7 -Xmx1g
public class MultiThreadOOM {
public static class SleepThread implements Runnable {
@Override
public void run() {
try {
Thread.sleep(10000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 1500000; i++) {
new Thread(new SleepThread(), "Thread: " + i).start();
System.out.println("Thread:" + i + " created");
}
}
}
运行结果:
永久区溢出
永久区(Perm) 是存放类元数据的区域。如果一 一个系统定了太多的类型,那么永久区是有可能溢出的。在JDK 1.8中,永久区被一块称为元数据的区域替代, 但是它们的功能是类似的,都是为了保存类的元信息。
【示例】Jdk1.6执行,1.8已经去掉了-XX:MaxPermSize
public class PermOOM {
public static void main(String[] args) {
for (int i=0;i<100000;i++){
Apple apple = Apple.builder().id(i).build();
}
}
}
运行结果:
解决可以从以下方面考虑:
- 增加MaxPermSize的值
- 减少系统需要的类的数量
- 使用ClassLoader合理地装载各个类,并定期回收