JVM学习笔记(3)---OutOfMemory详解

作者: 18587a1108f1 | 来源:发表于2018-08-28 11:23 被阅读12次

堆溢出

之前说过,堆中主要存储的是对象实例。
所以如果不断创建对象,并保证GC Roots(之后会说明)到对象间有可达路径来避免垃圾回收机制清除这些对象,就会在对象数量达到堆的容量限制后产生内存溢出。

异常示例代码如下:
/**
 * 堆溢出
 * VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
 * -Xms设置堆的最小值 -Xmx设置堆的最大值
 *
 */
public class HeapOOM {
    static class OOMObject {

    }

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

}

通过-Xms和-Xmx设置Vm最小、最大堆的值为20m(将最大最小值设为一样可以避免堆的自动扩展)

栈溢出

HotSpot虚拟机不区分虚拟机栈和本地方法栈,所以这里统称为栈溢出
之前已经讨论过,虚拟机栈会抛出两种异常,StackOverflowErrorOutOfMemoryError 异常。(见JVM学习笔记(2)---Java内存区域

Java虚拟机规范中,对该区域规定了两种异常:

  • StackOverFlowError:线程请求的栈深度大于虚拟机允许的栈深度
  • OverOfMemoryError:动态扩展的线程无法申请到足够的内存

简单理解来看:

  1. 对于前者而言,是由于增加过大栈帧深度或限制虚拟机栈内存产生。
    单线程中,我们不断增大栈帧中本地变量表的长度(如定义大量的本地变量),或者限制栈内存容量(通过Vm启动参数),都输出StackOverFlowError
  2. 对于后者而言,是由于不断建立线程产生。
    这样产生的 OutOfMemoryError 与栈空间大小不存在关系。在内存总量一定时,每个线程的栈分配的内存越大,则越容易产生内存溢出。
异常示例代码如下:
/**
 * 栈溢出 OutOfMemoryError
 * VM Args:-Xss2m
 * -Xss设置栈大小 来减小栈容量
 * 运行本代码前请先保存当前电脑环境,可能假死
 */
public class StackOOM {
    public int threadCount = 0;
    public void addThread() {
        while (true) {
            Thread thread = new Thread(new Runnable() {
                public void run() {
                    while (true) {
                    }
                }
            });
            thread.start();
        }
    }
    public static void main(String[] args){
        StackOOM stackOOM = new StackOOM();
        stackOOM.addThread();
    }
}

直接内存溢出

在JDK1.4之后,新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以直接使用Native函数库分配堆外内存,通过一个存储在Java堆中的 DirectByteBuffer对象 作为这块内存的引用进行操作。
这样能在一些场景中显著提高性能,避免了在Java堆和Native堆中来回复制数据。

通过建立Unsafe实例,可以申请分配直接内存。通过MaxDirectMemorySize参数,可以指定直接内存容量,当申请的直接内存过大时,会出现直接内存溢出。

异常示例代码如下:
/**
 * Vm Args: -Xmx20M -xx:MaxDirectMemorySize=10M
 *
 */
public class DirectMemoryOOM {
    private static final int _1MB = 1024*1024;

    public static void main(String[] args)throws Exception{
        //通过反射获取Unsafe实例,来申请内存分配
        Field unsafeFiled = Unsafe.class.getDeclaredFields()[0];
        unsafeFiled.setAccessible(true);
        Unsafe unsafe = (Unsafe)unsafeFiled.get(null);
        while (true){
            unsafe.allocateMemory(_1MB);
        }
    }
}

元空间溢出

我们已知,方法区中主要存放的是一些描述性信息,即元数据元空间是方法区的一种实现方式。
(注意,方法区是一种规范元空间 和 永久代 都是一种实现

在JDK1.8之前,使用 永久代(PermGen)来实现方法区,但有以下问题:

  • 永久代内存经常不够用或发生内存泄露,爆出异常 OutOfMemoryError: PermGen
  • 动态类加载的情况越来越多,这块内存我们变得不太可控。
  • 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
  • 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

在jdk8之后,用元空间(MetaSpace)替代。 元空间是和本地内存相关的。默认上限大小是本地内存。

元空间与永久代之间 最大区别 在于:
元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。

异常代码如下:
import net.sf.cglib.beans.BeanGenerator;
import net.sf.cglib.beans.BeanMap;

import java.util.*;

/**
 * VM args: -XX:MaxMetaspaceSize=1M -XX:+PrintGCDetails
 */
public class MetaSpaceOOM {
    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 80000; i++) {
            //动态创建类
            Map<Object, Object> propertyMap = new HashMap<Object, Object>();
            propertyMap.put("id", Class.forName("java.lang.Integer"));
            //建立一个动态生成bean
            CglibBean bean = new CglibBean(propertyMap);
            //给 Bean 设置值
            bean.setValue("id", new Random().nextInt(100));
            //打印 Bean的属性id
            System.out.println("num=" + i + "  id=" + bean.getValue("id"));
        }
    }

    static class CglibBean {
        /**
         * 实体Object
         */
        public Object object = null;

        /**
         * 属性map
         */
        public BeanMap beanMap = null;

        public CglibBean() {
            super();
        }

        @SuppressWarnings("unchecked")
        public CglibBean(Map propertyMap) {
            this.object = generateBean(propertyMap);
            this.beanMap = BeanMap.create(this.object);
        }

        /**
         * 给bean属性赋值
         * @param property 属性名
         * @param value 值
         */
        public void setValue(String property, Object value) {
            beanMap.put(property, value);
        }

        /**
         * 通过属性名得到属性值
         * @param property 属性名
         * @return 值
         */
        public Object getValue(String property) {
            return beanMap.get(property);
        }

        /**
         * 得到该实体bean对象
         * @return
         */
        public Object getObject() {
            return this.object;
        }

        @SuppressWarnings("unchecked")
        private Object generateBean(Map propertyMap) {
            BeanGenerator generator = new BeanGenerator();
            Set keySet = propertyMap.keySet();
            for (Iterator i = keySet.iterator(); i.hasNext();) {
                String key = (String) i.next();
                generator.addProperty(key, (Class) propertyMap.get(key));
            }
            return generator.create();
        }
    }

}

由于代码循环创建class,大量的class元数据存放在元数据区,超过了设置的1M空间,因此报元数据区OOM:


运行结果

解决办法也很简单,去掉MaxMetaspaceSize 限制 即可。

在JDK1.8.1之后,错误提示不再报OOM,改为 MaxMetaspaceSize is too small

运行结果

参考与推荐

OOM情况分析
Java Heap dump文件分析工具jhat简介
JVM调优命令-jmap
内存分析工具MAT(Memory Analyzer Tool)从安装到使用
Java was started but returned exit code=13 问题解决

相关文章

网友评论

    本文标题:JVM学习笔记(3)---OutOfMemory详解

    本文链接:https://www.haomeiwen.com/subject/vnspiftx.html