JVM内存区域与内存溢出异常

你离开我真会死。 提交于 2019-12-11 03:46:43

序言:在java应用中,内存溢出是很常见的问题.只有了解内存区域的分布和各自负责的功能,我们才能在编写代码时注意编码质量,并且在发生溢出异常的时候能够根据异常提升快速找到溢出原因。

 

1:JVM虚拟机所管理的内存包括以下几个区域,如图所示:

 1.1 Java堆内存

对于我们应用环境来说,堆内存的占比是Jvm所管理内存是最大的。该内存是被所有线程所共享的,在虚拟机启动的时候被分配。

应用程序中大部分的对象的实例和数组都会在堆上被创建(涉及JIT,对象逃逸技术)。这一块也是垃圾回收器管理的主要区域(后续会在垃圾回收机制中描述)

1.2 方法区

方法区和java堆一样,也是被线程所共享的,该区域主要存放的是虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码(这是一种规范)。不论是jdk1.6-jdk1.7中的永久代还是jdk1.8的元空间其实都是方法区这个规范的具体实现(针对的是Sun HotSpot虚拟机来说)。下面是HotSpot对于

  1. 常量池从方法区移到堆中(jdk1.7开始)
  2. 取消了永久代,使用元空间(Metaspace)替代(1.8开始)
  3. 那么对应的虚拟机加载的类信息(class matadata)也从1.7的永久代中转移到了元空间中
  4. 永久代中的静态变量转移到堆内存中(jdk1.8开始)
  5. 永久代参数PermSize(设置方法区初始值大小)和 MaxPermSize(设置方法区最大内存大小) 替换为元空间参数MetaspaceSize(设置元空间初始值大小)和 MaxMetaspaceSiz(设置方法区最大内存大小)(jdk1.8开始)

为何oracle的sun团队从1.8开始舍弃永久代而启用元空间。原因在于:

  1. 每一个类被加载的时候都将自己类元数据加载到永久代中,而永久代的内存空间是有大小限制的(元空间它使用的是本地内,它的内存瓶颈只会是机器本身可分配的最大内存),一旦需要记载类过多,可能会存在内存溢出的风险;
  2. 类及方法的信息难以确定其大小,所以对于永久代的大小指定比较困难,太小容易出现永久代内存溢出,太大则容易导致老年代(小了)溢出。
  3. HotSpot虚拟机的每种类型的垃圾回收器都需要特殊处理永久代中的元数据。永久代会为GC带来不必要的复杂度,并且回收效率偏低

1.3 Java虚拟机栈

虚拟机栈是被线程所私有的,它的生命周期和线程相同(创建线程同时会创建与之相对应的虚拟机栈)。虚拟机栈为Java方法(也就是字节码)的执行提供支持:在每个方法被执行的时候会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息,每一个方法从调用到结束,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

局部变量表:是一组变量值存储空间,用于存放方法参数和方法定义的局部变量。例如编译期就可知的8大基础数据类型(boolean,byte,char,short,int,float,long,double),对象引用(对象本身存储在堆中)和returanAddress类型 (指向了一条字节码指令的地址)。

操作数栈:它是一个先入后出栈,最大深度也在编译时写入Code属性的max_stacks数据项中,里面存储的元素可以是任意Java类型,32位数据类型占栈容量为,64位数据类型所占栈容量位2。当一个方法刚执行的时候该栈位空,在方法的执行过程中会有各种字节码指令往操作数栈中写入和提取。列入在做算数运算的时候是通过操作数栈来进行的;或者在调用其它方法的时候也是通过操作数

方法出口信息(方法返回地址):当一个方法执行只有两种可以结束,第一种当执行引擎遇到任意一个方法返回的字节码指令这种为正常完成出口,根据遇到字节码指令不同确定是包含返回值。第二种当方法执行中遇到异常,并且异常在方法体中未得到异常处理,这种退出方式为异常完成出口。

1.4 本地方法栈

与虚拟机栈的功能很相似,也是线程私有的,只不过Java虚拟机栈为虚拟机执行方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。

被native关键字修饰的方法一般称为本地方法,该方法的实现由非java语言实现。例如在某java应用程序出有某个需求需要调用底层硬件,而java是无法做到,这个时候我们一般使用c语言来实现底层的调用,再通过native关键字表明使该程序可以在java中使用(其实java源码中本身就存在大量调用本地方法,有兴趣可以了解下sun.misc.Unsafe源码)。

Hotspot虚拟机将本地方法栈和虚拟机方法栈合二为一。

1.5程序计数器

和虚拟机栈一样,程序计算器也是线程私有的。它本身是一快很小的内存,用于记录着当前线程所执行的字节码(方法)的行号.

