深入理解Java虚拟机 第二章 Java运行时数据区域

时间:2022-12-28 13:03:05

运行时数据区域

Java虚拟机在执行Java程序过程中会把它所管理的内存划分为若干个不同的数据区域。

深入理解Java虚拟机 第二章 Java运行时数据区域

其中方法区和堆是由所有线程共享的数据区域。
虚拟机栈、本地方法栈和程序计数器是线程私有的内存。

2.2.1 程序计数器

  • 程序计数器(Program Counter Register)是一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。

  • 在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器完成。

  • 此内存区域是唯一一个不会抛出OutOfMemoryError异常的区域。

2.2.2 Java虚拟机栈

  • Java虚拟机栈(Java Virtual Machine Stacks)描述的是Java方法执行的内存模型:
    每个方法在执行的同时都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口、附加信息等信息。
    每一个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
    一个线程中的方法调用链可能很长,很多方法都同时处于执行状态,对于执行引擎来说,活动线程中,只有栈顶的栈帧是有效的,成为当前栈帧。这个栈帧所关联的方法成为当前方法。

  • 此区域会抛出*Error异常和OutOfMemoryError异常。

  • 栈帧的概念结构
    深入理解Java虚拟机 第二章 Java运行时数据区域

  • 局部变量表

    • 局部变量表用于存放方法参数和方法内部定义的局部变量。
      它存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型)、returnAddress类型(指向了一条字节码指令的地址)。
    • 局部变量表所需的内存空间在编译期间时确定。
  • 操作数栈

    • 操作数栈的最大深度也在编译期间时确定。
    • 当一个方法刚刚开始时,这个方法的操作数栈是空的。在方法执行过程中,会有各种字节码指令向操作数栈写入和提取内容。
    • 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配。在编译时编译器严格保证这一点,在类校验阶段的数据流分析中还要再次验证这一点。
  • 动态链接
    每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持调用过程中的动态链接。
    Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。
    这些符号引用一部分会在类加载阶段或第一次使用时转化为直接引用,这种转化称为静态分析。
    另一部分将在每一次的运行期间转化为直接引用,这部分称为动态链接。

  • 方法返回地址

    • 方法执行后有两种方式退出这个方法:
      一种是遇到任意一个方法返回的字节码指令,这称为正常完成出口(Normal Method Invocation Completion);
      另一种是执行过程中遇到异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出;这称为异常完成出口(Abrupt Method Invocation Completion)。

    • 方法返回时,可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。
      正常退出时,调用者的PC计数器的值就可以作为返回地址。
      异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。

    • 方法退出的实际过程上等同于当前栈帧出站,因为退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。

  • 附加信息
    具体的虚拟机实现时增加的一些规范中没有描述的信息。比如调试信息等。

2.2.3 本地方法栈

  • 本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用十分类似。不过虚拟机栈为Java方法(字节码)服务,而本地方法栈为虚拟机使用到的Native方法服务。

  • 在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定,因此虚拟机可以*实现它。

  • 本地方法栈区域也会抛出*Error和OutOfMemoryError异常。

2.2.4 Java堆

Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。它是被所有线程共享的一块内存区域,在虚拟机启动时创建。
此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。规范中描述:所有的对象实例和数组都要在堆上分配。

2.2.5 方法区

  • 方法区(Method Area)也是线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

  • 规范对方法区的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或可扩展外,还可以选择不实现垃圾收集。
    该区域的内存回收目标主要是针对常量池的回收和对类型的卸载。

  • 此区域会抛出OutOfMemoryError异常。

  • 运行时常量池:
    运行时常量池(Runtime Constant Pool)是方法区的一部分。
    Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

2.2.7 直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是规范中定义的内存区域。

  • 在JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的IO方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在堆中的DirectByteBuffer对象作为这块内存的引用进行操作。
    这样在某些场景中显著提高了性能,避免了在Java堆和Native堆中来回复制数据。