不出意料,java.lang.OutOfMemoryError 可以算是Java应用中最常见的JVM错误 了,这类错误往往是Java堆没有足够的空间容纳新对象,但引起这类错误的原因还有诸多种,可能平时很少有缘分能相见,本文将探究其中几种引起该错误的场景,以备不时之需。
java.lang.OutOfMemoryError: Java heap space
这应该是最常见的OutOfMemoryError 错误场景,通常JVM内存会被分成堆(Heap) 和永久代(Perm Gen,Java 8中已被Metaspace取代) ,可分别通过-Xmx 和-XX:MaxPermSize 来设置。而通常引起该错误的原因是,当程序正在尝试创建新对象,此时JVM堆中没有足够连续的空间容纳该对象 。除此之外,程序中的一些问题也可能引起这类错误:
用户/数据量达到峰值 。在业务面临峰值时,会有可能触发java.lang.OutOfMemoryError: Java heap space ,但随着业务趋于平缓,这种错误也会随之消失,这通常受限于自身配置。
内存泄露 。由于开发人员编写程序不当,造成某些对象既没有被应用程序使用,也不能被垃圾收集器回收,这类对象将不断消耗内存,最终也会导致java.lang.OutOfMemoryError: Java heap space 。
案例
内存不足
class OOM {
static final int SIZE = 2 * 1024 * 1024 ;
public static void main ( String [] a ) {
int [] i = new int [ SIZE ];
}
}
当配置-Xmx12m 时,程序如下抛出了异常,配置为-Xmx13m 正常运行:
Exception in thread "main" java . lang . OutOfMemoryError : Java heap space
at me . hao0 . jvm . oom . OOM . main ( OOM . java : 8 )
at sun . reflect . NativeMethodAccessorImpl . invoke0 ( Native Method )
at sun . reflect . NativeMethodAccessorImpl . invoke ( NativeMethodAccessorImpl . java : 57 )
at sun . reflect . DelegatingMethodAccessorImpl . invoke ( DelegatingMethodAccessorImpl . java : 43 )
at java . lang . reflect . Method . invoke ( Method . java : 606 )
内存泄露
在Java中其实并没有确切存在内存泄露 ,内存管理都交由JVM 自身去处理,如分配内存 ,回收不用的对象 等。但在实际中,由于开发人员的疏忽,有可能导致内存泄露 的现象,如不再被应用使用的对象不能被垃圾收集器识别 等,导致Java堆占用持续增长,最终触发java.lang.OutOfMemoryError: Java heap space 。如下面的代码片段:
public class KeylessEntry {
static class Key {
Integer id ;
Key ( Integer id ) {
this . id = id ;
}
@Override
public int hashCode () {
return id . hashCode ();
}
}
public static void main ( String [] args ){
Map m = new HashMap ();
while ( true ){
for ( int i = 0 ; i < 10000 ; i ++){
if (! m . containsKey ( new Key ( i ))){
m . put ( new Key ( i ), "Number:" + i );
}
}
}
}
}
上面的代码看似通过containsKey 来保证Map中最多含有10000个对象,但由于Key没有正确重写equals() 方法,导致containsKey 内部比较对象时,始终调用Object.equals() ,两个对象始终不相等,因此会不断往Map中放入新对象,造成java.lang.OutOfMemoryError: Java heap space (但需将-Xmx 配置小些,如8M,否则抛出的是GC overhead limit exceeded )。修复这个问题也很简单,即Key 重写equals() 方法:
@Override
public boolean equals ( Object o ) {
boolean response = false ;
if ( o instanceof Key ) {
response = ((( Key ) o ). id ). equals ( this . id );
}
return response ;
}
但通常面临java.lang.OutOfMemoryError: Java heap space ,第一时间可能是增加堆大小 ,但只要堆配置不是特别不合理,那么原因可能并不是堆大小的问题,所以建议启动JVM进程时都将GC日志 及堆dump信息 打开,便于排查。
java.lang.OutOfMemoryError: GC overhead limit exceeded
与其他一些语言相比,JVM 提供了一个举足轻重的功能就是自动内存回收 ,JVM通过在运行时启动了专门的GC线程 来进行内存回收的工作,使得开发人员从内存管理的痛苦 中得以解脱。但不是任何时候,GC线程 都能安然无恙地工作,比如抛出异常java.lang.OutOfMemoryError: GC overhead limit exceeded 时,该错误表明垃圾收集器 正在尝试释放内存,但实际释放的内存却很少(默认情况下,当JVM在GC上花费了总时间的98%,然后GC之后却回收了不到2%的堆内存 ),出现这样的情况,则是由于应用程序耗尽了所有可用的内存,多次GC都不能回收这些内存 。
案例
正如之前的代码片段 ,若将堆大小设置大一些(如-Xms16m ),最终抛出的异常会是GC overhead limit exceeded :
Exception in thread "main" java . lang . OutOfMemoryError : GC overhead limit exceeded
at java . lang . Integer . valueOf ( Integer . java : 642 )
at me . hao0 . jvm . oom . KeylessEntry . main ( KeylessEntry . java : 31 )
at sun . reflect . NativeMethodAccessorImpl . invoke0 ( Native Method )
at sun . reflect . NativeMethodAccessorImpl . invoke ( NativeMethodAccessorImpl . java : 57 )
at sun . reflect . DelegatingMethodAccessorImpl . invoke ( DelegatingMethodAccessorImpl . java : 43 )
at java . lang . reflect . Method . invoke ( Method . java : 606 )
at com . intellij . rt . execution . application . AppMain . main ( AppMain . java : 144 )
但当将GC换为-XX:+UseConcMarkSweepGC 或-XX:+UseG1GC ,上面的代码片段则只会抛出Java heap space ,JVM中也提供了-XX:-UseGCOverheadLimit 来去除GC overhead limit exceeded 这种异常消息,但并不推荐这么做,而是针对该问题进行修复。
java.lang.OutOfMemoryError: Permgen space
Java 7及其之前,JVM内存被分为了几个不同的区:
Permgen space 表明该异常发生在永久代 ,即永久代 内存已经耗尽。从JVM内存管理 中可知,永久代 主要用于存放已加载类的元数据 (包括类名称,字段及其方法字节码等),常量池信息 ,类相关的对象数据和类型数组 ,及JIT编译器的优化信息 等,而往往出现这类错误的原因是JVM加载的类太多或太大 。
案例
下面的例子通过javassist 库不断生成class:
import javassist.ClassPool ;
public class MicroGenerator {
public static void main ( String [] args ) throws Exception {
for ( int i = 0 ; i < 100_000_000 ; i ++) {
generate ( "eu.plumbr.demo.Generated" + i );
}
}
public static Class generate ( String name ) throws Exception {
ClassPool pool = ClassPool . getDefault ();
return pool . makeClass ( name ). toClass ();
}
}
然而,在本机环境(Mac OSX 10.11.5 ,JDK 1.7.0_80 )中并没有抛出java.lang.OutOfMemoryError: PermGen space ,而是:
Exception in thread "main"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"
大概意思是说,java.lang.OutOfMemoryError 没有被main线程catch,调用了线程的UncaughtExceptionHandler ,出现该情况时,表明JVM已经没有足够内存 分配OutOfMemoryError实例 ,因此将堆栈信息输出到了打印流(print stream)中,开发人员可以在运行程序前设置线程的UncaughtExceptionHandler ,输出对应的错误信息,如:
Thread . currentThread (). setUncaughtExceptionHandler ( new Thread . UncaughtExceptionHandler () {
@Override
public void uncaughtException ( Thread t , Throwable e ) {
System . err . println ( e . getMessage ());
}
});
除了上述这种情况,在平时重新部署应用时,有可能由于一些第三方库错误的资源处理,在每次重部署时,导致Class 的旧版本不能被ClassLoader 卸载,最终造成java.lang.OutOfMemoryError: PermGen space ,对于这类比较隐藏的问题,可以dump堆信息(jmap -dump:format=b, le=dump.hprof ),通过MAT 工具进行分析,如:
还有可能就是,JVM使用CMS 垃圾收集器时,会默认关闭卸载类 的选项,因而不会卸载一些已经不再使用类数据信息,导致java.lang.OutOfMemoryError: PermGen space ,可以通过-XX:+CMSClassUnloadingEnabled 开启该选项。
java.lang.OutOfMemoryError: Metaspace
Java 8之后,永久代(Perm Gen) 被Metaspace 取代:
而之所以要用Metaspace 取代Perm Gen ,大致有如下原因:
Perm Gen大小难以确定 。如果设置太小,则有可能导致java.lang.OutOfMemoryError: Permgen ,设置过大,则浪费了系统资源。
通过并发地为类数据分配内存,且不用暂停垃圾收集 和特定的元数据迭代器 ,提升了垃圾收集性能。
针对G1垃圾收集器,支持并发地卸载类 。
而引起java.lang.OutOfMemoryError: Metaspace 异常的原因,通常则是有太多的类或太大的类 被加载到Metaspace 区。
案例
同样,利用javassist 不断生成类:
public class Metaspace {
static javassist . ClassPool cp = javassist . ClassPool . getDefault ();
public static void main ( String [] args ) throws Exception {
for ( int i = 0 ; ; i ++) {
Class c = cp . makeClass ( "eu.plumbr.demo.Generated" + i ). toClass ();
}
}
}
将Metaspace 设置小一些,如-XX:MaxMetaspaceSize=64m ,运行一段时间,则会抛出:
Exception in thread "main" javassist.CannotCompileException: by java.lang.OutOfMemoryError: Metaspace
at javassist.ClassPool.toClass( ClassPool.java:1170)
at javassist.ClassPool.toClass( ClassPool.java:1113)
at javassist.ClassPool.toClass( ClassPool.java:1071)
at javassist.CtClass.toClass( CtClass.java:1264)
at me.hao0.jvm.gc.Metaspace.main( Metaspace.java:9)
at sun.reflect.NativeMethodAccessorImpl.invoke0( Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke( NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke( DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke( Method.java:498)
at com.intellij.rt.execution.application.AppMain.main( AppMain.java:144)
Caused by: java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1( Native Method)
at java.lang.ClassLoader.defineClass( ClassLoader.java:763)
at java.lang.ClassLoader.defineClass( ClassLoader.java:642)
at sun.reflect.GeneratedMethodAccessor1.invoke( Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke( DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke( Method.java:498)
at javassist.ClassPool.toClass2( ClassPool.java:1183)
at javassist.ClassPool.toClass( ClassPool.java:1164)
... 9 more
面对这类错误,比较直接的办法就是增大Metaspace ,或者不设置MaxMetaspaceSize ,使得Metaspace 区能自动增长,但这有可能导致系统内存资源不足,造成严重的Swap 操作。更适合的做法,还是需要通过堆dump 分析是否存在内存泄露,确认是由于Metaspace 不足导致的。
java.lang.OutOfMemoryError: Unable to create new native thread
Unable to create new native thread 意味着,Java应用已经达到其能申请的最大线程数,而该值取决于不同的平台,而该异常通常会在以下几个阶段发生:
Java应用尝试创建一个新线程;
JVM本地代码代理 应用程序,向操作系统发起创建本地线程的请求;
操作系统尝试创建新的本地线程,并为其分配内存;
由于Java进程已耗尽了自己限制的内存地址空间,或者系统虚拟内存 已经耗尽,操作系统拒绝本地内存分配;
java.lang.OutOfMemoryError: Unable to create new native thread 异常被抛出。
案例
下面的代码片段,不断地创建新线程,不同平台创建的最大线程数也会不一致:
public class UnableCreateNativeThread {
private static int threadCount = 0 ;
public static void main ( String [] args ) {
while ( true ) {
new Thread ( new Runnable () {
public void run () {
try {
System . out . println (++ threadCount );
Thread . sleep ( 10000000 );
} catch ( InterruptedException e ) {
}
}
}). start ();
}
}
}
默认情况下,在本机Mac 10.11.5,JDK8_91 上,默认创建了2027个线程后,抛出了该异常:
从堆dump信息 中看,可见Thread实例过多:
除此外,线程dump信息(jstack pid) ,可以看见太多线程处于TIMED_WAITING (sleeping) 。对于这类情况,有时可以通过增大系统最大用户进程数 来解决无法创建本地线程的问题,如:
ulimit -a
...
-u : processes 709
...
java.lang.OutOfMemoryError: Out of swap space
当JVM 启动时,指定的堆大小(-Xmx )比操作系统的可用内存大时,操作系统将会把内存数据保存至虚拟内存(Swap space) ,如:
当Java应用请求内存分配时,但此时本地堆空间已经耗尽,则会抛出java.lang.OutOfMemoryError: Out of swap space 异常,而通常该异常由一些系统级别问题引起,如:
操作系统配置的虚拟内存(Swap space)空间 不足;
系统中的其他进程占用了所有的内存资源。
当然,除了操作系统级别的问题,也有可能是Java应用程序的本地代码没有正确释放内存资源,导致本地内存泄露所致。当遇到这样的情况时,最直接简单的办法就是增加虚拟内存(Swap space) ,如:
swapoff -a
dd if = /dev/zero of = swap le bs = 1024 count = 655360
mkswap swapfile
swapon swapfile
但单纯增加虚拟内存(Swap space) ,是有可能延长GC暂停时间的,所以需要慎重考虑。也有可能Java应用所在机器上,有其他进程在争用物理内存,这时应该将该进程与Java进程隔离,或升级物理内存,以减少内存争用。而针对内存使用优化方面,则可以通过分析堆dump信息,减少一些不必要的内存分配。
java.lang.OutOfMemoryError: Requested array size exceeds VM limit
对于Java应用中的数组对象,其最大长度(取决于不同的平台,通常在10亿到21亿之间 )是有限制的:
该异常由JVM本地代码抛出,JVM在分配内存,会检查该数据结构在当前平台上是否可寻址。然而这个错误不会太常见,因为Java中数组以int型为下标,即最大值为2^31 – 1 = 2,147,483,647 ,而平台对数据长度限制也接近该值,因此可能会优先抛出java.lang.OutOfMemoryError: Java heap space
案例
下面的代码片段设置数组长度接近为Integer.MAX_VALUE :
public class ArrayLenLimit {
public static void main ( String [] args ) {
for ( int i = 3 ; i >= 0 ; i --) {
try {
int [] arr = new int [ Integer . MAX_VALUE - i ];
System . out . format ( "Successfully initialized an array with %,d elements.\n" , Integer . MAX_VALUE - i );
} catch ( Throwable t ) {
t . printStackTrace ();
}
}
}
}
结果抛出了如下异常:
前两次由于分配内存时,超出了JVM堆大小,但未超出数组最大长度限制 ,因此抛出了java.lang.OutOfMemoryError: Java heap space 异常,后面两次优先超出了最大长度限制 ,抛出java.lang.OutOfMemoryError: Requested array size exceeds VM limit 。
在实际应用中,有可能由于数组使用不当,造成数组长度不断增加,最终超出最大长度限制 ,遇到这种情况,通常很少会需要这种大的数组,或者将大数组分割为多个小数组。
Out of memory: Kill process or sacrifice child
Out of memory: Kill process or sacrifice child 主要由操作系统内置的OOM Killer 机制造成的。OOM Killer 是Linux中的一种内存保护机制,当系统内存已经极限不足时,OOM Killer 将对现有进程作评分,将评分最低的进程kill掉,以释放内存供其他进程使用:
当可用的虚拟内存(包括swap)达到某一程度(将给操作系统的稳定性带来风险 )时,OOM Killer 就会尝试kill掉那些游手好闲 的进程。
针对这种情况,最简单直接的办法是将Java应用迁移到内存足够的机器上,或者当前机器进行OOM Killer内存调优 ,以减少应用之间的内存争用。另外一个不推荐的做法,是增加虚拟内存(Swap space) ,但这却有可能带来系统性能上的问题。
总结
以上,则是java.lang.OutOfMemoryError 异常相关的情形,有一些比较常见,有一些可能会很少碰到,待在实际中碰到时,能够对症下药。
参考文献
OOM错误指南
如何使永久代内存泄漏
OOM杀手
Linux内存调优