00 开篇词 | 为什么我们要学习Java虚拟机?

2019/12/14 posted in  极客-深入拆解Java虚拟机

00 深入拆解Java虚拟机

开篇词 (1讲)

开篇词 | 为什么我们要学习Java虚拟机?

模块一:Java虚拟机基本原理 (12讲)

01 | Java代码是怎么运行的?

02 | Java的基本类型

03 | Java虚拟机是如何加载Java类的?

04 | JVM是如何执行方法调用的?(上)

05 | JVM是如何执行方法调用的?(下)

06 | JVM是如何处理异常的?

07 | JVM是如何实现反射的?

08 | JVM是怎么实现invokedynamic的?(上)

09 | JVM是怎么实现invokedynamic的?(下)

10 | Java对象的内存布局

11 | 垃圾回收(上)

12 | 垃圾回收(下)

模块二:高效编译 (12讲)

【工具篇】 常用工具介绍

13 | Java内存模型

14 | Java虚拟机是怎么实现synchronized的?

15 | Java语法糖与Java编译器

16 | 即时编译(上)

17 | 即时编译(下)

18 | 即时编译器的中间表达形式

19 | Java字节码(基础篇)

20 | 方法内联(上)

21 | 方法内联(下)

22 | HotSpot虚拟机的intrinsic

23 | 逃逸分析

模块三:代码优化 (10讲)

24 | 字段访问相关优化

25 | 循环优化

26 | 向量化

27 | 注解处理器

28 | 基准测试框架JMH(上)

29 | 基准测试框架JMH(下)

30 | Java虚拟机的监控及诊断工具(命令行篇)

31 | Java虚拟机的监控及诊断工具(GUI篇)

32 | JNI的运行机制

33 | Java Agent与字节码注入

模块四:黑科技 (3讲)

34 | Graal:用Java编译Java

35 | Truffle:语言实现框架

36 | SubstrateVM:AOT编译框架

尾声 (1讲)

尾声 | 道阻且长,努力加餐

2019/12/14 posted in  极客-深入拆解Java虚拟机

00【工具篇】 常用工具介绍

2019/12/14 posted in  极客-深入拆解Java虚拟机

01 | Java代码是怎么运行的?

  • 为什么 Java 要在虚拟机里运行?
  • Java 虚拟机具体是怎样运行 Java 字节码的?
  • Java 虚拟机的运行效率究竟是怎么样的?

为什么 Java 要在虚拟机里运行?

  1. Java是一种高级语言,不能直接运行在硬件,需要转换
  2. 设计一个面向Java语言的虚拟机,通过编译器将 Java 程序转换成该虚拟机所能识别的指令序列。
  3. 在现有平台(Windows Linux)上实现Java虚拟机,Java程序便可以在不同平台上的虚拟机实现里运行。

Java 虚拟机具体是怎样运行 Java 字节码的?

  • 从虚拟机视角来看
    1. 执行 Java 代码首先需要将它编译而成的 class 文件加载到 Java 虚拟机中。加载后的 Java 类会被存放于方法区(Method Area)中。实际运行时,虚拟机会执行方法区内的代码。
    2. 在运行过程中,每当调用进入一个 Java 方法,Java 虚拟机会在当前线程的 Java 方法栈中生成一个栈帧,用以存放局部变量以及字节码的操作数。这个栈帧的大小是提前计算好的,而且 Java 虚拟机不要求栈帧在内存空间里连续分布。
    3. 当退出当前执行的方法时,不管是正常返回还是异常返回,Java 虚拟机均会弹出当前线程的当前栈帧,并将之舍弃。
  • 从硬件视角来看,Java 字节码无法直接执行。因此,Java 虚拟机需要将字节码翻译成机器码。
    1. 第一种是解释执行,即逐条将字节码翻译成机器码并执行;
    2. 第二种是即时编译(Just-In-Time compilation,JIT),即将一个方法中包含的所有字节码编译成机器码后再执行。

Java 虚拟机的运行效率究竟是怎么样的?

  1. 即时编译建立在程序符合二八定律的假设上,也就是百分之二十的代码占据了百分之八十的计算资源。
  2. 即时编译,对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;
  3. 另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。

为了满足不同用户场景的需要,HotSpot 内置了多个即时编译器:

  1. C1 又叫做 Client 编译器,面向的是对启动性能有要求的客户端 GUI 程序,采用的优化手段相对简单,因此编译时间较短。
  2. C2 又叫做 Server 编译器,面向的是对峰值性能有要求的服务器端程序,采用的优化手段相对复杂,因此编译时间较长,但同时生成代码的执行效率较高。
  3. Graal 是 Java 10 正式引入的实验性即时编译器。

