Sun JDK OOM

Java的自动内存管理机制给开发人员带来了很多的便利,在设计、开发时可以完全不用考虑要分配多少内存,要记得回收内存等,但同时也带来了各种各样的问题,其中最典型的问题就是OOM,大部分Java开发人员估计都看到过java.lang.OutOfMemoryError这样的错误信息,在这篇文章中,就来介绍下Sun JDK中有哪几种OOM、OOM示例、造成OOM的原因的查找、解决以及Sun JDK代码中处理OOM的方式。

PDF版本请从此下载:http://blog.bluedavy.com/open/Sun-JDK-OOM.pdf

Java的自动内存管理机制给开发人员带来了很多的便利,在设计、开发时可以完全不用考虑要分配多少内存,要记得回收内存等,但同时也带来了各种各样的问题,其中最典型的问题就是OOM,大部分Java开发人员估计都看到过java.lang.OutOfMemoryError这样的错误信息,在这篇文章中,就来介绍下Sun JDK中有哪几种OOM、OOM示例、造成OOM的原因的查找、解决以及Sun JDK代码中处理OOM的方式。

PDF版本请从此下载:http://blog.bluedavy.com/open/Sun-JDK-OOM.pdf

1 OOM的种类
在Sun JDK中运行时,Java程序有可能出现如下几种OOM错误:
 java.lang.OutOfMemoryError: unable to create new native thread
当调用new Thread时,如已创建不了线程了,则会抛出此错误,如果是JDK内部必须创建成功的线程,那么会造成Java进程退出,如果是用户线程,则仅抛出OOM,创建不了的原因通常是创建了太多线程,耗尽了内存,通常可通过减少创建的线程数,或通过-Xss调小线程所占用的栈大小来减少对Java 对外内存的消耗。

 java.lang.OutOfMemoryError: request bytes for . Out of swap space?
当JNI模块或JVM内部进行malloc操作(例如GC时做mark)时,需要消耗堆外的内存,如此时Java进程所占用的地址空间超过限制(例如windows: 2G,linux: 3G),或物理内存、swap区均使用完毕,那么则会出现此错误,当出现此错误时,Java进程将会退出。

 java.lang.OutOfMemoryError: Java heap space
这是最常见的OOM错误,当通过new创建对象或数组时,如Java Heap空间不足(新生代不足,触发minor GC,还是不够,触发Full GC,还是不够),则抛出此错误。

 java.lang.OutOfMemoryError: GC overhead limit execeeded
当通过new创建对象或数组时,如Java Heap空间不足,且GC所使用的时间占了程序总时间的98%,且Heap剩余空间小于2%,则抛出此错误,以避免Full GC一直执行,可通过UseGCOverheadLimit来决定是否开启这种策略,可通过GCTimeLimit和GCHeapFreeLimit来控制百分比。

 java.lang.OutOfMemoryError: PermGen space
当加载class时,在进行了Full GC后如PermGen空间仍然不足,则抛出此错误。
对于以上几种OOM错误,其中容易造成严重后果的是Out of swap space这种,因为这种会造成Java进程退出,而其他几种只要不是在main线程抛出的,就不会造成Java进程退出。

2 OOM示例、原因查找和解决
这些示例的class以及源码请从http://blog.bluedavy.com/jvm/cases/oom/OOMCases.zip下载,建议在运行前不要看源码,毕竟源码是简单的例子,如果直接看源码的话,可能会少了查找原因的乐趣。

当Java程序运行时,会有很多种造成OOM的现象,这里面有些会比较容易查找出原因,而有些会非常困难,下面是来看一些OOM的Example。
 Example 1
