深入 Java 虚拟机-读书笔记(二)

2019/01/30 java

对应原书第 5 章:Java 虚拟机。

Java 虚拟机是什么

不同语境下含义不同,主要有三种理解:

  • 抽象规范
  • 一个具体的实现
  • 一个运行中的虚拟机实例

Java 虚拟机的生命周期

启动一个 Java 程序时,一个虚拟机实例就诞生了。当程序关闭退出,这个虚拟机实例就随之消亡了。 Java 虚拟机内部有两种线程:守护线程和非守护线程。 只要还有任何非守护线程在运行,那么这个 Java 程序也在继续运行,虚拟机实例仍然存活。 当程序中所有非守护线程都终止,虚拟机实例将自动退出。

Java 虚拟机的体系结构

在 Java 虚拟机规范中,一个虚拟机实例的行为是分别按照子系统、内存区、数据类型以及指令这几个术语来描述的。 下图为Java 虚拟机的体积结构概览图: jvm 内部体系结构

每个 Java 虚拟机实例都有一个方法区以及一个堆,它们都是由该虚拟机实例的所有线程共享的。 当每一个新线程被创建时,它都将得到它自己的 PC 寄存器(程序计数器)以及一个 Java 栈:如果线程正在执行的是一个 Java 方法(非本地方法),那么 PC 寄存器的值总是指向下一条将被执行的指令,而它的 Java 栈总是存储该线程中 Java 方法的调用状态–包括它的局部变量,被调用时传入的参数,它的返回值,以及运算的中间结果等。

Java 栈是由许多栈帧(stack frame)组成的,一个栈帧包含一个 Java 方法调用的状态。 Java 虚拟机没有寄存器,其指令集使用栈来存储中间数据。原因是:

  • 保持 Java 虚拟机指令集尽量紧凑
  • 便于 Java 虚拟机在仅有很少通用寄存器的平台上实现
  • 有助于运行时某些虚拟机实现的动态编译器和即时编译器的代码优化

接下来从各个方面一一展开来介绍 Java 虚拟机:

数据类型

jvm 数据类型

字长的考量

Java 虚拟机中最基本的数据单元是字(word),字的大小由虚拟机设计者决定,但是至少要保证:一个字能够表示int、returnAddress,两个字足以持有 long 或者 double的值。所以字至少要32位。通常设计者会根据主机平台的指针长度来选择字长。

类加载子系统

jvm 类加载子系统

方法区

被加载类型的信息存储在一个逻辑上称为方法区的内存中。Java 虚拟机加载某个类型时,它通过类加载器定位对应的 class 文件,然后读入这个 class 文件(一个线性二进制数据流)到虚拟机中,紧接着虚拟机提取其中的类型信息,并把这些信息存储到方法区。该类型的类(静态)变量同样也存储在方法区中。 由于所有线程共享方法区,因此多线程多方法区数据的访问必须被设计成是线程安全的。 用 Java 的反射机制时,类型信息就来自方法区。方法区中存储的信息有:

  • 类型信息(基本的)
    • 全限定名
    • 直接超类的全限定名(除非是 java.lang.Object 类没有超类)
    • 是类还是接口
    • 类型的访问修饰符
    • 任何直接超接口的全限定名的有序列表
  • 常量池
    • 直接常量池(string,integer的常量)
    • 对其他类型、字段、方法的符号引用
  • 字段信息(声明顺序也要保存)
    • 名称
    • 类型
    • 修饰符
  • 方法信息(声明顺序也要保存)
    • 名称
    • 返回类型
    • 参数数量和类型(按声明顺序)
    • 修饰符

    如果不是抽象方法和本地方法,还需要保存:

    • 方法的字节码
    • 方法的局部变量区大小和操作数栈大小
    • 异常表
  • 除了常量以外的所有类(静态)变量
  • 一个到类加载器的引用(动态连接时需要用到)
  • 一个到 class 类的引用(Java 虚拟机会对每一个被装载的类型创建一个 java.lang.Class 的实例,并且以某种方式把这个实例和存储在方法区中的类型信息关联起来)

Java 程序在运行时创建的所有类实例和数组都放在同一个堆空间中,多个线程共享这个堆空间。 Java 虚拟机有在堆中分配新对象的指令,但是没有释放内存的指令。虚拟机把内存回收的任务交给垃圾收集器。Java 虚拟机没有规定 Java 对象在堆中如何表示。 一种可能的堆空间设计是,堆分成两个部分:

  • 一个句柄池 一个对象引用就是一个指向句柄池的本地指针。句柄池中的每项又有两个部分:
    • 一个指向对象实例变量的指针
    • 一个指向方法区类型数据的指针
  • 一个对象池

这种表示法的示意图如下: jvm 堆中对象数据的表示方式一