从 Java 7 开始,HotSpot 默认采用分层编译的方式:热点方法首先会被 C1 编译,而后热点方法中的热点会进一步被 C2 编译。

总结与实践

今天我简单介绍了 Java 代码为何在虚拟机中运行,以及如何在虚拟机中运行。

之所以要在虚拟机中运行,是因为它提供了可移植性。一旦 Java 代码被编译为 Java 字节码,便可以在不同平台上的 Java 虚拟机实现上运行。此外,虚拟机还提供了一个代码托管的环境,代替我们处理部分冗长而且容易出错的事务,例如内存管理。

Java 虚拟机将运行时内存区域划分为五个部分,分别为方法区、堆、PC 寄存器、Java 方法栈和本地方法栈。Java 程序编译而成的 class 文件,需要先加载至方法区中,方能在 Java 虚拟机中运行。

为了提高运行效率,标准 JDK 中的 HotSpot 虚拟机采用的是一种混合执行的策略。它会解释执行 Java 字节码,然后会将其中反复执行的热点代码,以方法为单位进行即时编译,翻译成机器码后直接运行在底层硬件之上。

HotSpot 装载了多个不同的即时编译器,以便在编译时间和生成代码的执行效率之间做取舍。

2019/12/14 posted in  极客-深入拆解Java虚拟机

02 | Java的基本类型

  • Java 虚拟机的 boolean 类型
  • Java 的基本类型
  • Java 基本类型的大小

Java 虚拟机的 boolean 类型

在 Java 虚拟机规范中,boolean 类型则被映射成 int 类型。具体来说,“true”被映射为整数 1,而“false”被映射为整数 0。

Java 的基本类型

除了上面提到的 boolean 类型外,Java 的基本类型还包括整数类型 byte、short、char、int 和 long,以及浮点类型 float 和 double。

Java 的基本类型都有对应的值域和默认值。可以看到,byte、short、int、long、float 以及 double 的值域依次扩大,而且前面的值域被后面的值域所包含。因此,从前面的基本类型转换至后面的基本类型,无需强制转换。另外一点值得注意的是,尽管他们的默认值看起来不一样,但在内存中都是 0。

Java 基本类型的大小

在第一篇中我曾经提到,Java 虚拟机每调用一个 Java 方法,便会创建一个栈帧。为了方便理解,这里我只讨论供解释器使用的解释栈帧(interpreted frame)。

这种栈帧有两个主要的组成部分,分别是局部变量区,以及字节码的操作数栈。这里的局部变量是广义的,除了普遍意义下的局部变量之外,它还包含实例方法的“this 指针”以及方法所接收的参数。

1.存储

在 Java 虚拟机规范中,局部变量区等价于一个数组,并且可以用正整数来索引。除了 long、double 值需要用两个数组单元来存储之外,其他基本类型以及引用类型的值均占用一个数组单元。

也就是说,boolean、byte、char、short 这四种类型,在上占用的空间和 int 是一样的,和引用类型也是一样的。因此,在 32 位的 HotSpot 中,这些类型在栈上将占用 4 个字节;而在 64 位的 HotSpot 中,他们将占 8 个字节。

当然,这种情况仅存在于局部变量,而并不会出现在存储于中的字段或者数组元素上。对于 byte、char 以及 short 这三种类型的字段或者数组单元,它们在堆上占用的空间分别为一字节、两字节,以及两字节,也就是说,跟这些类型的值域相吻合。

1.加载
讲完了存储,现在我来讲讲加载。Java 虚拟机的算数运算几乎全部依赖于操作数栈。也就是说,我们需要将堆中的 boolean、byte、char 以及 short 加载到操作数栈上,而后将栈上的值当成 int 类型来运算。

对于 boolean、char 这两个无符号类型来说,加载伴随着零扩展。举个例子,char 的大小为两个字节。在加载时 char 的值会被复制到 int 类型的低二字节,而高二字节则会用 0 来填充。

对于 byte、short 这两个类型来说,加载伴随着符号扩展。举个例子,short 的大小为两个字节。在加载时 short 的值同样会被复制到 int 类型的低二字节。如果该 short 值为非负数,即最高位为 0,那么该 int 类型的值的高二字节会用 0 来填充,否则用 1 来填充。

总结与实践

今天我介绍了 Java 里的基本类型。

其中,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 类型。

2019/12/14 posted in  极客-深入拆解Java虚拟机

03 | Java虚拟机是如何加载Java类的?

