JVM系列之内存结构与内存溢出异常

概述

对于所有的java开发人员来说,了解和熟悉JVM的内存结构是非常重要的,无论是对于开发时的一些参数说的设置还是维护时的异常排查都有很大的帮助,下面我们先来了解下JVM内存结构

内存结构

从JDK1.8开始,JVM内存结构发生一点变化

JDK1.8的JVM内存结构图

JDK1.8

JDK1.8之前的JVM内存结构图

JDK1.7

从上面两图中我们可以发现,内存结构确实有点变化,但其实变化其实很小:

  • 可以发现就是JDK1.8之后去掉了方法区,取而代之的是MetaSpace区域;
  • 将运行常量池从方法区中移到了堆中.
    接下来我们就来一一分析一下每一个结构区(这里我们以JDK1.8之前的来主要分析)

程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,主要可以看作当前线程所执行的字节码的行号指示器。它会指示出下一条将要执行指令的地址。

  • 线程私有的内存;
  • 如果线程正在执行的是一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;
  • 如果正在执行的一个Native方法,这个计数器则会为空;
  • 唯一一个再java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

Java虚拟机栈

  • 线程私有的,生命周期与线程相同
  • 虚拟机栈描述的java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧用于存储局部变量表、操作栈、动态链接、方法出口等信息。
  • 局部变量表中存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,他不等同于对象本身)和returnAddress类型(指向了一条字节码指令的地址);
  • Java虚拟机规范中,对这个区域规定了两种异常情况:如果线程请求的栈深度大于虚拟机所允许的栈深度,将抛出StackOverflowError异常;但是一般情况下现在都允许动态扩展,当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。
  • 如果单线程工作环境下,调用某一个方法并在该方法中递归调用自己,会抛出StackOverFlowError,如以下代码
// VM Args: -Xss128k (将虚拟机栈设为128k大小)
public class Test {
    public int length = 0;
    public void stackLeak() {
        length++;
        stackLeak();
    }
    //运行下本代码会出先StackOverflowError
    public static void main(String[] args) {
        Test test = new Test();
        try {
            test.stackLeak();
        } catch(Throwable e) {
            System.out.println("stack length:" + test.length);
            throw e;
        }
    }
}
  • 想要出现OutOfMemoryError的话需要在多线程的环境下,不停的开启线程,才有可能会出现OutOfMemoryError异常,如以下代码
// VM Args:-Xss2M
public class JavaVMStackOOM {
    private void dontStop() {
        while (true) {
            
        }
    }
    //运行本代码会出现OutOfMemoryError异常,但是运行此代码可能会导致Windows直接挂了,所以得注意,记得保存自己手头的工作再决定是不是运行
    public static void main(String[] args) {
        while(true) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    dontStop();
                }
            });
            thread.start();
        }
    }
}

本地方法栈

  • 本地方法栈与虚拟机栈发挥的作用非常相似,一个是为执行java方法服务的,一个是为执行native方法服务的。
  • 本地方法栈也可能抛出StackOverflowError和OutOfMemoryError异常

Java堆

  • 对于大多数应用来说,java堆是java虚拟机所管理的内存中最大的一块
  • java堆是所有线程共享的一块内存,在虚拟机创建时启动
  • 几乎所有的对象实例都放在java堆中(jdk1.8之后运行时常量池也放到java堆中了)
  • java堆是垃圾收集器管理的主要区域,可以分为新生代和老年代,而新生代又可以分为Eden区、From Survivor区、To Survivor区等
  • Java堆可以通过Xmx(最大堆大小)和Xms(最小堆大小)两个参数来指定堆的大小
  • Java堆可以处理物理上不连续的内存空间中,只要逻辑上是连续的即可
  • 如果在Java堆中没存完成实例分配,并且堆也无法进行扩展时,将会抛出OutOfMemoryError异常
  • 可想而知,如果我们需要测试Java堆的OutOfMemoryError异常的话,只需要在堆上不停的分配对象实例就可以了,如下代码
//VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
public class HeapOOM {
    static class OOMObject {
        
    }
    //运行这段代码会出现OutOfMemoryError异常
    public static void main(String[] args) {
        List list = new ArrayList<>();
        while (true) {
            list.add(new OOMObject());
        }
    }
}

方法区

  • 与Java堆一样,是所有线程共享的内存区域;
  • 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据(也会被叫做永久代【Permanent Generation】)。
  • jdk1.8之前运行时常量池是在方法区分配的,1.8之后方法区被取消了,然后新增了一个元数据区,同时也将运行时常量池放到了堆中
  • 根据java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
  • 下面展示一下如何测试出方法区的OutOfMemoryError异常
//VM args : -XX:PermSize=10M -XX:MaxPermSize=10M
public class JavaMethodAreaOOM {
    public static void main(String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(OOMObject.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                    return proxy.invokeSuper(obj, args);
                }
            });
            enhancer.create();
        }
    }
}

运行时常量池

  • 在我们上面提到的,运行时常量池时方法区的一部分,用于存放编译期生成的各种字面量和符号引用
  • 在运行期间将新的常量放进常量池可以通过String类的intern()方法;
  • 运行时常量池无法再申请到内存会抛出OutOfMemoryError异常;
  • 所以为了测试运行常量池的OutOfMemoryError异常,我们应该只需要一直往常量池里面放常量,按理来说应该就会出现该异常了,如下代码所示:
//VM args : -XX:PermSize=10m -XX:MaxPermSize=10m
public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        List list = new ArrayList();
        int i = 0;
        while (true) {
            list.add(String.valueOf(i++).intern());
        }
    }
}

直接内存

  • 直接内存不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也会被频繁使用,而且也可能导致OutOfMemoryError异常的出现

对象访问

  • Java中对象访问方式主要有两种:句柄访问和直接指针
  • 使用句柄访问方式,Java堆中会划分出一块内存来作为句柄池,reference中存储的是对象的句柄地址,而句柄中包含了对象实例数据和类型数据的各自的具体地址信息,如下图所示:
句柄访问
  • 使用直接指针方式,Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference中直接存储的就是对象的地址,如下图所示:
直接方式
  • 这两种对象的访问方式各有优势,使用句柄访问方式的最大好处就是reference中存的是对象句柄的地址,当对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要被修改;使用直接指针访问的方式最大好处就是快,因为少了一次寻址的过程,由于对象访问在Java中是非常频繁的,所有这类开销积少成多也是非常可观的执行成本。Sun HotSpot虚拟机就是采用的直接指针的方式来访问对象的。

总结

本文主要总结了下JVM的内存结构以及每个结构区相应的存放内容和溢出异常,很多只是借鉴于《深入理解Java虚拟机++JVM高级特性与最佳实践这本书》,如果想更好的了解的朋友也可以去看看这本书,写的很好很详细!

你可能感兴趣的:(JVM系列之内存结构与内存溢出异常)