OOM为out of memory的简称,称之为内存溢出,来源于java.lang.OutOfMemoryError。当JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出这个error(注:非exception,因为这个问题已经严重到不足以被应用处理)。
既然是内存溢出,那我们就先看看JVM有哪些内存区间。之前文章提到过,Java虚拟机在运行时,会把内存划分为若干不同的数据区域,主要有以下几部分:堆、方法区、栈、直接内存。
一.内存溢出(OOM)的原因
1.堆溢出
java堆内存溢出,堆上分配的对象越来越多,有没有及时回收,引起内存溢出。此种情况最常见,一般由于内存泄露或者堆的大小设置不当引起。
代码例子:
public static void main(String args[]){
ArrayList<byte[]> list=new ArrayList<byte[]>();
for(int i=0;i<1024;i++){
list.add(new byte[1024*1024]);
}
}
解决方法:
a.增大堆空间
b.及时释放内存
2.方法区溢出
方法区用于保存已被虚拟机加载的类信息,这部分内存溢出,一般出现于大量Class或者jsp页面,或者采用cglib等反射机制的情况,因为上述情况会产生大量的Class信息存储于方法区。
代码例子:(生成大量的类)
public static void main(String[] args) {
for(int i=0;i<100000;i++){
CglibBean bean = new CglibBean("geym.jvm.ch3.perm.bean"+i,new HashMap());
}
}
解决方法:
a.增大Perm区
b.设置允许Class回收
3.栈溢出
栈溢出两种情况:
1、创建线程过多,抛出OutOfMemoryError
2、线程请求的栈深度大于虚拟机允许的最大深度 StackOverflowError
第一种栈溢出指,在创建线程的时候,需要为线程分配栈空间,这个栈空间是向操作系统请求的,如果操作系统无法给出足够的空间,就会抛出OOM。
代码例子:(设置-Xss1m,表示每创建1个线程需要分配1MB的空间)
public static class SleepThread implements Runnable{
public void run(){
try {
Thread.sleep(10000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}public static void main(String args[]){
for(int i=0;i<1000;i++){
new Thread(new SleepThread(),"Thread"+i).start();
System.out.println("Thread"+i+" created");
}
}
解决方法:
a.减少堆内存,因为堆空间和线程栈空间大小总和不能大于操作系统的可分配内存,因此减少堆空间可为栈节约出空间。
b.减少线程栈大小,通过-Xss将单个线程栈空间缩小,就能创建更多的线程,当然前提是单个线程栈空间够用。
第二种栈溢出指StackOverflowError。 是一个java中常出现的错误。在jvm运行时的数据区域中有一个java虚拟机栈,当执行java方法时会进行压栈弹栈的操作。在栈中会保存局部变量,操作数栈,方法出口等等。
由JVM的内存结构我们可知:
- 随着线程栈的大小越大,能够支持越多的方法调用,也即是能够存储更多的栈帧;
- 局部变量表内容越多,那么栈帧就越大,栈深度就越小。
jvm设置限制了栈的最大深度,当执行时栈的深度大于了允许的深度,就会抛出StackOverflowError错误。一般在递归的时候,递归层级过多,导致栈溢出。
然而,递归并不是导致此错误的唯一原因。在应用程序不断从方法内调用方法直到堆栈耗尽的情况下,也可能发生这种情况。这是一种罕见的情况,因为没有开发人员会故意遵循糟糕的编码实践。
另外一种可能的原因是方法中有大量局部变量,因为局部变量表内容越多,那么栈帧就越大,栈深度就越小。造成虚拟机在扩展栈深度时,无法申请到足够的内存空间。
当应用程序设计为类之间具有循环关系时,也可以抛出StackOverflowError。在这种情况下,会重复调用彼此的构造函数,从而引发此错误。这也可以被视为递归的一种形式。
解决方法:如果程序没有问题,确实需要较大的调用深度,可以通过调整-Xss增加栈内存。
4.直接内存溢出
直接内存并不是虚拟机运行时数据区的一部分,也不是Java 虚拟机规范中定义的内存区域。在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用native 函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
操作系统可分配给JVM的内存是一定的,这部分内存要被堆空间、线程栈空间和直接内存瓜分。这三块空间加起来,是不能大于操作系统可分配内存的。直接内存的分配不会受到Java 堆大小的限制,受到本机总内存大小限制,它也有可能出现OutOfMemoryError。
代码例子:
for(int i=0;i<1024;i++){
ByteBuffer.allocateDirect(1024*1024);
System.out.println(i);
System.gc();
}
解决方法:
a.减少堆内存,道理同栈溢出
b.有意触发GC,直接内存的过度使用会导致OOM,但不会触发GC,只有堆和方法区的分配会导致GC发生。直接内存不触发GC,但却可以通过GC进行回收,因此可以人工触发GC,进行直接内存的回收。
二.内存溢出(OOM)分析
当发生OOM时,需要借助工具进行分析。常用的如Memory Analyzer(MAT)、Visual VM等。使用MAT进行分析时,要理解以下基础概念:
1.浅堆:
一个对象结构所占用的内存大小。
以String类型为例:
3个int类型以及一个引用类型合计占用内存3*4+4=16个字节。再加上对象头的8个字节,因此String对象占用的空间,即浅堆的大小是16+8=24字节。对象大小按照8字节对齐,一定是8的倍数。
注意:浅堆大小和对象的内容无关,只和对象的结构有关。
2.深堆:
一个对象被GC回收后,可以真实释放的内存大小。也就是只能通过该对象访问到的(直接或者间接)所有对象的浅堆之和。