Java 语言的类型可以分为两大类:基本类型(primitive types)和引用类型(reference types)。

  • Java 的基本类型,它们是由 Java 虚拟机预先定义好的。
  • 引用类型,Java 将其细分为四种:类、接口、数组类和泛型参数。由于泛型参数会在编译过程中被擦除(我会在专栏的第二部分详细介绍),因此 Java 虚拟机实际上只有前三种。在类、接口和数组类中,数组类是由 Java 虚拟机直接生成的,其他两种则有对应的字节流。

说到字节流,最常见的形式要属由 Java 编译器生成的 class 文件。除此之外,我们也可以在程序内部直接生成,或者从网络中获取(例如网页中内嵌的小程序 Java applet)字节流。这些不同形式的字节流,都会被加载到 Java 虚拟机中,成为类或接口。

无论是直接生成的数组类,还是加载的类,Java 虚拟机都需要对其进行链接和初始化。

加载

加载,是指查找字节流,并且据此创建类的过程。对于数组类来说,它并没有对应的字节流,而是由 Java 虚拟机直接生成的。对于其他的类来说,Java 虚拟机则需要借助类加载器来完成查找字节流的过程。

启动类加载器是由 C++ 实现的,没有对应的 Java 对象,因此在 Java 中只能用 null 来指代。除了启动类加载器之外,其他的类加载器都是 java.lang.ClassLoader 的子类,因此有对应的 Java 对象。这些类加载器需要先由另一个类加载器,比如说启动类加载器,加载至 Java 虚拟机中,方能执行类加载。

双亲委派模型:每当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器。在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试去加载。

在 Java 9 之前,启动类加载器负责加载最为基础、最为重要的类,比如存放在 JRE 的 lib 目录下 jar 包中的类(以及由虚拟机参数 -Xbootclasspath 指定的类)。除了启动类加载器之外,另外两个重要的类加载器是扩展类加载器(extension class loader)和应用类加载器(application class loader),均由 Java 核心类库提供。

Java 9 引入了模块系统,并且略微更改了上述的类加载器。扩展类加载器被改名为平台类加载器(platform class loader)。Java SE 中除了少数几个关键模块,比如说 java.base 是由启动类加载器加载之外,其他的模块均由平台类加载器所加载。

在 Java 虚拟机中,类的唯一性是由类加载器实例以及类的全名一同确定的。即便是同一串字节流,经由不同的类加载器加载,也会得到两个不同的类。

链接

链接,是指将创建成的类合并至 Java 虚拟机中,使之能够执行的过程。它可分为验证、准备以及解析三个阶段。

验证阶段的目的,在于确保被加载类能够满足 Java 虚拟机的约束条件。

准备阶段的目的,则是为被加载类的静态字段分配内存。除了分配内存外,部分 Java 虚拟机还会在此阶段构造其他跟类层次相关的数据结构,比如说用来实现虚方法的动态绑定的方法表。

在 class 文件被加载至 Java 虚拟机之前,这个类无法知道其他类及其方法、字段所对应的具体地址,甚至不知道自己方法、字段的地址。因此,每当需要引用这些成员时,Java 编译器会生成一个符号引用。在运行阶段,这个符号引用一般都能够无歧义地定位到具体目标上。
解析阶段的目的,正是将这些符号引用解析成为实际引用。如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载(但未必触发这个类的链接以及初始化。)

Java 虚拟机规范并没有要求在链接过程中完成解析。它仅规定了:如果某些字节码使用了符号引用,那么在执行这些字节码之前,需要完成对这些符号引用的解析。

初始化

在 Java 代码中,如果要初始化一个静态字段,我们可以在声明时直接赋值,也可以在静态代码块中对其赋值。

如果直接赋值的静态字段被 final 所修饰,并且它的类型是基本类型或字符串时,那么该字段便会被 Java 编译器标记成常量值(ConstantValue),其初始化直接由 Java 虚拟机完成。除此之外的直接赋值操作,以及所有静态代码块中的代码,则会被 Java 编译器置于同一方法中,并把它命名为 < clinit >。

类加载的最后一步是初始化,便是为标记为常量值的字段赋值,以及执行 < clinit > 方法的过程。Java 虚拟机会通过加锁来确保类的 < clinit > 方法仅被执行一次。

那么,类的初始化何时会被触发呢?JVM 规范枚举了下述多种触发情况:

  1. 当虚拟机启动时,初始化用户指定的主类;
  2. 当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类;
  3. 当遇到调用静态方法的指令时,初始化该静态方法所在的类;
  4. 当遇到访问静态字段的指令时,初始化该静态字段所在的类;
  5. 子类的初始化会触发父类的初始化;
  6. 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;
  7. 使用反射 API 对某个类进行反射调用时,初始化这个类;
  8. 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。

