jvm OOM异常的模拟

x33g5p2x  于2021-12-18 转载在 其他  
字(4.9k)|赞(0)|评价(0)|浏览(425)

OutOfMemoryError异常

在JVM内存区域中,除了程序计数器外,其他内存区域都有可能发生OOM异常,下面我们来一一模拟每个内存区域OOM异常的场景。

先介绍几个JVM参数:

  • -Xms:设置JVM初始堆内存的大小。
  • -Xmx:设置JVM最大堆内存的大小。
  • -Xmn: 设置年轻代的大小、
  • -Xss:设置每个线程对应的栈的大小。
  • -XX:+HeapDumpOnOutOfMemoryError:发生OOM异常时生成heap dump文件
  • -XX:HeapDumpPath=path:heap dump文件生成的路径,例如XX:HeapDumpPath=/var/log/java/java_heapdump.hprof
  • -XX:+PrintGCDetails:打印GC的详细信息。
  • -XX:+PrintGCTimeStamps:打印GC的时间戳。
  • -XX:MetaspaceSize:设置元空间触发垃圾回收的大小。
  • -XX:MaxMetaspaceSize:设置元空间的最大值。

堆溢出

堆中存放的是对象和数组,只要不断的创建对象或数组,堆就会溢出。

package com.morris.jvm.oom;

import java.util.ArrayList;
import java.util.List;

/** * 演示堆的溢出 * VM args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=c:\dump\heap.hprof -XX:+PrintGCDetails -XX:+PrintGCTimeStamps */
public class HeapOOM {

    public static void main(String[] args) {

        List<byte[]> list = new ArrayList<>();

        while (true) {
            list.add(new byte[1024 * 1024]); // 每次增加一个1M大小的数组对象
        }

    }

}

运行之后就会抛出OOM异常:java.lang.OutOfMemoryError: Java heap space

堆中还可能出现下面一种OOM异常:

package com.morris.jvm.oom;

import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/** * VM args: -Xms30m -Xmx30m -XX:PrintGCDetails */
public class HeapOOM2 {

    public static void main(String[] args) throws Exception {
        List<Object> list = new LinkedList<>();
        int i = 0;
        while (true) {
            i++;
            if (0 == i % 1000) {
                TimeUnit.MILLISECONDS.sleep(10);
            }
            list.add(new Object());
        }
    }

}

运行之后就会抛出OOM异常:java.lang.OutOfMemoryError: GC overhead limit exceeded。JVM花费了98%的时间进行垃圾回收,而只得到2%可用的内存,频繁的进行内存回收,JVM就会曝出java.lang.OutOfMemoryError: GC overhead limit exceeded错误。

虚拟机栈溢出

虚拟机栈这个区域会出现两种异常状况:

  1. 线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
  2. 当虚拟机栈扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常(无法重现)。
package com.morris.jvm.oom;

/** * 演示栈的溢出 * VM args:-Xss1m */
public class StackSOE {

	private static int index = 1;

	private static void test() {
		index++;
		test();
	}
	
	public static void main(String[] args) {
		try {
			test();
		}catch (Throwable e){
			System.out.println("Stack deep : "+index);
			e.printStackTrace();
		}
	}

}

运行之后就会抛出OOM异常:java.lang.StackOverflowError

虚拟机参数-Xss在64位机器上默认的大小为1m,栈越大,能够容纳的栈帧就会越多,方法调用的深度就会越深。

方法区溢出

方法区中存放的是类的数据结构,只要不断往方法区中加入新的类,就会产生方法区的溢出,可以使用类加载器不断加载类或者动态代理不断生成类来演示。

我这里使用的是JDK8,方法区的具体实现为元空间,也就是说下面的代码演示的是元空间的溢出。

package com.morris.jvm.oom;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;

/** * 演示元空间的溢出 * VM args:-XX:MetaspaceSize=16m -XX:MaxMetaspaceSize=16m */
public class MetaSpaceOOM {

