常见OOM详解

1,095 阅读14分钟

引言:更多相关请看 JVM+GC解析系列
在java虚拟机规范中,虚拟机栈和本地方法栈都会出现StackOverflowError和OutofMemoryError,程序计数器是java虚拟机中唯一一块不会产生error的内存区域。
注意:OOM是Error,不是Exception。
如StackOverflowError的继承体系:Object->Throwable->Error->VirtualMachineError->StackOverflowError。
如OutOfMemoryError的继承体系:Object->Throwable->Error->VirtualMachineError->OutOfMemoryError。

Java.lang.StackOverflowError

(栈溢出)代表的是,当栈深度超过虚拟机分配给线程的栈大小时就会出现此error。 引发 StackOverFlowError 的常见原因有以下几种:
无限递归循环调用(最常见:对象/方法自我调用、两个对象/方法相互调用);
执行了大量方法,导致线程栈空间耗尽;
方法内声明了海量的局部变量;
代码:

@Data
class User{
    public User(){
        new Stu(); // Exception in thread "main" java.lang.StackOverflowError
    }
}

@Data
class Stu{
    public Stu(){
        new User();// Exception in thread "main" java.lang.StackOverflowError
    }
}

public class OOMDemo {
    public OOMDemo(){
        new OOMDemo();// Exception in thread "main" java.lang.StackOverflowError
    }
    public static void main(String[] args) throws Exception {
//        new OOMDemo();// 对象自我调用
//        stackOverflowError01();// 方法自我调用
//        stackOverflowError02();// 两个方法相互调用
        new Stu();// 两个对象相互调用
    }



    private static void stackOverflowError01() {
        stackOverflowError01();// Exception in thread "main" java.lang.StackOverflowError
    }

    private static void stackOverflowError02() {
        stackOverflowError03();// Exception in thread "main" java.lang.StackOverflowError
    }

    private static void stackOverflowError03() {
        stackOverflowError02();// Exception in thread "main" java.lang.StackOverflowError
    }

}

Java.lang.OutOfMemoryError:Java heap space

内存溢出之堆内存不足。 JVM参数设置最大最小内存为10m:

-Xms10m -Xmx10m -XX:+PrintGCDetails

代码:

public class OOMDemo {

    public static void main(String[] args) throws Exception {
        /**
         * JVM参数设置最大最小内存为10m:-Xms10m -Xmx10m -XX:+PrintGCDetails
         * 设定一个50M的大对象
         * */
//        byte[] bytes = new byte[50*1024*1024];
        // 无限产生对象也会爆Error
        String str = "hello";
        while (true){
            str += str + new Random().nextInt(333333333) + new Random().nextInt(555555555);
            str.intern();
        }
    }

}

执行效果:

