1. 代码执行
虚拟机
之所以要在虚拟机中运行,是因为它提供了可移植性。一旦 Java 代码被编译为Java字节码,便可以在不同平台上的 Java虚拟机实现上运行。此外,虚拟机还提供了一个代码托管的环境,代替我们处理部分冗长而且容易出错的事务,例如内存管理。
内存区域
代码执行
Java 字节码无法直接执行。因此,Java 虚拟机需要将字节码翻译成机器码。在 HotSpot 里面,上述翻译过程有两种形式:第一种是解释执行,即逐条将字节码翻译成机器码并执行;第二种是即时编译(Just-In-Time compilation,JIT),即将一个方法中包含的所有字节码编译成机器码后再执行。
前者的优势在于无需等待编译,而后者的优势在于实际运行速度更快。HotSpot 默认采用混合模式,综合了解释执行和即时编译两者的优点。它会先解释执行字节码,而后将其中反复执行的热点代码,以方法为单位进行即时编译。
执行优化(即时编译)
HotSpot 内置了多个即时编译器:C1、C2 和 Graal。
C1 又叫做Client 编译器,面向的是对启动性能有要求的客户端GUI 程序,采用的优化手段相对简单,因此编译时间较短。
C2 又叫做 Server编译器,面向的是对峰值性能有要求的服务器端程序,采用的优化手段相对复杂,因此编译时间较长,但同时生成代码的执行效率较高。从 Java 7开始,HotSpot 默认采用分层编译的方式:热点方法首先会被 C1 编译,而后热点方法中的热点会进一步被 C2 编译。
2. 基本类型
其中,boolean 类型在 Java 虚拟机中被映射为整数类型:“true”被映射为 1,而“false”被映射为0。Java 代码中的逻辑运算以及条件跳转,都是用整数相关的字节码来实现的。
除 boolean 类型之外,Java 还有另外 7 个基本类型。它们拥有不同的值域,但默认值在内存中均为 0。这些基本类型之中,浮点类型比较特殊。基于它的运算或比较,需要考虑 +0.0F、-0.0F 以及NaN 的情况。
除 long 和 double 外,其他基本类型与引用类型在解释执行的方法栈帧中占用的大小是一致的,但它们在堆中占用的大小确不同。在将 boolean、byte、char 以及 short 的值存入字段或者数组单元时,Java 虚拟机会进行掩码操作。在读取时,Java 虚拟机则会将其扩展为 int 类型。
算术运算
Java 虚拟机的算数运算几乎全部依赖于操作数栈。也就是说,我们需要将堆中的 boolean、byte、char 以及 short 加载到操作数栈上,而后将栈上的值当成 int 类型来运算。
对于 boolean、char 这两个无符号类型来说,加载伴随着零扩展。举个例子,char 的大小为两个字节。在加载时 char 的值会被复制到 int 类型的低二字节,而高二字节则会用 0 来填充。
对于 byte、short 这两个类型来说,加载伴随着符号扩展。举个例子,short 的大小为两个字节。在加载时 short 的值同样会被复制到 int 类型的低二字节。如果该 short 值为非负数,即最高位为 0,那么该 int 类型的值的高二字节会用 0 来填充,否则用 1 来填充。
3. 类加载
从 class 文件到内存中的类,按先后顺序需要经过加载、链接以及初始化三大步骤。
加载,是指查找字节流,并且据此创建类的过程。前面提到,对于数组类来说,它并没有对应的字
节流,而是由 Java 虚拟机直接生成的。对于其他的类来说,Java 虚拟机则需要借助类加载器来完
成查找字节流的过程。
字节流
说到字节流,最常见的形式要属由 Java 编译器生成的 class 文件。除此之外,我们也可以在程序内部直接生成,或者从网络中获取(例如网页中内嵌的小程序 Java applet)字节流。
链接
链接,是指将创建成的类合并至 Java 虚拟机中,使之能够执行的过程。它可分为验证、准备以及解析三个阶段。
初始化,则是为标记为常量值的字段赋值,以及执行 < clinit > 方法的过程。类的初始化仅会被执行一次,这个特性被用来实现单例的延迟初始化。
4. JVM 的静态绑定和动态绑定
具体来说,Java 字节码中与调用相关的指令共有五种。
1. invokestatic:用于调用静态方法。
2. invokespecial:用于调用私有实例方法、构造器,以及使用 super 关键字调用父类的实例方法或构造器,和所实现接口的默认方法。
3. invokevirtual:用于调用非私有实例方法。
4. invokeinterface:用于调用接口方法。
5. invokedynamic:用于调用动态方法。
对于 invokestatic 以及 invokespecial 而言,Java 虚拟机能够直接识别具体的目标方法。
而对于 invokevirtual 以及 invokeinterface 而言,在绝大部分情况下,虚拟机需要在执行过程中,根据调用者的动态类型,来确定具体的目标方法。
虚方法调用在 Java 虚拟机中的实现方式。
虚方法调用包括 invokevirtual 指令和 invokeinterface 指令。如果这两种指令所声明的目标方法被标记为 fnal,那么 Java 虚拟机会采用静态绑定。否则,Java 虚拟机将采用动态绑定,在运行过程
中根据调用者的动态类型,来决定具体的目标方法。Java 虚拟机的动态绑定是通过方法表这一数据结构来实现的。方法表中每一个重写方法的索引值,与父类方法表中被重写的方法的索引值一致。在解析虚方法
调用时,Java 虚拟机会纪录下所声明的目标方法的索引值,并且在运行过程中根据这个索引值查找具体的目标方法。
Java 虚拟机中的即时编译器会使用内联缓存来加速动态绑定。Java 虚拟机所采用的单态内联缓存将纪录调用者的动态类型,以及它所对应的目标方法。
当碰到新的调用者时,如果其动态类型与缓存中的类型匹配,则直接调用缓存的目标方法。否则,Java 虚拟机将该内联缓存劣化为超多态内联缓存,在今后的执行过程中直接使用方法表进行动态绑定。
5. 异常(看)
6. 反射
(反射是 Java 语言中一个相当重要的特性,它允许正在运行的 Java 程序观测,甚至是修改程序的动态行为。)
Class.forName,Class.getMethod 以及 Method.invoke
MethodAccessor 是一个接口,它有两