JVM 运行时数据区

JVM 运行时数据区图

PC 寄存器

Java 虚拟机可以支持多条线程同时执行,每个线程都有自己的 PC 寄存器。在任意时刻,一个线程只会执行一个方法的代码,这个正在被线程执行的方法称为该线程的当前方法。如果这个方法不是 native 的,那 PC 寄存器就保存 Java 虚拟机正在执行的字节码指令的地址,如果该方法是 native 的,那 PC 寄存器的值是 undefined。PC 寄存器的容量至少应当能保存一个 returnAddress 类型的数据或者一个与平台相关的本地指针的值。

Java 虚拟机栈

每个线程都有自己私有的 Java 虚拟机栈,这个栈与线程同时创建,用于存储栈帧(Frames)。Java 虚拟机栈的作用就是用于存储局部变量与一些过程结果的地方。除了栈帧的出栈和入栈之外,Java 虚拟机栈不会再受其他因素的影,Java 虚拟机栈所使用的内存不需要保证是连续的。

Java 虚拟机栈可以是固定大小的或者是根据计算动态扩展和收缩的。

Java 虚拟机栈可能发生如下异常情况:

  • 如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量时,Java 虚拟机将会抛出一个 StackOverflowError 异常。
  • 如果 Java 虚拟机栈可以动态扩展,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。

Java 堆

在 Java 虚拟机中,堆(Heap)是可供各条线程共享的运行时内存区域,也是供所有类实例和数组对象分配内存的区域。

Java 堆在虚拟机启动的时候就被创建,它存储了被 GC 所管理的各种对象,这些受管理的对象无需,也无法显式地被销毁。Java 堆的容量可以是固定大小的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩。Java 堆所使用的内存不需要保证是连续的。

Java 堆可能发生如下异常情况:

  • 如果实际所需的堆超过了自动内存管理系统能提供的最大容量,那 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。

方法区

在 Java 虚拟机中,方法区是可供各线程共享的运行时内存区域。方法区它存储了每一个类的结构信息,例如运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法。

方法区在虚拟机启动的时候被创建,虽然方法区是堆的逻辑组成部分,但是简单的虚拟机实现可以选择在这个区域不实现垃圾收集。方法区在实际内存空间中可以是不连续的。

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

  • 如果方法区的内存空间不能满足内存分配请求,那 Java 虚拟机将抛出一个 OutOfMemoryError 异常。

运行时常量池

运行时常量池是每一个类或接口的常量池的运行时表示形式,它包括了若干种不同的常量:从编译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。

每一个运行时常量池都分配在 Java 虚拟机的方法区之中,在类和接口被加载到虚拟机后,对应的运行时常量池就被创建出来。

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

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

本地方法栈

Java 虚拟机实现可能会使用到 native 方法,这个栈就是本地方法栈。Java 虚拟机规范允许本地方法栈被实现成固定大小的或者是根据计算动态扩展和收缩的。

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

  • 如果线程请求分配的栈容量超过本地方法栈允许的最大容量时,Java 虚拟机将会抛出一个StackOverflowError 异常。
  • 如果本地方法栈可以动态扩展,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的本地方法栈,那 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。

虚拟机类加载机制

类加载的时机

类的生命周期

在初始化阶段有且仅有四种情况必须立即对类进行初始化:

  • 遇到 new、getstatic、putstatic、invokestatic 这4条字节码指令时候,如果类没有进行初始化,则需要先触发初始化。
  • 使用反射调用时候,如果类没有进行初始化则需要先触发初始化。
  • 当初始化一个类时候,发现其父类还没初始化,先对其父类进行初始化。
  • 当虚拟机启动时,用户需要指定执行带 main 方法的类,虚拟机会先初始化这个类。

这些情况称为对一个类的主动引用。并且第一条对应到 Java 代码的场景是:

  • 创建类的实例(new Object())
  • 读取或设置某个类或者接口的静态变量,(静态编译时常量(即编译时可以确定值的常量)不会导致类的初始化)
  • 调用一个类的静态方法

类加载

查找并且加载类的二进制数据,JVM 规范没有强制约束加载动作,可以由不同虚拟机自行实现。将 class 文件加载到内存当中,将其放在运行时数据区的方法区内,然后再堆区创建一个 java.lang.Class 对象,用来封装类在方法区内的数据结构。

连接

  • 验证:确保被加载类的正确性。
  • 准备:给类的静态变量分配内存,并且将其初始化成默认值。
  • 解析:把类中的符号引用转换成直接引用。

需要说明一下在准备阶段的初始化成默认值:

1
public static int value = 12;

在准备阶段过后的初始值是0,而不是12;

初始化

初始化阶段才真正开始执行类中的 Java 代码。初始化阶段是执行类构造器 <clinit>() 方法的过程。

  • <clinit>() 方法是编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,顺序是源文件中顺序。静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但是不能访问。
  • 虚拟机会保证在子类 <clinit>() 方法执行前,父类的 <clinit>() 方法已经执行完毕。
  • 由于父类的 <clinit>() 方法先执行,就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
static class Parent {
public static int A = 1;
static {
A = 2;
}
}

static class Sub extends Parent {
public static int B = A;
}

public static void main(String[] args) {
System.out.println(Sub.B);//输出2
}
  • <clinit>() 方法不是必须的,如果类中没静态语句块也没有对变量的赋值操作,那么编译器可以不为这个类生成 <clinit>() 方法。
  • 接口中不可使用静态语句块,但是接口中也会生成 <clinit>() 方法。但是接口与类不同的是,执行接口的 <clinit>() 方法不需要先执行父接口的 <clinit>() 方法。只有当父接口中定义的变量被使用时,才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的 <clinit>() 方法。
  • 虚拟机会保证 <clinit>() 方法是同步的。

—EOF—