另一种可能的堆空间设计是,使对象指针直接指向一组数据,而该数据包括对象实例数据和一个指向方法区类型数据的指针。 这种表示法的示意图如下: jvm 堆中对象数据的表示方式二

在上面两种表示方式中,堆上的对象数据还有一个逻辑的部分:对象锁,这是一个互斥对象。虚拟机中的每个对象都有一个对象锁,它被用于协调多个线程访问一个对象时的同步。很多对象在其整个生命周期内都没有被任何线程加锁,在线程实际请求某个对象的锁之前,实现对象锁所需要的数据不是必须的,所以很多实现不在对象自身内部保存一个指向锁数据的指针,而只有当第一次需要加锁时才分配对应的锁数据,但这时候虚拟机需要以某种间接方式来联系对象数据和对应的锁数据,比如把锁数据放在一个以对象地址为索引的搜索树中。

除了实现锁所需要的数据外,每个 Java 对象逻辑上还与实现等待集合(wait set)的数据相关联。锁用来实现互斥,等待集合用来让多个线程为完成一个共同的目标而协调工作的。

最后还有与垃圾收集有关的数据,比如对象是否被引用,finalizer 方法是否被调用。不同的垃圾回收算法需要记录的数据不一样。

数组的表示:Java 中数组是真正的对象,和其他对象一样也是保存在堆空间中,数组也有一个与它们的类相关联的 Class 实例,所有具有相同维度和元素类型数组都是同一个类的实例。 数据类的名称表示方式:每一维用一个方括号 “[” 表示,用字符或者字符串表示元素类型,比如 “[[B” 表示元素类型为 byte 的二维数组,“[[[LJava/lang/Object” 表示元素类型为 Object 的三维数组。用堆表示数组的一种可能是:

jvm 堆中数组的可能表示方式

程序计数器 (pc 寄存器)

对一个运行中的 Java 程序而言,每一个线程都有它自己的程序计数器,它在线程启动时创建,大小为 1 个字长。

Java 栈

每当启动一个新线程时,虚拟机会为它分配一个 Java 栈,Java 栈以栈帧为单位来保存线程的运行状态,虚拟机只直接对 Java 栈执行两种操作:以栈帧为单位的压栈和出栈。一个栈帧对应于一个方法调用。 Java 栈上的所有数据都是该线程私有的。 线程专有的运行时数据区

Java 栈中包含的栈帧由 3 部分组成:

  • 局部变量区
    • 存放方式:是一个以字长为单位的,从 0 开始计数的数组,long/double 的值在该数组中占连续的两项,其他类型的值占一项
    • 访问方式:字节码指令通过从 0 开始的索引来使用其中的数据
    • 包含内容:局部变量区包含对应方法的参数和局部变量。如果是实例方法,局部变量区的第一个参数是一个 reference (引用)类型
  • 操作数栈
    • 存放方式:也是以字长为单位的数组
    • 访问方式:压栈和出栈

    Java 虚拟机没有寄存器,程序计数器也无法被程序指令直接访问,Java 虚拟机的指令(主要)是从操作数栈中而不是从寄存器中获取的操作数,也就是说 Java 虚拟机是基于栈的,而不是基于寄存器的。

    虚拟机把操作数栈作为它的工作区:大多数指令都要从这里弹出数据,执行计算,然后把结果压回操作数栈。以 iadd 指令为例,它要从操作数栈中弹出 2 个整数,然后做加法,最后把结果压回到操作数栈:

      iload_0   // 把局部变量区中的索引为 0 的整数压入操作数栈中
      iload_1   // 把局部变量区中的索引为 1 的整数压入操作数栈中
      iadd      // 弹出上面压入操作数栈中的 2 个整数,相加,把结果压回操作数栈中
      istore_2  // 从操作数栈中弹出结果,并把它存储到局部变量区中索引为 2 的位置
    

    上述过程的示意图: 两个局部变量相加的过程

  • 帧数据区 Java 栈帧还需要一些数据来支持常量池解析、正常方法返回以及异常派发机制,这些信息都保存在帧数据区。(此外也可以保存用于调试的数据)

Java 栈的可能实现方式:

  1. 从堆中分配每一帧
  2. 从一个连续的栈中分配(这种方式允许相邻方法的栈帧可以互相重叠:调用者的操作数栈部分就成了被调用者的局部变量区的底层,既节省了存储空间,也减少了拷贝时间)
  3. 混合 1 和 2
本地方法栈
执行引擎

不同语境下有 3 种理解:

  • 抽象规范

    使用指令集描述其行为

  • 具体实现

    可能使用多种不同的技术:软件、硬件、多种技术结合,解释、即时编译、自适应优化、芯片级直接执行等

  • 正在运行的实例

    线程。运行中的每个线程都是一个独立的虚拟机执行引擎的实例,从线程生命周期的开始到结束,它要么在执行字节码,要么在执行本地方法

参考: JVM architecture - tutorial

Search

    Table of Contents