    public static void main(String[] args) {

        List<ClassLoader> classLoaderList = new ArrayList<>();
        while (true) {
            ClassLoader loader = new URLClassLoader(new URL[]{});
            Facade t = (Facade) Proxy.newProxyInstance(loader, new Class<?>[]{Facade.class}, new MetaspaceFacadeInvocationHandler(new FacadeImpl()));
            classLoaderList.add(loader);
        }
    }

    public interface Facade {
    }

    public static class FacadeImpl implements Facade {
    }

    public static class MetaspaceFacadeInvocationHandler implements InvocationHandler {
        private Object impl;

        public MetaspaceFacadeInvocationHandler(Object impl) {
            this.impl = impl;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            return method.invoke(impl, args);
        }
    }
}

运行之后就会抛出OOM异常:java.lang.OutOfMemoryError: Metaspace

直接内存溢出

严格来说,上面的元空间也是属于直接内存(堆外内存)的。但是我们这里的直接内存指的是Java应用程序通过直接方式从操作系统中申请的内存。

直接内存的容量可以通过-XX:MaxDirectMemorySize来设置(默认与堆内存最大值一样),与元空间是分开来管理的。

package com.morris.jvm.oom;

import java.nio.ByteBuffer;
import java.util.LinkedList;
import java.util.List;

/** * 演示直接内存的溢出 * VM args:-Xmx20M -XX:MaxDirectMemorySize=10M */
public class DirectMemoryOOM {
	
	public static void main(String[] args) throws IllegalArgumentException, IllegalAccessException {
		List<ByteBuffer> list = new LinkedList<>();
		while (true) {
			ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024 * 1024);
			list.add(byteBuffer);
		}

	}
}

运行之后就会抛出OOM异常:java.lang.OutOfMemoryError: Direct buffer memory

注意:-XX:MaxDirectMemorySize只能限制通过DirectByteBuffer申请的内存,而其他堆外内存,如使用了Unsafe或者其他JNI手段直接直接申请的内存是无法限制的。

下面的程序会使用Unsafe不停的申请内存,注意谨慎运行,会使电脑死机。

package com.morris.jvm.oom;

import sun.misc.Unsafe;

import java.lang.reflect.Field;

/** * 演示本地内存的溢出 * VM args: -Xmx20M -XX:MaxDirectMemorySize=10M */
public class LocalMemoryOOM {

    public static void main(String[] args) throws IllegalAccessException {
        Field field = Unsafe.class.getDeclaredFields()[0];
        field.setAccessible(true);

        Unsafe unsafe = (Unsafe) field.get(null);

        while (true) {
            unsafe.allocateMemory(1024 * 1024);
        }
    }
}

上面的代码在我的windows下会抛出java.lang.OutOfMemoryError异常,感觉像是通过-XX:MaxDirectMemorySize参数限制住了,但是在linux下运行会导致堆外内存一直增长,直到机器物理内存爆满,被系统oom killer。

说它的内存增长,是通过top命令去观察的,看它的RES列的数值;反之,如果使用jmap命令去看内存占用,得到的只是堆的大小,只能看到一小块可怜的空间。

上面的代码运行一段时间后会悄悄的退出,那么怎么定位到原因呢?

$ dmesg -T
......
[Wed Jul 22 18:03:56 2020] Out of memory: Kill process 25991 (java) score 632 or sacrifice child
[Wed Jul 22 18:03:56 2020] Killed process 25991 (java) total-vm:1345034596kB, anon-rss:3187820kB, file-rss:144kB, shmem-rss:0kB

这个现象,其实和Linux的内存管理有关。由于Linux系统采用的是虚拟内存分配方式,JVM的代码、库、堆和栈的使用都会消耗内存,但是申请出来的内存,只要没真正access过,是不算的,因为没有真正为之分配物理页面。

随着使用内存越用越多。第一层防护墙就是SWAP;当SWAP也用的差不多了,会尝试释放cache;当这两者资源都耗尽,杀手就出现了。oom-killer会在系统内存耗尽的情况下跳出来,选择性的干掉一些进程以求释放一点内存。所以这时候我们的Java进程,是操作系统“主动”终结的,JVM连发表遗言的机会都没有。这个信息,只能在操作系统日志里查找。

相关文章