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大小的数组对象 }
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
运行之后就会抛出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 list = new LinkedList<>();
int i = 0;
while (true) {
i++;
if (0 == i % 1000) {
TimeUnit.MILLISECONDS.sleep(10);
}
list.add(new Object());
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
运行之后就会抛出OOM异常:java.lang.OutOfMemoryError: GC overhead limit exceeded。JVM花费了98%的时间进行垃圾回收,而只得到2%可用的内存,频繁的进行内存回收,JVM就会曝出java.lang.OutOfMemoryError: GC overhead limit exceeded错误。
虚拟机栈溢出
虚拟机栈这个区域会出现两种异常状况:
线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
当虚拟机栈扩展时无法申请到足够的内存时会抛出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();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
运行之后就会抛出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); }
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
运行之后就会抛出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 list = new LinkedList<>();
while (true) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024 * 1024);
list.add(byteBuffer);
}}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
运行之后就会抛出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); }
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
上面的代码在我的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
1
2
3
4
这个现象,其实和Linux的内存管理有关。由于Linux系统采用的是虚拟内存分配方式,JVM的代码、库、堆和栈的使用都会消耗内存,但是申请出来的内存,只要没真正access过,是不算的,因为没有真正为之分配物理页面。
随着使用内存越用越多。第一层防护墙就是SWAP;当SWAP也用的差不多了,会尝试释放cache;当这两者资源都耗尽,杀手就出现了。oom-killer会在系统内存耗尽的情况下跳出来,选择性的干掉一些进程以求释放一点内存。所以这时候我们的Java进程,是操作系统“主动”终结的,JVM连发表遗言的机会都没有。这个信息,只能在操作系统日志里查找。