[GC (Allocation Failure) [PSYoungGen: 2048K->504K(2560K)] 2048K->879K(9728K), 0.0011060 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 789K->488K(2560K)] 1165K->911K(9728K), 0.0016022 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 488K->488K(2560K)] 911K->935K(9728K), 0.0007646 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 488K->0K(2560K)] [ParOldGen: 447K->861K(7168K)] 935K->861K(9728K), [Metaspace: 3079K->3079K(1056768K)], 0.0052523 secs] [Times: user=0.11 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 861K->861K(9728K), 0.0003575 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 861K->844K(7168K)] 861K->844K(9728K), [Metaspace: 3079K->3079K(1056768K)], 0.0073385 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 2560K, used 111K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 5% used [0x00000000ffd00000,0x00000000ffd1bc18,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 7168K, used 844K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 11% used [0x00000000ff600000,0x00000000ff6d3018,0x00000000ffd00000)
 Metaspace       used 3157K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 344K, capacity 388K, committed 512K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.example.demo.test01.OOMDemo.main(OOMDemo.java:10)

Java.lang.OutOfMemeoryError:GC overhead limit exceeded

内存溢出之垃圾回收超出限制。
程序在垃圾回收上花费了98%的时间,却收集不回2%的空间,通常这样的异常伴随着CPU的冲高。 GC回收时间过长时会抛出OutOfMemoryError,过长的定义是,超过98%的时间用来做CC并且回收了不到2%的堆内存,连续多次GC却只回收了不到2%的情况下才会抛出,假如不抛出GC overhead limit错误会出现:GC清理的这么点内存快会再次填满,追使GC再次执行,没有意义,这样就形成恶性循环。CPU使用率一直都是100%,而GC却没有任何成果。
JVM参数设置:(-XX:MaxDirectMemorySize 最大直接内存大小)

-Xms10m -Xmx10m -XX:MaxDirectMemorySize=5m

代码:

public class OOMDemo {

    public static void main(String[] args) throws Exception {
        int i = 0;
        List<String> list = new ArrayList<>();
        try {
            while (true){
                list.add(String.valueOf(i++).intern());
            }
        }catch (Throwable e){
            System.out.println("******************* i:"+ i);
            e.printStackTrace();
            throw e;
        }
    }

}

执行效果:

...[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7046K->7046K(7168K)] 9094K->9094K(9728K), [Metaspace: 3457K->3457K(1056768K)], 0.0450893 secs] [Times: user=0.23 sys=0.00, real=0.05 secs] 
******************* i:138816
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7066K->7046K(7168K)] 9114K->9094K(9728K), [Metaspace: 3479K->3479K(1056768K)], 0.0482812 secs] [Times: user=0.19 sys=0.00, real=0.05 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7066K->7048K(7168K)] 9114K->9096K(9728K), [Metaspace: 3512K->3512K(1056768K)], 0.0427306 secs] [Times: user=0.25 sys=0.00, real=0.04 secs] 
[Full GC (Ergonomics) java.lang.OutOfMemoryError: GC overhead limit exceeded
[PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7061K->7050K(7168K)] 9108K->9098K(9728K), [Metaspace: 3525K->3525K(1056768K)], 0.0552589 secs] [Times: user=0.36 sys=0.00, real=0.06 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2047K->0K(2560K)] [ParOldGen: 7065K->964K(7168K)] 9113K->964K(9728K), [Metaspace: 3539K->3539K(1056768K)], 0.0081120 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 2560K, used 61K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 3% used [0x00000000ffd00000,0x00000000ffd0f610,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 7168K, used 964K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 13% used [0x00000000ff600000,0x00000000ff6f1048,0x00000000ffd00000)
 Metaspace       used 3564K, capacity 4504K, committed 4864K, reserved 1056768K
  class space    used 389K, capacity 392K, committed 512K, reserved 1048576K
	at java.lang.Integer.toString(Integer.java:401)
	at java.lang.String.valueOf(String.java:3099)
	at com.example.demo.test01.OOMDemo.main(OOMDemo.java:13)
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
	at java.lang.Integer.toString(Integer.java:401)
	at java.lang.String.valueOf(String.java:3099)
	at com.example.demo.test01.OOMDemo.main(OOMDemo.java:13)
...

从执行效果可以看见,最后的Full GC几乎不能有回收了。

Java.lang.OutOfMemeoryError:Direct buffer memory

内存溢出之直接缓存。
写NIO程序经常使用ByteBuffer来读或者写入数据、这是一种基于通道( Channel)与缓冲区( Buffer)的I/0方式。它可以使Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著地提高性能,因为免了在Java堆和Native堆中来回复制数据。
1.ByteBuffer.allocate(capacity);直接分配JVM堆内存,属于GC管辖范围,由于需要拷贝所以速度相对较慢。 2.ByteBuffer.allocateDirect(capacity);分配OS本地内存,不属于GC管辖,由于不需要内存拷贝所以速度相对较快。但如果不断分配本地内存,JVM堆内存很少使用,那么JVM就不需要执行GC,DirectByteBuffer对象就不会被回收。这时候堆内存充足,但本地内存可能已经使用光了,再次尝试分配本地内存就会出现OutOfMemoryError ,那程序直接就奔溃了。

案例

代码:

/**
 * 配置参数:-Xms10m -Xmx10m -XX:MaxDirectMemorySize=5m -XX:+PrintGCDetails
 * 故障现象:Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
 * 写NIO程序经常使用ByteBuffer来读或者写入数据、这是一种基于通道( Channel)与缓冲区( Buffer)的I/0方式。它可以使Native函数库直接分配堆外内
 * 存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这
 * 样能在一些场景中显著地提高性能,因为免了在Java堆和Native堆中来回复制数据。
 * 1.ByteBuffer.allocate(capacity);直接分配JVM堆内存,属于GC管辖范围,由于需要拷贝
 * 所以速度相对较慢。
 * 2.ByteBuffer.allocateDirect(capacity);分配OS本地内存,不属于GC管辖,由于不需要内
 * 存拷贝所以速度相对较快。但如果不断分配本地内存,JVM堆内存很少使用,那么JVM就不需要
 * 执行GC,DirectByteBuffer对象就不会被回收。这时候堆内存充足,但本地内存可能已经使用
 * 光了,再次尝试分配本地内存就会出现OutOfMemoryError
 * ,那程序直接就奔溃了。
 */
public class OOMDemo {

    public static void main(String[] args) throws Exception {
        System.out.println("分配的内存:" + (sun.misc.VM.maxDirectMemory() / (double) 1024 / 1024) + "MB");
        try {
            Thread.sleep(200);
        } catch (Throwable e) {
            e.printStackTrace();
        }
        // 实际为5m 这里分配为6m,故意使坏。
        ByteBuffer allocate = ByteBuffer.allocateDirect(6 * 1024 * 1024);
    }

}

执行效果:

[GC (Allocation Failure) [PSYoungGen: 2048K->504K(2560K)] 2048K->877K(9728K), 0.0008484 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
分配的内存:5.0MB
[GC (System.gc()) [PSYoungGen: 990K->504K(2560K)] 1363K->1021K(9728K), 0.0012199 secs] [Times: user=0.06 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 504K->0K(2560K)] [ParOldGen: 517K->937K(7168K)] 1021K->937K(9728K), [Metaspace: 3298K->3298K(1056768K)], 0.0070385 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
	at java.nio.Bits.reserveMemory(Bits.java:694)
	at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
	at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
	at com.example.demo.test01.OOMDemo.main(OOMDemo.java:29)
Heap
 PSYoungGen      total 2560K, used 61K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 2% used [0x00000000ffd00000,0x00000000ffd0f448,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 7168K, used 937K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 13% used [0x00000000ff600000,0x00000000ff6ea4c8,0x00000000ffd00000)
 Metaspace       used 3329K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 360K, capacity 388K, committed 512K, reserved 1048576K

Java.lang.OutOfMemeoryError:unable to create new native thread

内存溢出之不能创建一个新的原生线程。
高并发请求服务器时,经常出如下异常:Java.lang.OutOfMemeoryError:unable to create new native thread,精确的讲Native thread异常与对应的平台有关。
导致原因:
1.应用创建了太多线程,一个应用进程创建多个线程,超过系统就极限。
2服务器并不允许应用程序创建这么多线程,Linux系统允许单个进程可创建的线程数是1024个。应用创建线程超过这个数量,就会报Java.lang.OutOfMemeoryError:unable to create new native thread。
解决办法:
1.降低应用程序创建线程的数量,分折应用是否真的需便的建这么多线君,如果不是改代码将线程数降到最低。
2.对于有的应用确实需要创建很多线程,远超过inux系统的默认1024个线程的限制可以通过修改Linux服务器配置,扩大Linux默认限制。
注意:linux不一定是1024,具体要看linux服务器相关配置文件,参数:

vim /etc/security/limits.d/20-nproc.conf

我的是4096

案例

将上面代码上传到linux,并使用非root用户登录Linux系统测试行:

public class OOMDemo {

    public static void main(String[] args) throws Exception {
        for (int i = 1; ; i++) {
            System.out.println("================i : " + i);
            new Thread(() -> {
                try {
                    Thread.sleep(Integer.MAX_VALUE);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, String.valueOf(i)).start();
        }
    }

}

执行效果:(注意:我的默认限制是4096,至于为什么没到4096就报异常,是因为系统默认会占一些线程)

...
================i : 3769
================i : 3770
================i : 3771
================i : 3772
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
	at java.lang.Thread.start0(Native Method)
	at java.lang.Thread.start(Thread.java:717)
	at OOMDemo.main(OOMDemo.java:15)
...

服务器级别参数调优

在linux服务器打开配置文件,参数:

vim /etc/security/limits.d/20-nproc.conf

内容如下:

# Default limit for number of user's processes to prevent
# accidental fork bombs.
# See rhbz #432903 for reasoning.

*          soft    nproc     4096 
root       soft    nproc     unlimited

可以看到root用户不做限制unlimited,其它用户4096,如果想设置自己用户的单个线程大小,可添加下面内容,sire用户的单个最大限制线程是3000:

# Default limit for number of user's processes to prevent
# accidental fork bombs.
# See rhbz #432903 for reasoning.

*          soft    nproc     4096
root       soft    nproc     unlimited
sire       sotf    nproc     3000

Java.lang.OutOfMemeoryError:Metaspace

内存溢出之元空间。
VM参数设置:-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=8m Jova8及之后的版本使用Metaspace来代替永久代。Metaspace是方法区在HotSpot中的实现,与持久代最大的区別在于: Metaspace并不在虚拟机内而是使用本地内存。也既在Java8中,class metadata(the virtual machines internal presentation of Java class),被存在叫做Metaspace的native memory里,永久代(jova8后被元空间Metaspace取代了)存放ア以下信息:虚拟机加载的类信息、常量池、静态交量、即时编译后的代码。
模拟Metaspace元空间溢出,不断生成类往元空间灌,类占据的空间总是会超过Metaspace指定的空间大小的。
使用Java -XX:+PrintFlagsInitial命令查看本机的初始化参数,-XX:MetaspaceSize为21810376B(约20M)。

案例

VM参数设置:

-XX:MetaspaceSize=18m -XX:MaxMetaspaceSize=18m

代码:

/**
 *VM参数设置:-XX:MetaspaceSize=18m -XX:MaxMetaspaceSize=18m
 * Jova8及之后的版本使用Metaspace来代替永久代。Metaspace是方法区在HotSpot中的实现,与持久代最大的区
 * 別在于: Metaspace并不在虚拟机内而是使用本地内存。也既在Java8中,class metadata(t
 * he virtual machines internal presentation of Java class),被存在叫做Metaspace
 * 的native memory里,永久代(jova8后被元空间Metaspace取代了)存放ア以下信息:虚拟机
 * 加载的类信息、常量池、静态交量、即时编译后的代码。
 * 模拟Metaspace元空间溢出,不断生成类往元空间灌,类占据的空间总是会超过Metaspace指
 * 定的空间大小的。
 */
public class OOMDemo {

    static class OOMClass{}

    public static void main(String[] args) throws Exception {
        int i = 0;
        try{
            while (true){
                i++;
                Enhancer enhancer = new Enhancer();
                enhancer.setSuperclass(OOMClass.class);
                enhancer.setUseCache(false);
                enhancer.setCallback(new MethodInterceptor() {
                    @Override
                    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                        return methodProxy.invokeSuper(o, args);
                    }
                });
                enhancer.create();
            }
        }catch (Throwable e){
            System.out.println("===============多少次后发生异常 : " + i);
            e.printStackTrace();
        }
    }

}

执行效果:

===============多少次后发生异常 : 1519
java.lang.OutOfMemoryError: Metaspace
	at org.springframework.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:530)
	at org.springframework.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:363)
	at org.springframework.cglib.proxy.Enhancer.generate(Enhancer.java:582)
	at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:131)
	at org.springframework.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:319)
	at org.springframework.cglib.proxy.Enhancer.createHelper(Enhancer.java:569)
	at org.springframework.cglib.proxy.Enhancer.create(Enhancer.java:384)
	at com.example.demo.test01.OOMDemo.main(OOMDemo.java:37)