java中的多线程的执行是轮流切换并分配cpu的执行时间来实现的,那么在任何一个时刻一个cpu( 如果是多核处理器指的是一个内核)只会执行任意一个线程中代码,为了保证线程切换后能够恢复到正确的执行位置,每个线程都需要一个独立的组件去记录当前线程执行的位置,这个组件就是程序计数器。

如果线程执行的是一个java方法,那么计数器记录的是正在执行的虚拟机字节码指令的地址,如果执行的是一个Native方法那么这个计数器为空(Undefined).

1.6直接内存

直接内存并不是JVM管理的内存区域, 它实际指定的jvm运行环境中的机器内存。我们可以通过Native方法直接操作于此块区域。

2:内存溢出异常

2.1 java堆内存溢出

java中对象实例的存储在堆中,如果不断的创建对象,并垃圾回收器无法回收清除这些对象时(可参考垃圾回收机制)。当堆中内存总量达到设定阀值时,就会产生内存溢出异常。如代码所示:

   public static void main(String[] args) {

        List<byte[]> datas = new ArrayList<>();

        while(true){
            datas.add(new byte[1024*1024]);
        }
}

通过参数设置堆内存大小:-Xms10m(最小堆内存数) -Xmx10m(最大堆内存数)。尽量使最大内存数与最小内存数一致避免java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小而浪费资源。在测试项目稳定性时。可通过设置-XX:+HeapDumpOnOfMemoryError使当jvm方式内存溢出是Dump出当前内存堆转储快照帮助我们发现内存溢出点,完善代码。

2.2虚拟机栈和本地方法栈溢出

虚拟机栈和本地方法栈会产生两种异常

  1. 如果线程请求的栈深度大于虚拟机所容许的最大深度,此时就会抛出StackOverflowError异常。(无法评估的方法递归递归)很容易产生此异常
  2. 如果虚拟机在扩展栈时无法申请到足够的内存空间时,就会抛出OutOfMemoryError异常。

测试代码:

1:对于StackOverflowError异常非常容易产生,进行无终止的递归方法调用就可以产生

    //方法递归
    public static void test(){
        test();
    }

    public static void main(String[] args) {
        test();
    }

2:对于虚拟机栈产生OutOfMemoryError异常

/**
 * 测试虚拟机栈尝试OOM异常
 * 此代码会导致机器死机 在使用之前先保存当前工作 不然易丢失数据
 * -Xss=10m 设置 Java 线程堆栈大小
 * @author  fangyuan
 */
public class JavaStackOOM {

    private void dontStop(){
        while (true){

        }
    }

    public void test(){

        //不断创建线程 每创建一个线程都会创建与之对应的虚拟机栈
        while(true){
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    dontStop();
                }
            });
            thread.start();
        }
    }

    public static void main(String[] args) {
        new JavaStackOOM().test();
    }
}

运行结果:Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread;

因为Hotspot虚拟机将虚拟机栈和本地方法栈合并在一起,-Xoss参数(设置本地方法栈大小)已无用。对于栈容量只能通过-Xss参数设置。

2.3方法区内存溢出

测试代码:

/**
 * 通过cglib动态产生大量的类 导致方法区内存溢出
 *-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m jdk1.8
 *-XX:PermSize=10m -XX:MaxPermSize=10m jdk1.7
 * @author fangyuan
 */
public class MethodMemoryErrorTest {

    public static void main(String[] args) {

        while (true) {

            Enhancer enhancer = new Enhancer();

            enhancer.setSuperclass(Test.class);

            enhancer.setUseCache(false);

            enhancer.setCallback(new MethodInterceptor() {
                @Override
                public Object intercept(Object obj, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                    return methodProxy.invokeSuper(obj, args);
                }
            });
            enhancer.create();
        }

    }

    /**
     * 测试类
     */
    static class Test {}
}

2.3.1: 对于jdk1.7之前是永久代内存溢出:

 

2.3.2:对于jdk1.8之后是元空间内存溢出:

2.4直接内存溢出

我们可以通过-XX:MaxDirectMemorySize指定。如果不指定默和Java堆最大值(-Xmx指定)一样。

测试代码(代码中使用Unsafe分配直接内存,此类非常有用 详细可参考这篇博客 :Java中Unsafe类):

/**
 * 这里通过调用native方法直接操作直接内存
 * -XX:MaxDirectMemorySize=20m 设置直接内存使用最大数为50m 快速产生效果
 * @author fangyuan
 */
public class NativeMemoryOOM {

    private static final int _1MB = 1024 *1024;

    public static void main(String[] args) throws Exception {

        Field field =Unsafe.class.getDeclaredFields()[0];

        field.setAccessible(true);

        Unsafe unsafe =  (Unsafe)field.get(null);

        while(true){
            unsafe.allocateMemory(_1MB);
        }

    }
}

借鉴:周志明老师的<深入理解Java虚拟机>

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!