jvm OOM异常的模拟(转载)

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连发表遗言的机会都没有。这个信息,只能在操作系统日志里查找。

原文链接:https://blog.csdn.net/u022812849/article/details/107537021?utm_medium=distribute.pc_feed.none-task-blog-personrec_tag-3.nonecase&depth_1-utm_source=distribute.pc_feed.none-task-blog-personrec_tag-3.nonecase&request_id=5f209e1cf769fa6e5f5d46e6

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值