通常谈到JVM的内存模型,一般人会想到堆和栈等,那么堆和栈如何理解呢?

栈是运行时的单位;

堆是存储的单位。

通俗来说栈解决的是程序如何运行,数据如何处理的问题;而堆解决的是数据如何存储,存储在哪的问题。

JMM

如上图所示,java虚拟机内存模型主要分为以上五个部分,这里以java8为学习对象。

一、本地方法栈 (Native Method Stacks)

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的。其区别在于虚拟机栈为虚拟机执行Java方法所服务,而本地方法栈则是为虚拟机使用到的native方法所服务。

本地方法栈也是一个私有(线程私有)的内存区域,也是后进先出。

虚拟机可以自由实现它,有的虚拟机(如HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。

本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常:

二、虚拟机栈 (Java Virtual Machine Stacks)

每个Java线程都有一个私有Java虚拟机栈,与该线程同时创建。

在虚拟机栈内,每个方法会生成一个栈帧。每个栈帧代表一次次的方法调用,一个方法的执行到执行完成的过程,代表栈帧从入栈到出栈的过程。

虚拟机栈会抛出StackOverflowError和OutOfMemoryError。

2.1 栈帧结构

下图表示了栈帧的组成结构:

栈帧

2.1.1 局部变量表

局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。

具体数据类型的内容参考:https://www.jianshu.com/p/f46e02173552

2.1.2 操作数栈

操作数栈是一个后入先出的栈。

一个方法刚开始执行时操作数栈是空的,方法执行过程中会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈 / 入栈操作。

例如执行iadd指令时,就会将最接近栈顶的两个int元素取出并相加,然后将相加的结果再入栈。

操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,在编译程序代码的时候,编译器要严格保证这一点。比如刚才的iadd指令,它取出的元素必须是int的,不能出现诸如long和float类型的变量。

虽然概念模型中不同栈帧之间是完全相互独立的,但大多虚拟机实现中会有一些优化处理:令两个栈帧出现一部分重叠,让下面的栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起重叠在一起,这样在进行方法调用时就可以公用一部分数据,无须进行额外的参数复制传递,如图所示:

重叠

2.1.3 动态链接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。

静态解析:我们知道Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数,这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化称为静态解析。

动态链接:出去静态解析的另外一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。

所以要执行某个方法时,某个指令(例如invokevirtual)将常量池中的引用作为参数,而根据这个引用就可以找到真正的栈帧。

2.1.4 方法出口

方法出口也可以通俗的理解为方法返回方式:在jvm中,方法返回方式有两种:正常和异常。

正常出口:当程序执行遇到方法返回的字节码指令,就完成此次方法执行,并根据调用方指定的返回值去返回(可以无返回值)。

异常出口:方法在执行中遇到了异常,并且在方法体内没有得到处理,会导致方法退出,这时候不会有任何返回值给调用方。

一个方法退出时需要返回到其被调用的位置,上层调用方法才能继续执行。

程序正常退出时,相当于把当前栈帧出栈,调用pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。

程序异常退出时:当程序发生异常时,返回地址需要通过异常表来确定,在栈帧中没有保存异常表。

2.2 虚拟机栈与本地方法栈的关系

为了更好地理解虚拟机栈和本地方法栈的结构模型以及关系,我们以晚上很多的例子简单标书下,如下图:

虚拟机栈与本地方法栈的相互调用

三、寄存器 (The pc Register)

Java虚拟机可以支持多个线程同时执行,每个Java线程都有其自己的 pc(程序计数器)寄存器。在任何时候,每个Java虚拟机线程都在执行单个方法的代码,即该线程的当前方法。(如果不是native,则该pc寄存器包含当前正在执行的Java虚拟机指令的地址。如果线程当前正在执行的方法是native,则Java虚拟机的pc寄存器值未定义。

pc寄存器中的值就是当前指令所在的内存地址,即returnAddress类型的数据,当线程执行native方法时,pc中的值为undefined。

四、方法区 (Method Area)

Java虚拟机具有一个在所有Java虚拟机线程之间共享的方法区域。该方法区域类似于常规语言的编译代码的存储区域,或者类似于操作系统过程中的“文本”段。它存储每个类的结构,例如运行时常量池,字段和方法数据,以及方法和构造函数的代码,包括用于类和实例初始化以及接口初始化的特殊方法。

方法区是在虚拟机启动时创建的。尽管方法区在逻辑上是堆的一部分,但是可以选择不进行垃圾回收或压缩。该规范没有规定方法区域的位置或用于管理已编译代码的策略。方法区域可以是固定大小的,或者可以根据计算的需要进行扩展,如果不需要更大的方法区域,则可以缩小。方法区域的内存不必是连续的。

可能抛出OutOfMemoryError异常。

五、堆 (Heap)

Java虚拟机具有一个在所有Java虚拟机线程之间共享的堆。堆是运行时数据区,从中分配所有类实例和数组的内存。

堆是在虚拟机启动时创建的。对象的堆存储由GC(垃圾收集器)回收;对象永远不会显式释放。Java虚拟机可以根据实现者的系统要求选择GC。堆的大小可以是固定的,也可以根据计算要求进行扩展,如果不需要更大的堆,则可以将其收缩。堆的内存不必是连续的。

可能抛出OutOfMemoryError异常。

在jdk1.8之前的版本堆内存空间是不同的,主要却别在于:1.8中删除了永久代,新增了元空间。

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过参数来指定元空间的大小。

堆内存

jvm中的常量池