以-Xms20m -Xmx20m -Xmn10m -XX:+UseParallelGC参数运行com.bluedavy.oom.JavaHeapSpaceCase1
运行后在输出的日志中可看到大量的Full GC信息,以及:
java.lang.OutOfMemoryError: GC overhead limit exceeded
和java.lang.OutOfMemoryError: Java heap space
根据上面所说的OOM种类,可以知道这是在new对象或数组时Java Heap Space不足造成的,对于这种OOM,需要知道的是程序中哪些部分占用了Java Heap。
要知道程序中哪些部分占用了Java Heap,首先必须拿到Java Heap中的信息,尤其是OOM时的内存信息,在Sun JDK中可通过在启动参数上加上-XX:+ HeapDumpOnOutOfMemoryError来获得OOM时的Java Heap中的信息,当出现OOM时,会自动生成一个java_pid[pid].hprof的文件。
于是在启动参数上加上上面的参数,再次运行JavaHeapSpaceCase1,可看到在当前运行的路径下生成了一个java_pid10852.hprof(由于pid不同,你看到的可能是不一样的文件名)的文件,在拿到这个文件后,就可通过mat(http://www.eclipse.org/mat/)来进行分析了。
用mat打开上面的文件(默认情况下mat认为heap dump文件应以.bin结尾,因此请改名或以open file方式打开),打开后点击dominator_tree,可看到sun.misc.Launcher$AppClassLoader占据了大部分的内存,继续点开看,则可看到是由于com.bluedavy.oom.Caches中有一个ArrayList,其中存放的对象占据了大部分的内存,因此解决这个OOM的办法是,让Caches类中放的对象总大小是有限制的,或者限制放入Caches的ArrayList中的对象个数。
这种情况造成的OOM,在实际的场景中当使用缓存时很容易产生,对于所有的缓存、集合大小都应给定限制的最大大小,以避免出现缓存或集合太大,导致消耗了过多的内存,从而导致OOM。

 Example 2
以-Xms20m -Xmx20m -Xmn10m -XX:+HeapDumpOnOutOfMemoryError执行com.bluedavy.oom.JavaHeapSpaceCase2
运行后在输出的日志中可看到大量的Full GC和java.lang.OutOfMemoryError: Java heap space。
同样,首先用mat打开需要分析的hprof文件,很惊讶的发现什么都看不出来,Java Heap Space还很充足,这就奇怪了,还好除了能在OOM时自动dump出Heap的信息外,还可通过jmap命令手工dump,于是,在运行期出现频繁Full GC、OOM的时候,手工通过jmap –dump:file=heap.bin,format=b [pid]生成一个heap.bin文件,把这个heap.bin文件也拿到mat中分析,杯具,还是什么都看不出来,还好,还有一招,就是直接用jmap –histo看看到底什么对象占用了大多数的内存,执行一次,看到[I占用了最多的内存,没用,因为没法知道这个[I到底是代码中哪个部分创建的,不甘心,多执行几次,很幸运,突然看到com.bluedavy.oom.Case2Object占据了最大的内存,于是查找代码中哪些地方创建了这个对象,发现了代码中有一个线程创建了大量的Case2Object,修改即可。
从这个例子中,可以看到,在分析OOM问题时,一方面是依赖OOM时dump出来的文件,但这个文件其实只会在Java进程中第一次出现OOM时生成,之后再OOM就不会生成了,这有可能出现真实的OOM的原因被假的OOM原因给掩盖掉;另一方面是依赖在出现OOM时的人工操作,这种人肉方式其实比较杯具,因为只能先等到频繁Full GC、OOM,首先通过jmap –histo来看看到底什么对象占用了大部分的内存(需要多执行几次,以确保正确性),上面的例子比较幸运,因为刚好是自定义的对象,如果是原生的类型,那就只能借助dump文件来分析了,通过jmap –dump手工dump出Heap文件,然后用MAT分析,但有可能会出现上面例子中的状况,就是mat分析上也看不出什么,顶多只能看到unreachable objects里某些对象占用了大部分的内存,而通常情况看到的可能都是原生类型,一旦真的碰到jmap –histo看到的是原生类型占用较多,jmap dump看到的是Java Heap Space也不满的话,那只能说杯具了,在这种情况下,唯一能做的是捕捉所有的异常,然后打印,从而判断OOM是在哪行代码抛出的。

 Example 3
以-Xms20m -Xmx20m -Xmn10m -XX:+HeapDumpOnOutOfMemoryError执行com.bluedavy.oom.JavaHeapSpaceCase3
在控制台中可看到大量的java.lang.OutOfMemoryError: Java heap space,把生成的hprof文件放入MAT中进行分析,还好看到确实是Java Heap Space满了,这就好办了,点开Dominator Tree视图,可看到有一堆的线程,每个都占用了一定的内存,从而导致了OOM,要解决这个例子中的OOM,有四种办法:一是减少处理的线程数;二是处理线程需要消耗的内存;三是提升线程的处理速度;四是增加机器,减少单台机器所需承担的请求量。
上面这种状况在系统处理慢的时候比较容易出现。

 Example 4
以-Xms20m -Xmx20m -Xmn10m -XX:+HeapDumpOnOutOfMemoryError执行com.bluedavy.oom.JavaHeapSpaceCase4
在控制台可看到大量的java.lang.OutOfMemoryError: Java heap space,把生成的hprof文件放入MAT中进行分析,可看到TaskExecutor中的handlers占据了大量的内存,分析代码,发现是由于在task处理完后没有清除掉对应的handler造成的,修改即可解决此OOM问题。
这是个典型的内存泄露的例子,如果不小心持有了本应释放引用的对象,那么就会造成内存泄露,这是在编写Java程序时需要特别注意的地方。

 Example 5
以-Xms1536m -Xmx1536m -Xss100m执行com.bluedavy.oom.CannotCreateThreadCase
在控制台可看到java.lang.OutOfMemoryError: unable to create new native thread。
对于这种情况,一需要查看目前-Xss的大小,例如在这个例子中-Xss太大,导致连20个线程都无法创建,因此可解决的方法是降低-Xss的大小;如果Xss使用的是默认值,那么可通过jstack来查看下目前Java进程中是不是创建了过多的线程,或者是java heap太大,导致os没剩多少内存,从而创建不出线程。

 Example 6
Out of swap的例子实在太难举了,就没在此列出了,对于Out of swap的OOM,需要首先观察是否因为Java Heap设置太大,导致物理内存+swap区不够用;如果不是的话,则需关注到底是哪些地方占用了堆外的内存,可通过google-perftools来跟踪是哪些代码在调用malloc,以及所耗的内存比例,在跟踪到后可继续查找原因。
总体来说,Out of swap通常是最难查的OOM,由于其是直接退出java进程的,因此需要结合core dump文件和hs_err_pid[pid].log进行分析,最关键的还是像查java heap那样,要查出到底是什么代码造成了native heap的消耗。

 Example 7
PermGen空间满造成OOM的情况通常采取的解决方法是简单的扩大PermSize。

总结上面的例子来看,对于OOM的情况,最重要的是根据OOM的种类查找到底是代码中的什么部分造成的消耗。

对于Java Heap Space OOM和GC overhead limit exceeded这两种类型,可通过heap dump文件以及jmap –histo来进行分析,多数情况下可通过heap dump分析出原因,但也有少数情况会出现heap dump分析不出原因,而jmap –histo看到某原生类型占据了大部分的内存,这种情况就非常复杂了,只能是仔细查看代码,并捕捉OutOfMemoryError,从而来逐渐定位到代码中哪部分抛出的。

对于Out of swap这种类型,其主要是地址空间超过了限制或者对外内存不够用了造成的,首先需要查看Java Heap设置是否过大,然后可结合google-perftools来查看到底是哪些代码调用了malloc,在堆外分配内存。

3 Sun JDK代码中处理OOM的方式
在Sun JDK代码中,在不同的OOM时,会调用不同的处理方式来进行处理,下面就来看看JDK中几个典型的处理OOM的代码。
 创建线程失败
compiler_thread = new CompilerThread(queue, counters);
if (compiler_thread == NULL || compiler_thread->osthread() == NULL){
vm_exit_during_initialization(“java.lang.OutOfMemoryError”,
“unable to create new native thread”);
}
对于JDK中必须创建成功的线程,如失败会通过调用vm_exit_during_initialization打印出OOM错误,并退出Java进程。
对于非必须创建成功的线程,通常会调用
THROW_MSG(vmSymbols::java_lang_OutOfMemoryError(),
“unable to create new native thread”);
抛出OOM错误信息。

 调用os:malloc失败
void *p = os::malloc(bytes);
if (p == NULL)
vm_exit_out_of_memory(bytes, “Chunk::new”);
return p;
当os:malloc或os:commit_memory失败时,会直接输出错误信息,并退出Java进程。

 Java Heap上分配失败后
report_java_out_of_memory(“Java heap space”);
调用这个就表明了不会退出vm,而只是抛出OOM错误。
例如PermGen分配失败时的代码更为直观:
HeapWord* result = Universe::heap()->permanent_mem_allocate(size);
if (result != NULL) {
NOT_PRODUCT(Universe::heap()->
check_for_non_bad_heap_word_value(result, size));
assert(!HAS_PENDING_EXCEPTION,
“Unexpected exception, will result in uninitialized storage”);
return result;
}
// -XX:+HeapDumpOnOutOfMemoryError and -XX:OnOutOfMemoryError support
report_java_out_of_memory(“PermGen space”);

总体而言,对于一个大型系统而言,通常OOM是难以避免的现象,最重要的还是一旦出现OOM,要掌握排查的方法,另外就是,随着现在内存越来越便宜,CMS GC越来越成熟,采用64 bit操作系统,开启大内存也是一种可选方式,基本上可以避免内存成为大问题,毕竟在Java中完全可能随便写几行代码就不知不觉消耗了很多内存。

ps: 感兴趣的同学还可参考sun官方的这篇关于OOM的文章:
http://java.sun.com/javase/6/webnotes/trouble/TSG-VM/html/memleaks.html