本文简介运行时数据区,包括程序计数器,Java 虚拟机栈,堆,方法区,本地方法栈,直接内存及对应的内存溢出异常发生条件。
持续更新中~
Java 虚拟机定义了在程序运行期间不同的运行时数据区。其中的一些数据区在虚拟机启动创建,在虚拟机退出时销毁。其他的数据区是依赖用户线程的启动和结束而建立和销毁。Java 虚拟机规范规定 Java 虚拟机锁管理的内存包括以下几个运行时数据区,如下图所示。
Java 虚拟机能够支持很多线程同时运行。每个 Java 虚拟机线程有自己的程序计数器(program counter register)。任何时候,每个 Java 虚拟机线程都在执行单个方法的代码,即该线程的当前方法。
如果该方法不是 native,程序计数器包含当前被执行的 Java 虚拟机指令的地址;否则,程序计数器中的值为 undefined。
此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 的区域。
Java 虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时会穿点一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
Java 虚拟机栈相关的异常条件:
**栈帧(Frame)**用于存储数据和部分结果,以及执行动态链接,返回方法的值和分派异常。
每次调用一个方法时,都会创建一个新的栈帧。当栈帧的方法调用完成时,无论该完成时正常的还是意外的(抛出了未捕获的异常),该栈帧将被销毁。栈帧是创建栈帧线程的 Java 虚拟机堆栈中分配的。每个栈帧都有自己本地变量表(Local Variable Table),操作数栈(Operand Stack),以及对当前方法类的运行时常量池(run-time constant pool)的引用。
本地变量数组和操作数栈的大小在编译时确定的,并与栈帧相关的方法代码一起提供。
在给定的控制线程中,只有一个栈帧(执行方法的栈帧,位于栈顶的栈帧)是活动的,该栈帧成为当前栈帧,其方法称为当前方法。
当一个栈帧的方法调用了另一个方法,或者它的方法完成了,那么这个栈帧不再是当前栈帧。当调用一个方法时,将创建一个新的栈帧,并在控制转移到新方法时成为当前栈帧。在方法返回时,当前栈帧将其方法调用的结果(若果有的话)传给前一个栈帧。当前一栈帧成为当前栈帧时,当前栈帧将被丢弃。
注意:一个线程创建的栈帧时该线程的本地栈帧,不能被任何其他线程引用。
执行引擎运行的所有字节码指令都只针对于当前栈帧进行操作,栈帧的概念结构如下图所示。
接下来,详细介绍下栈帧中的局部变量表、操作数栈、动态连接、方法返回地址等各部分的作用于数据结构。
**局部变量表(Loca Variable Table)**是一个变量数组,用于存储方法参数和方法内定义的局部变量。栈帧的局部变量表的长度是在编译时确定的,在方法的 Code 属性的 max_locals 数据项中确定了该方法所需分配的局部变量表的最大容量。
局部变量表的容量以变量槽(Variable Slot)为最小单位,虚拟机规范中未明确指明一个 Slot 占用的内存空间大小,只说明每个 Slot 可以存放一个 boolean,byte,char,int,float,reference 或 returnAddress 类型的数据。对于 reference 类型,可以获取引用中直接或间接地查找到对象在 Java 堆中的数据存放的起始地址索引,也可以直接或间接地查找到对象所属的数据类型在方法区中的存储的类型信息。对于 returnAddress 类型,它是为字节码 jsr,jsr_w 和 ret 服务的,指向一条字节码指令的地址,在很早的 Java 虚拟机中层使用者几条指令来实现异常处理,目前已经由异常表替代。除上述 8 中类型外,还有 64位的 long 和 double 类型,占两个连续的 Slot 空间。此处 long 和 double 数据类型读写分割为两次 32 位读写做法与“long 和 double 的非原子性协定”中把一次 long 和 double 的读写分割为两次 32 位读写的做法类似,可以与 Java 内存模型[[1]](./Java 内存模型)做对比。不过,由于局部变量表建立在线程的堆栈上,是线程私有的数据,无论读写两个连续 Slot 是否为原子操作,都不会引起线程安全问题。
在方法执行时,虚拟机使用局部变量表完成参数值到参数变量列表的传递过程,如果执行的是实例变量(非 static 方法),那局部变量表中第0位索引的 Slot 默认是用于传递方法所属对象实例的引用,在方法中可通过关键字“this”访问到该隐含参数。其余参数按照参数表顺序排列,参数表分配完后,再根据方法体内部定义的变量顺序和作用域分配其余的 Slot。
**操作数栈(Operand Stack)**是一个后进先出(Last In First Out,LIFO)栈,和局部变量表类似,操作数栈的最大深度在编译时写入 Code 属性的 max_stacks 数据项中。在方法执行的任何时候,操作数栈的深度都不会超过在 max_stacks 数据项中设定的最大值。
当一个方法开始执行时该方法的操作数栈是空的,在方法执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,就是入栈/出栈操作。例如,在做算数运算时是通过操作数栈来进行的,又或者在调用其他方法时候通过操作数栈来进行参数传递。
在概念模型中,两个栈帧作为虚拟机的元素是完全互相独立。但在很多虚拟机实现中会做将两个栈帧部分重叠的优化处理,如此,在方法调用时可以共用一部分数据,无须进行额外的参数复制传递,重叠的过程如下图所示。
每个栈帧都包含对当前方法类型的运行时常量池的引用,以支持方法代码的动态连接(Dynamic Linking)。根据 Class 文件结构可知,Class 文件的常量池中存有大量的符号引用,字节码中的方法调用指令就是以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或第一次使用时就转化为直接引用,该转化成为静态解析。另一部分在每一次运行期间转化为直接引用,该部分成为动态连接。两个转换过程的详细信息,可参考方法调用[2]。
方法开始执行时,只有两种方式可以退出该方法。
正常的方法调用完成(Normal Method Invocation Completion)
当执行引擎遇到任意一个方法返回的字节码指令时,可能会有返回值传递上层的方法调用者。
异常的方法调用完成(Abrupt Method Invocation Completion)
在方法执行过程中遇到异常,且该异常没有在方法体内得到处理,无论是 Java 虚拟机内部产生的异常,还是代码中使用 athrow 字节码产生的异常,只要在本方法的异常表中未搜索到匹配的异常处理器,会导致方法退出。异常的方法调用完成的方式不会给它的上层调用调用者产生任何返回值。
无论采用何种退出方式,在方法退出胡,都需要返回到方法被调用的位置,程序才能继续执行,方法执行时可能需要在栈帧中保留一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的 PC 计数器的值可作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址要通过异常处理器表来确定,栈帧中一般不会保留这部分信息。
方法退出的过程实际上就是等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整 PC 计数器的值以指向方法调用指令后一条指令等。
Java 虚拟机有一个被所有 Java 虚拟机线程共享的堆(heap)。堆是为所有类实例和数组分配内存的运行时数据区(run-time data area)。
Java 堆是垃圾收集器管理的主要区域,因此很多时候被成为 “GC 堆”(Garbage Collected Heap)。
从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以 Java 堆中还可以细分为:新生代和老年代;再细致一点有 Eden 空间、From Survivor 空间、To Survivor 空间等;从内存分配的角度来看,线程共享的 Java 堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocationo Buffer,TLAB)。
Java 虚拟机规范规定,Java 堆可以处于物理上不连续的内存空间,只要逻辑上连续即可。在实现时,既可以实现固定大小的,也可以是可扩展的,不过当前主流虚拟机都是按照可扩展来实现的(通过 -Xmx 和 -Xms 控制)。
堆相关的异常条件:
方法区(Method Area)是各个线程共享的内存区域。方法区类似于传统语言编译代码的存储区域,或类似于操作系统进程中的“text”段。存储着 class 的结构,例如运行时常量池,字段和方法数据,方法和构造器的代码,包括在类和接口初始化以及实例初始化中使用的特殊方法。
Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它有一个别名叫 Non-Heap(非堆),目的是与 Java 堆区分开。
Java 虚拟机规范对方法区的限制非常宽松,除了不需要连续的内存和可以选择固定大小或者可扩展,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在区域是比较少出现的,但并非数据进入方法区就如永久代的名字一样“永久”存在。该区域的内存回收目标主要是针对常量池的回收和对类型的卸载。
方法区是 JVM 的规范,而永久代,元空间是方法区的实现。
方法区相关的异常条件:
**运行时常量池(Run-Time Constant Pool)**是方法区的一部分。运行时常量池是每一个 class 或每一个 interface 运行时在 class 文件中 constant_pool table 的代表。它包含了几种类型的常量,从编译时期已知的数值文字到必须在运行时解析的方法和字段引用。运行时常量池提供了与传统编程语言的符号表(symbol table
)类似的功能。
Class 文件[[3]](./Class 文件结构.md)中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期间生成的各种字面量和符号引用,该部分内容将在类加载后进入方法区的运行时常量池中存放。
类或接口的运行时常量池的构造相关的异常条件:
**本地方法栈(Native Method Stack)**与虚拟机栈发挥的作用非常相似,它们之间的区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到 Native 方法服务。
在 Java 虚拟机规范中对本地方法栈中方法使用的语言,方式及数据结构没有强制规定。Sun HotSpot 虚拟机把本地房发展和虚拟机栈合二为一。
本地方法栈相关的异常条件:
**直接内存(Direct Memory)**并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域。但是该部分内存被频繁使用,而且也可能抛出 OutOfMemoryError。
JDK 1.4 中引入了 NIO(New Input/Output)类,引入基于通道(channel
)与缓冲区(buffer
)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,通过存储在 Java 堆中的 DirectByteBuffer 对象作为该内存的引用进行操作。该技术能够避免在 Java 堆和 Native 堆中来回复制数据(zero copy,零拷贝),在某些场景中能显著提高性能。
直接内存相关的异常条件:
本文简介运行时数据区,包括程序计数器,Java 虚拟机栈,堆,方法区,本地方法栈,直接内存及对应的内存溢出异常发生条件。
[1] [Java 内存模型](./Java 内存模型)
[2] 方法调用
[3] [Class 文件结构](./Class 文件结构.md)
[4] 虚拟机字节码执行引擎
[5] Run-Time Data Areas, Java Virtual Machine Specification, Java SE 14
[6] Frames, Java Virtual Machine Specification, Java SE 14
[7] Java 内存区域与内存溢出,深入理解 Java 虚拟机,第二版
[8] 虚拟机字节码执行引擎,深入理解 Java 虚拟机,第二版