总结与实践

今天我介绍了 Java 虚拟机将字节流转化为 Java 类的过程。这个过程可分为加载、链接以及初始化三大步骤。

加载是指查找字节流,并且据此创建类的过程。加载需要借助类加载器,在 Java 虚拟机中,类加载器使用了双亲委派模型,即接收到加载请求时,会先将请求转发给父类加载器。

链接,是指将创建成的类合并至 Java 虚拟机中,使之能够执行的过程。链接还分验证、准备和解析三个阶段。其中,解析阶段为非必须的。

初始化,则是为标记为常量值的字段赋值,以及执行 < clinit > 方法的过程。类的初始化仅会被执行一次,这个特性被用来实现单例的延迟初始化。

2019/12/14 posted in  极客-深入拆解Java虚拟机

04 | JVM是如何执行方法调用的?(上)

2019/12/14 posted in  极客-深入拆解Java虚拟机

05 | JVM是如何执行方法调用的?(下)

2019/12/14 posted in  极客-深入拆解Java虚拟机

06 | JVM是如何处理异常的?

2019/12/14 posted in  极客-深入拆解Java虚拟机

07 | JVM是如何实现反射的?

2019/12/14 posted in  极客-深入拆解Java虚拟机

08 | JVM是怎么实现invokedynamic的?(上)

2019/12/14 posted in  极客-深入拆解Java虚拟机

09 | JVM是怎么实现invokedynamic的?(下)

2019/12/14 posted in  极客-深入拆解Java虚拟机

10 | Java对象的内存布局

2019/12/14 posted in  极客-深入拆解Java虚拟机

11 | 垃圾回收(上)

2019/12/14 posted in  极客-深入拆解Java虚拟机

12 | 垃圾回收(下)

2019/12/14 posted in  极客-深入拆解Java虚拟机

13 | Java内存模型

2019/12/14 posted in  极客-深入拆解Java虚拟机

14 | Java虚拟机是怎么实现synchronized的?

2019/12/14 posted in  极客-深入拆解Java虚拟机

15 | Java语法糖与Java编译器

2019/12/14 posted in  极客-深入拆解Java虚拟机

16 | 即时编译(上)

2019/12/14 posted in  极客-深入拆解Java虚拟机

17 | 即时编译(下)

2019/12/14 posted in  极客-深入拆解Java虚拟机

18 | 即时编译器的中间表达形式

2019/12/14 posted in  极客-深入拆解Java虚拟机

19 | Java字节码(基础篇)

2019/12/14 posted in  极客-深入拆解Java虚拟机

20 | 方法内联(上)

2019/12/14 posted in  极客-深入拆解Java虚拟机

21 | 方法内联(下)

2019/12/14 posted in  极客-深入拆解Java虚拟机

22 | HotSpot虚拟机的intrinsic

2019/12/14 posted in  极客-深入拆解Java虚拟机

23 | 逃逸分析

2019/12/14 posted in  极客-深入拆解Java虚拟机

24 | 字段访问相关优化

2019/12/14 posted in  极客-深入拆解Java虚拟机

25 | 循环优化

2019/12/14 posted in  极客-深入拆解Java虚拟机

26 | 向量化

2019/12/14 posted in  极客-深入拆解Java虚拟机

27 | 注解处理器

2019/12/14 posted in  极客-深入拆解Java虚拟机

28 | 基准测试框架JMH(上)

2019/12/14 posted in  极客-深入拆解Java虚拟机

29 | 基准测试框架JMH(下)

2019/12/14 posted in  极客-深入拆解Java虚拟机

30 | Java虚拟机的监控及诊断工具(命令行篇)

2019/12/14 posted in  极客-深入拆解Java虚拟机

31 | Java虚拟机的监控及诊断工具(GUI篇)

2019/12/14 posted in  极客-深入拆解Java虚拟机

32 | JNI的运行机制

2019/12/14 posted in  极客-深入拆解Java虚拟机

33 | Java Agent与字节码注入

2019/12/14 posted in  极客-深入拆解Java虚拟机

34 | Graal:用Java编译Java

2019/12/14 posted in  极客-深入拆解Java虚拟机

35 | Truffle:语言实现框架

2019/12/14 posted in  极客-深入拆解Java虚拟机

36 | SubstrateVM:AOT编译框架

2019/12/14 posted in  极客-深入拆解Java虚拟机

37 尾声 | 道阻且长,努力加餐

2019/12/14 posted in  极客-深入拆解Java虚拟机