在JVM内存区域中,除了程序计数器外,其他内存区域都有可能发生OOM异常,下面我们来一一模拟每个内存区域OOM异常的场景。
先介绍几个JVM参数:
堆中存放的是对象和数组,只要不断的创建对象或数组,堆就会溢出。
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
错误。
虚拟机栈这个区域会出现两种异常状况:
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连发表遗言的机会都没有。这个信息,只能在操作系统日志里查找。
更多精彩内容关注本人公众号:架构师升级之路