JVM 内存模型

Java 语言在虚拟机 JVM 的帮助下,不需要用户手动释放内存,减少了很多工作,也不容易出现内存泄漏和内存溢出问题。不过正是因为把内存的控制权交给了 JVM,这使得在出现问题后定位问题的难度也增加了。JVM 在运行的时候会把内存分为:

JVM内存模型
JVM内存模型

直接内存:该部分内存不是虚拟机规范和运行时数据区的内存,但是在一些 NIO 操作中会用到。NIO 是基于 Channel 和缓冲区的 IO 方式,可以使用 native 库函数直接分配堆外内存,然后通过存储在堆内的 DirectByteBuffer 对象作为这块内存的引用来进行操作,这样避免了在 Java 堆和 native 堆复制数据,提高了性能。直接内存的分配不受 JVM 限制,但是受本机物理总内存限制,所以如果有使用到这部分内存,在分配 JVM 内存时不能全部占用物理内存。

程序寄存器

JVM 可以允许多个线程同时执行,每个 JVM 线程都有自己的程序寄存器,线程正在执行的当前方法不是 native 方法则程序寄存器保存 JVM 正在执行的字节码指令的地址;如果是 native 方法则程序寄存器的值是 undefined。字节码解释器工作时通过改变程序寄存器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复都需要寄存器来完成。

JVM 多线程是通过轮流切换 CPU 来获取程序执行资源,在任何时刻一个内核只会执行一个线程中的指令,程序寄存器就保证了线程重新获取 CPU 后能让程序恢复到正确的执行位置,所以各个线程之间不能互相影响,程序寄存器也是线程私有内存空间。

程序寄存器的容量至少能保存一个 returnAddress 类型的数据或者与一个平台相关的的本地指针的值。此内存区域是唯一一个 JVM 中没有规定任何 OutOfMemoryError 的区域。

returnAddress:指向某个操作码的指针,与 Java 虚拟机指令相对应。没有直接与 Java 中的数据类型相对应,程序运行期间也无法更改。

Java 虚拟机栈

方法在执行的时候都会创建一个栈帧,用于储存局部变量表、操作数栈、动态链接、方法出口等信息。方法的执行过程对应了栈帧在虚拟机中出栈入栈的过程。Java虚拟机栈也是线程私有的空间。

局部变量表

存放了编译器可知的基本数据类型(byte、short、int、long、char、float、double、boolean)、对象引用(reference 类型,对象起始地址的引用指针、代表对象的句柄或者对象的相关地址)、returnAddress类型。64 位的 long 和 double 类型会占用 2 个局部变量表空间,其他数据类型只占用一个。局部变量表空间是在编译时期完成分配,当执行方法时这部分空间是完全确定的,运行期间也不会改变。

操作数栈

栈帧里面包含一个先进后出的操作数栈,栈帧中的操作数栈的最大深度在编译器决定。

栈帧刚创建时,操作数栈是空的, JVM 通过字节码指令从局部变量表或者对象实例字段中复制常量或者变量值到操作数栈中,从操作数栈中取走数据、操作数据、操作结果入栈等;调用方法时也可将方法参数或者方法返回结果入栈。

操作数栈的位置可以保存任意的数据类型的值,long 和 double 占用两个单位的栈深度,其他类型则占用一个单位栈深度。

动态链接

动态链接指向当前方法所在类型的运行时常量池的引用。

class 文件中,一个方法需要通过符号引用调用其他方法或者访问成员变量。动态链接是在运行时将部分符号引用转化为直接引用。部分符号引用在类加载阶段(解析)时就转化为直接引用这种转化成为静态链接。

符号引用:用一组符号描述所引用的目标,可以使任何形式的字面量,在转化时可以无歧义的定位到目标,符号引用和虚拟机的布局无关。

直接引用:与虚拟机布局相关,已经转化为直接引用,那么引用目标也一定被加载到虚拟机内存里了。直接引用可以是:直接指向目标的指针、相对偏移量、可以间接定位到目标的句柄

方法出口

方法调用正常结束

方法执行过程中没有抛出任何异常。方法正常完成时会返回一个字节码标识给他的调用方法,返回指令类型由返回的数据类型决定(如果方法有返回值)。

方法正常结束时,当前栈帧需要恢复调用者的状态,包括局部变量表、操作数栈并且正确递增程序计数器,以跳过刚才执行的方法调用指令。调用者将被调方法的返回值压入操作数栈后继续正常执行。

方法调用异常结束

方法执行过程中某些指令导致虚拟机抛出异常或者遇到了 athrow 指令,代码无法处理或者没有捕获异常导致方法结束。异常结束时不会有返回值给调用方。

Java 虚拟机栈会产生两个异常:StackOverflowError 当线程请求的栈深度大于虚拟机栈所允许的栈深度;OutOfMemoryError 虚拟机栈可以动态扩展,如果无法申请到足够的内存则会抛出异常。

本地方法栈

Java 虚拟机栈为执行 Java 方法(字节码) 服务,本地方法栈(C stack)是为虚拟机使用到的 native 方法服务。本地方法栈的支持也需要看具体的 Java 虚拟机,如果虚拟机不支持本地方法栈,则不需要开辟这一块儿内存;如果支持则在线程创建的时候会按照线程来分配内存,所以本地方法栈也是线程私有内存。该部分内存也存在 StackOverflowErrorOutOfMemoryError异常。

方法区

方法区是线程共享的内存区域,存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区在虚拟机启动的时候创建。方法区是堆的逻辑组成部分,但是虚拟机的实现可以不考虑该部分内存的垃圾收集与压缩,方法区的容量可以固定也可以动态调整,这个可以由虚拟机来具体实现。方法区内存不能满足内存分配请求虚拟机会抛出 OutOfMemoryError 异常。

运行时常量池

运行时常量池都在 Java 虚拟机的方法区中分配,在加载类和接口到虚拟机后就创建对应的运行时常量池。是 class 文件中每一个类或者接口的常量池表的运行时表现形式,主要用于存放编译期生成的各种字面量或者运行期解析后才能获得的方法和字段引用。运行时常量池是方法区的一部分,所以受到方法区的内存限制。

运行时常量池比 class 文件常量池具备动态性,运行时产生的常量也可以放入到运行时常量池,例如:String.intern() 方法。

线程共享的内存区域,几乎所有对象实例在堆上分配内存,是虚拟机所管理的最大的一块内存空间,在虚拟机启动时创建。是垃圾收集器的主要工作区域。Java 堆可以处于物理上不连续的内存空间,逻辑上连续即可。实现时可以按照固定大小来分配空间也可以动态扩展,通过(-Xmx 和 -Xms)来控制,不需要过多空间时也可以自己收缩。如果堆中没有内存支持实例分配空间,虚拟机会抛出 OutOfMemoryError 异常。