JVM 运行时数据区规范

186 阅读8分钟

运行时数据区

《JVM 规范》定义的运行时数据区如下图所示:

image.png

线程隔离的数据区

线程隔离数据区中数据为线程私有,随着线程创建而创建,随线程销毁而销毁。

程序计数器(Program Counter Register)

程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在 JVM 的概念模型里,字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

如果当前方法是 Java 方法,这个计数器值的是正在执行的字节码指令的地址;如果当前方法是 native 方法,这个计数器值为undefined

JVM 栈

JVM 栈描述的是 Java 方法执行的线程内存模型:每个方法被执行的时候,JVM 都会同步创建一个栈帧(Stack Frame)。一个方法被调用至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

JVM 栈所使用的内存不需要保证是连续的,可以是固定大小或根据计算动态扩展和收缩的。

JVM 栈可能发生如下异常:

  • 如果线程请求分配的栈容量超过JVM 栈最大深度,将抛出StackOverflowError异常。
  • 如果JVM 栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的JVM 栈,将会抛出OutOfMemoryError异常。

本地方法栈(Native Method Stacks)

本地方法栈JVM 栈所发挥的作用是非常相似的,区别是JVM 栈为 JVM 执行的 Java 方法服务,而本地方法栈是为 JVM 使用到的 Native 方法服务。

本地方法栈所使用的内存可以是固定大小或根据计算动态扩展和收缩的。

本地方法栈可能发生如下异常:

  • 如果线程请求分配的栈容量超过本地方法栈允许的最大深度,将抛出StackOverflowError异常。
  • 如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,将抛出OurOfMemoryError异常。

线程共享数据区

线程共享数据区中数据为线程共享,随着虚拟机启动而创建,随着虚拟机退出而销毁。

Java 堆(Java Heap)

Java 堆是虚拟机所管理的内存中最大的一块,“几乎”是供所有类实例和数组对象分配内存的区域

Java 堆所使用的内存不需要保证是连续的,可以是固定大小或者根据计算动态扩展和收缩的。

Java 堆可能发生如下异常:

  • 如果实际所需的Java 堆超过了Java 堆允许的最大容量,将抛出OutOfMemoryError异常。

方法区(Method Area)

方法区存储每一个类的结构信息,例如,运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容,还包括一些在类、实例、接口初始化时用到的特殊方法

方法区所使用的内存不需要保证是连续的,可以是固定大小或者根据计算动态扩展和收缩的。

方法区可能发生如下异常:

  • 如果方法区的内存空间不能满足内存分配请求,将抛出OutOfMemoryError异常。

运行时常量池(runtime constant pool)

运行时常量池ClassFile 结构常量池表的运行时表示形式,它包括了若干种不同的常量。

运行时常量池相对于ClassFile 结构常量池表的一个重要特征是具备动态性,Java 语言并不要求常量一定只有编译期才能产生,也就是说,并非预置入ClassFile 结构常量池表的内容才能进入运行时常量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是String.intern()方法。

在创建类和接口的运行时常量池时,可能会发生如下异常:

  • 当创建类或接口时,如果构造运行时常量池所需要的内存空间超过了方法区所能提供的最大值,将抛出OutOfMemoryError异常。

特殊方法

特殊方法包括 3 种:

  • 实例初始化方法 在 JVM 层面上,Java 编程语言中的构造器是以一个名为<init>的特殊实例初始化方法的形式出现的。

  • 类或接口初始化方法 一个类或接口最多可以包含不超过一个类或接口初始化方法,类或接口就是通过这个方法完成初始化的。这个方法是一个不包含参数的、返回类型为 void 的方法,名为<clinit>

  • 签名多态方法(signature polymorphic) 当一个方法具体签名多态性。则意味着这个方法满足以下全部条件:

  • 通过java.lang.invoke.MethodHandle类或java.lang.invoke.VarHandle类进行声明。

  • 只有一个类型为Object[]的形参。

  • 返回值为Object

  • ACC_VARARGSACC_NATIVE标志被设置。

栈帧(frame)

栈帧是用来存储数据和部分过程结果的数据结构。同时也用来处理动态链接、方法返回值和 异常分派

栈帧随着方法调用而创建,随着方法结束而销毁 ———— 无论方法正常完成还是方法异常完成都算作方法结束。栈帧的存储空间由创建它的线程分配在JVM 栈中,每一个栈帧都有自己的局部变量表操作数栈和指向当前方法所属的类的运行时常量池的引用。

动态链接(dynamic linking)

每个栈帧内部都包含一个指向当前方法实现动态链接。在字节码文件格式里面,一个方法若要调用其他方法,或者访问成员变量,则需要通过符号引用来表示,动态链接的作用就是将这些以符号引用所表示的方法转换为对实际方法的直接引用

  • 符号引用(Symbolic References)符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
  • 直接引用(Direct References)直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄

局部变量表(local variable)

每个栈帧内部都包含一组称为局部变量表的变量列表。栈帧局部变量表的长度由编译期决定

局部变量表可以存放原始值引用值。这些数据在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中 long 和 double 类型的数据会占用两个变量槽,其余类型数据占用一个。long 和 double 类型的数据采用两个局部变量槽中较小的索引值来定位。例如,将一个 double 类型的数据存储在索引值为 n 的 局部变量槽中,实际上是索引值为 n 和 n+1 的两个局部变量槽都用来存储这个数据。然而,索引值为 n+1 的局部变量槽是无法直接读取的,但是可能会被写入。如果进行了这种操作,将导致局部变量 n 的内容失效。

JVM 使用局部变量表来完成方法调用时的参数传递。当调用类方法时,它的参数将会一次传递到局部变量表中从 0 开始的连续位置上。当调用实例方法时,第 0 个局部变量一定用来存储该实例方法所在对象的引用(this)。后续的其他参数将会传递至局部变量表中从 1 开始的连续位置上。

操作数栈

每个栈帧内部都包含一个称为操作数栈的后进先出(Last-In-First-Out,LIFO)栈。栈帧操作数栈的最大深度由编译期决定

栈帧在刚刚创建时,操作数栈是空的。JVM 提供一些字节码指令来从局部变量表或者对象实例的字段中复制常量或变量值到操作数栈中,也提供了一些指令用于从操作数栈取走数据、操作数据以及把操作结果重新入栈。在调用方法时,操作数栈也用来准备调用方法的参数以及接受方法返回结果。

操作数栈的位置上可以存放原始值引用值。long 或者 double 类型的数据占用两个单位的栈深度,其他数据占用一个单位的栈深度。