欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JVM知识点总结(三):OOM

程序员文章站 2022-04-26 22:58:19
...

常见OOM实测

by Kay 2017.9.11

OOM(OutOfMemoryError)是虚拟机内存中比较常见的错误,通过几个例子重现几种比较常见的OOM场景,来验证JVM的内存结构。

首先要明确一点的是,GC回收的时,会回收哪些对象?

通常,虚拟机通过根搜索算法,来找出存活的对象。基本思想就是通过“GC Roots”的对象作为起点向下寻找,当一个对象没有在一个GC Roots的寻找路径上时,也就是通过GC Roots引用链连接不到它时,那么这个对象就是不可达的,此时这个对象被断定为可以回收的对象。

在Java中,可以作为GC Roots的对象分为以下几种:
- Java栈中(栈帧中的本地变量表)引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(Java Native Invoke,也就是native方法)引用的对象

了解以上基础来看一下常见的OOM:

Java堆上的OOM

我们都知道,Java堆中主要存放的是对象的实例,也就是说如果我们不断的添加新的对象,并且不被GC回收,那就会出现堆上的OOM

package oom;
import java.util.ArrayList;
import java.util.List;

/**
 * Created by kay on 2017/9/11.
 * 测试堆上的 OOM
 */
public class Demo1 {

    static class HeapOOM{}

    public static void main(String[] args) {
        List<HeapOOM> list = new ArrayList<>();
        while (true) {
            list.add(new HeapOOM());
        }
    }
}

为了让上述代码加快OOM的发生,将虚拟机堆的大小限制在20M,通过指令:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 打印对象堆栈信息和限制堆的大小:
-Xms20M :堆最小20M
-Xmx20M:堆最大20M(最大与最小设置为一样,避免堆的自动扩容)
-Xmn10M:设置新生代的大小为10M(那么老年代就是20-10=10M)
-XX:SurvivorRatio=8 :设置新生代中Eden区与survivor区的大小比例为8:1(注意这里是Eden与一个幸存者区的比例,那么Eden:from:to=8:1:1)

使用IDEA配置VM参数如下:
JVM知识点总结(三):OOM

配置好之后,运行上面的代码,很快就会出现OOM的错误:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Arrays.java:3210)
    at java.util.Arrays.copyOf(Arrays.java:3181)
    at java.util.ArrayList.grow(ArrayList.java:261)
    at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
    at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
    at java.util.ArrayList.add(ArrayList.java:458)
[Full GC (Ergonomics) [PSYoungGen: 8192K->0K(9216K)] [ParOldGen: 8325K->639K(10240K)] 16517K->639K(19456K), [Metaspace: 3522K->3522K(1056768K)], 0.0064923 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    at oom.Demo1.main(Demo1.java:16)

Java heap space发生了错误。

Java栈和本地方法栈的溢出

我们使用的HotSpot并不区分Java栈和本地方法栈,通过-Xss参数来设置栈空间的大小。

package oom;

/**
 * Created by kay on 2017/9/11.
 *
 * 测试 *Error
 *  参数:-Xss128k
 */
public class Demo2 {

    private int i=1;

    public void method() {
        i++;
        method();
    }

    public static void main(String[] args) {
        Demo2 demo2 = new Demo2();
        demo2.method();
    }
}

很快就会得到Exception in thread "main" java.lang.*Error的错误,栈空间溢出。

在《深入理解Java虚拟机:JVM高级特性与最佳实践》一书中说道:
关于Java栈与本地方法栈,Java虚拟机描述了两种异常:
1. 如果线程请求的栈深度大于虚拟机所允许的最大深度,则抛出*Error(类似于上面代码测试的情况)
2. 如果虚拟机在扩展栈时无法申请到足够的内存,则抛出OutOfMemoryError

对于第二种该怎么理解呢?
由于Java栈和本地方法栈时线程私有的,也就是说在多线程的情况下,虚拟机为每个线程单独分配一个Java栈空间,如果线程太多,内存不够分配了,那么这个时候就会发生OutOfMemoryError:unable to create native thread,有趣的是,当发生这种情况时,可以通过减小堆内存来给栈腾出更多的空间,或是设置更小的栈空来给每个线程这样就能有更多的线程,这种通过“减小”内存的方式来解决内存溢出的方式确实难以想到。

运行时常量池溢出

常量池溢出的情况比较好实现,就是不断往常量池中添加内容来达到,通过String的intern()方法就可以实现。
需要注意的是,在 jdk1.6 和 jdk1.7中可以通过设置
-XX:PermSize=2M -XX:MaxPermSize=2M来设置永久代的大小,但是在jdk 1.8中,永久代已经被彻底移除,如果使用下面的代码,将创建的常量放在元数据区,这个区域是直接内存,理论上来讲,如果不设置元数据的大小,虚拟机会耗尽系统的可用资源。

package oom;

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

/**
 * Created by kay on 2017/9/11.
 * 常量池溢出
 */
public class Demo3 {

    public static void main(String[] args) {
        int i=1;
        List<String> list = new ArrayList<>();
        while (true) {
            list.add(String.valueOf(i++).intern());
        }
    }
}

方法区溢出

方法区主要存放的是Class的相关信息,如类名、字段描述、方法描述、常量池、访问修饰等,对于这个区域,主要是在运行时加载大量的类去填满它就会溢出,这类溢出情况主要会出现在利用字节码生成大量代理类的时候,比如一些框架如Spring、Hibernate对类增强时,利用CGLib字节码技术,如果增强的类越多,那么方法区就会动态生成Class放入。
还要一种情况是,大量的JSP文件生成,因为JSP第一次运行会被编译成Servlet(也是Java类),OSGI。。。这个不太了解,好像是它会用不同的类加载器加载,那也就会有更多不同的类产生。
CGLib没有用过,这里就不写测试了。

直接内存溢出

通过MaxDirectMemorySize指定直接内存大小,在BootstrapClassLoader加载时会rt.jar中的Unsafe类,我们只有通过反射取得这个类,在调用`unsafe.allocateMemory’时会申请分配直接内存。

package oom;
import sun.misc.Unsafe;
import java.lang.reflect.Field;

/**
 * Created by kay on 2017/9/11.
 * -Xmx20M -XX:MaxDirectMemorySize=10M
 */
public class Demo4 {
    public static void main(String[] args) throws IllegalAccessException {
        Field unsafeField= Unsafe.class.getDeclaredFields()[0];
        unsafeField.setAccessible(true);
        Unsafe unsafe =(Unsafe) unsafeField.get(null);
        while (true) {
            unsafe.allocateMemory(1024 * 1024);
        }

    }
}
Exception in thread "main" java.lang.OutOfMemoryError
    at sun.misc.Unsafe.allocateMemory(Native Method)
    at oom.Demo4.main(Demo4.java:16)
相关标签: jvm oom