学习笔记【Java 虚拟机③】类加载与字节码技术

发布于:2022-12-25 ⋅ 阅读:(923) ⋅ 点赞:(0)

若文章内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系博主删除。


总目录



前言


  • 参考书籍《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明

(三)类加载与字节码技术


本博客内容概览
  1. 类文件结构
  2. 字节码指令
  3. 编译期处理
  4. 类加载阶段
  5. 类加载器
  6. 运行期优化

在这里插入图片描述

JVM 结构图

13.类文件结构


建议诸位看书:《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明 ,此处仅摘抄了书中的一部分。


类文件简述

Class 文件是一组以 8 个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件之中,中间没有添加任何分隔符,这使得整个 Class 文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。

当遇到需要占用 8 个字节以上空间的数据项时,则会按照高位在前的方式分割成若干个 8 个字节进行存储。

根据《Java虚拟机规范》的规定,Class 文件格式采用一种类似于 C 语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:“无符号数” 和 “表”。后面的解析都要以这两种数据类型为基础,所以这里笔者必须先解释清楚这两个概念。

  • 无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表 1 个字节、2 个字节、4 个字节和 8 个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串值。
  • 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名都习惯性地以 “_info” 结尾。表用于描述有层次关系的复合结构的数据,整个 Class 文件本质上也可以视作是一张表

无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时候称这一系列连续的某一类型的数据为某一类型的 “集合”。


魔数

每个 Class 文件的头 4 个字节被称为魔数(Magic Number)。

它的唯一作用是确定这个文件是否为一个能被虚拟机接受的 Class 文件。

Class 文件的魔数取得很有 “浪漫气息”,值为 0xCAFEBABE(咖啡宝贝?)


版本号

紧接着魔数的 4 个字节存储的是 Class 文件的版本号

  • 第 5 和第 6 个字节是次版本号(Minor Version)
  • 第 7 和第 8 个字节是主版本号(Major Version)

常量池

紧接着主、次版本号之后的是常量池入口

常量池可以比喻为 Class 文件里的资源仓库

它是 Class 文件结构中与其他项目关联最多的数据,通常也是占用 Class 文件空间最大的数据项目之一

另外,它还是在 Class 文件中第一个出现的表类型数据项目。

常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。

常量池中每一项常量都是一个表,截至 JDK 13,常量表中分别有 17 种不同类型的常量。


访问标志

在常量池结束之后,紧接着的 2 个字节代表访问标志(access_flags)

这个标志用于识别一些类或者接口层次的访问信息,包括:

  • 这个 Class 是类还是接口;是否定义为 public 类型;是否定义为 abstract 类型;
  • 如果是类的话,是否被声明为 final;等等。

类索引父类索引与接口索引集合

类索引、父类索引和接口索引集合都按顺序排列在访问标志之后。

类索引(this_class)和父类索引(super_class)都是一个 u2 类型的数据,而接口索引集合(interfaces)是一组 u2 类型的数据的集合

Class 文件中由这三项数据来确定该类型的继承关系。

类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。

由于 Java 语言不允许多重继承,所以父类索引只有一个,除了 java.lang.Object 之外,所有的 Java 类都有父类

因此除了 java.lang.Object 外,所有 Java 类的父类索引都不为 0。

接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按 implements 关键字后的接口顺序从左到右排列在接口索引集合中。(如果这个 Class 文件表示的是一个接口,则应当是 extends 关键字)


字段表集合

字段表(field_info)用于描述接口或者类中声明的变量。

Java 语言中的 “字段”(Field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。

读者可以回忆一下在 Java 语言中描述一个字段可以包含哪些信息。

字段可以包括的修饰符有字段的作用域(public、private、protected 修饰符)、是实例变量还是类变量(static 修饰符)、可变性(final)、并发可见性(volatile 修饰符,是否强制从主内存读写)、可否被序列化(transient 修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。

上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。

而字段叫做什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。

字段修饰符放在 access_flags 项目中,它与类中的 access_flags 项目是非常类似的,都是一个 u2 的数据类型

跟随 access_flags 标志的是两项索引值:name_index 和 descriptor_index。

它们都是对常量池项的引用,分别代表着字段的简单名称以及字段和方法的描述符。

其中描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。

字段表所包含的固定数据项目到 descriptor_index 为止就全部结束了。

不过在 descrip-tor_index 之后跟随着一个属性表集合,用于存储一些额外的信息,字段表可以在属性表中附加描述零至多项的额外
信息。

在这里插入图片描述


方法表集合

Class 文件存储格式中对方法的描述与对字段的描述采用了几乎完全一致的方式,方法表的结构如同字段表一样,依次包括访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项 … …

方法的定义可以通过访问标志、名称索引、描述符索引来表达清楚,但方法里面的代码去哪里了?

方法里的 Java 代码,经过 Javac 编译器编译成字节码指令之后,存放在方法属性表集合中一个名为 “Code” 的属性里面。

属性表是 Class文件格式中最具扩展性的一种数据项目。

在这里插入图片描述


属性表集合

属性表(attribute_info)在前面的讲解之中已经出现过数次,Class文件、字段表、方法表都可以携带自己的属性表集合,以描述某些场景专有的信息。

与 Class 文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格顺序,并且《Java 虚拟机规范》允许只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java 虚拟机运行时会忽略掉它不认识的属性。

为了能正确解析 Class 文件,《Java 虚拟机规范》最初只预定义了 9 项所有 Java 虚拟机实现都应当能识别的属性。

而在最新的《Java 虚拟机规范》的 Java SE 12 版本中,预定义属性已经增加到 29 项

对于每一个属性,它的名称都要从常量池中引用一个 CONSTANT_Utf8_info 类型的常量来表示,而属性值的结构则是完全自定义的,只需要通过一个 u4 的长度属性去说明属性值所占用的位数即可。


官方文档链接https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html


这里推荐一个插件:jclasslib Bytecode Viewer,可借助此插件来帮助我们阅读字节码文件。

在这里插入图片描述

在这里插入图片描述


一个简单的 HelloWorld.java

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

执行 javac -parameters -d . HellowWorld.java

编译为 HelloWorld.class 后是这个样子的

[root@localhost ~]# od -t xC HelloWorld.class
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07
0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29
0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e
0000100 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63
0000120 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01
0000140 00 04 74 68 69 73 01 00 1d 4c 63 6e 2f 69 74 63
0000160 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c 6f
0000200 57 6f 72 6c 64 3b 01 00 04 6d 61 69 6e 01 00 16
0000220 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72
0000240 69 6e 67 3b 29 56 01 00 04 61 72 67 73 01 00 13
0000260 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69
0000300 6e 67 3b 01 00 10 4d 65 74 68 6f 64 50 61 72 61
0000320 6d 65 74 65 72 73 01 00 0a 53 6f 75 72 63 65 46
0000340 69 6c 65 01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64
0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e
0000400 00 1f 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64
0000420 07 00 20 0c 00 21 00 22 01 00 1b 63 6e 2f 69 74
0000440 63 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c
0000460 6f 57 6f 72 6c 64 01 00 10 6a 61 76 61 2f 6c 61
0000500 6e 67 2f 4f 62 6a 65 63 74 01 00 10 6a 61 76 61
0000520 2f 6c 61 6e 67 2f 53 79 73 74 65 6d 01 00 03 6f
0000540 75 74 01 00 15 4c 6a 61 76 61 2f 69 6f 2f 50 72
0000560 69 6e 74 53 74 72 65 61 6d 3b 01 00 13 6a 61 76
0000600 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d
0000620 01 00 07 70 72 69 6e 74 6c 6e 01 00 15 28 4c 6a
0000640 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
0000700 00 07 00 08 00 01 00 09 00 00 00 2f 00 01 00 01
0000720 00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a 00
0000740 00 00 06 00 01 00 00 00 04 00 0b 00 00 00 0c 00
0000760 01 00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e 00
0001000 0f 00 02 00 09 00 00 00 37 00 02 00 01 00 00 00
0001020 09 b2 00 02 12 03 b6 00 04 b1 00 00 00 02 00 0a
0001040 00 00 00 0a 00 02 00 00 00 06 00 08 00 07 00 0b
0001060 00 00 00 0c 00 01 00 00 00 09 00 10 00 11 00 00
0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00
0001120 00 00 02 00 14

根据 JVM 规范,类文件结构如下

ClassFile {
	u4 magic;
	u2 minor_version;
	u2 major_version;
	u2 constant_pool_count;
	cp_info constant_pool[constant_pool_count-1];
	u2 access_flags;
	u2 this_class;
	u2 super_class;
	u2 interfaces_count;
	u2 interfaces[interfaces_count];
	u2 fields_count;
	field_info fields[fields_count];
	u2 methods_count;
	method_info methods[methods_count];
	u2 attributes_count;
	attribute_info attributes[attributes_count];
}

14.字节码指令


14.1.入门体验


研究一下两组字节码指令

  • 一个是 public cn.itcast.jvm.t5.HelloWorld(); 构造方法的字节码指令
  • 另一个是 public static void main(java.lang.String[]); 主方法的字节码指令

  • public cn.itcast.jvm.t5.HelloWorld(); 构造方法的字节码指令
2a b7 00 01 b1
  1. 2a => aload_0 加载 slot 0 的局部变量,即 this,做为下面的 invokespecial 构造方法调用的参数
  2. b7 => invokespecial 预备调用构造方法,哪个方法呢?
  3. 00 01 引用常量池中 #1 项,即 Method java/lang/Object."<init>":()V
  4. b1 表示返回

  • public static void main(java.lang.String[]); 主方法的字节码指令
b2 00 02 12 03 b6 00 04 b1
  1. b2 => getstatic 用来加载静态变量,哪个静态变量呢?
  2. 00 02 引用常量池中 #2 项,即 Field java/lang/System.out:Ljava/io/PrintStream;
  3. 12 => ldc 加载参数,哪个参数呢?
  4. 03 引用常量池中 #3 项,即 String hello world
  5. b6 => invokevirtual 预备调用成员方法,哪个方法呢?
  6. 00 04 引用常量池中 #4 项,即 Method java/io/PrintStream.println:(Ljava/lang/String;)V
  7. b1 表示返回

14.2.javap(JAVA 字节码分析工具)


自己分析类文件结构太麻烦了,Oracle 提供了 javap (JAVA 字节码分析工具)来反编译 class 文件(javap -v HelloWorld.class

D:\IdeaProjects\tests\test_7\test_7a\out\production\test_7a>javap -v HelloWorld.class
Classfile /D:/IdeaProjects/tests/test_7/test_7a/out/production/test_7a/HelloWorld.class
  Last modified 2022-8-29; size 534 bytes
  MD5 checksum f085642c9a175e363b8cde7b90538c83
  Compiled from "HelloWorld.java"
public class HelloWorld 
  minor version: 0
  major version: 52 # 代表 jdk8
  flags: ACC_PUBLIC, ACC_SUPER # 类的访问修饰符
Constant pool: # 常量池
   #1 = Methodref          #6.#20         // java/lang/Object."<init>":()V
   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #23            // Hello World!
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #26            // HelloWorld
   #6 = Class              #27            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               LHelloWorld;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               HelloWorld.java
  #20 = NameAndType        #7:#8          // "<init>":()V
  #21 = Class              #28            // java/lang/System
  #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #23 = Utf8               Hello World!
  #24 = Class              #31            // java/io/PrintStream
  #25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
  #26 = Utf8               HelloWorld
  #27 = Utf8               java/lang/Object
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (Ljava/lang/String;)V
{ # 方法信息
  public HelloWorld();
    descriptor: ()V # 描述符:方法的参数列表和返回值。无返回值的 void 类型用一个大写字符 V 表示
    flags: ACC_PUBLIC # 访问标志
    Code: # 程序方法体里的代码
      stack=1, locals=1, args_size=1 # 操作数栈的深度的最大值,局部变量表所需的存储空间,参数长度
         0: aload_0 # 把局部变量的第 0 项加载到操作数栈
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V # 调用常量池中的第 1 项方法
         4: return
      LineNumberTable: # 用于描述 [Java 源码行号] 与 [字节码行号(字节码偏移量)] 之间的对应关系
        line 1: 0 
      LocalVariableTable: # 用于描述 [栈帧中局部变量表的变量] 与 [JAVA 源码中定义的变量] 之间的关系
        Start  Length  Slot  Name   Signature
            0       5     0  this   LHelloWorld;
		# Start、Length:[局部变量表] 的 [生命周期] 开始的 [字节码偏移量] 及其 [作用范围覆盖的长度]
		# 上述二者集合即为该 [局部变量] 在 [字节码] 中的 [作用域范围]
		# Slot:这个局部变量在栈帧的局部变量表中变量槽的位置
		# Name:局部变量的名称
		# Signature:字段的特征签名,功能上算是描述符的加强版(添加了准确描述泛型类型的功能)
		# 描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型、顺序)和返回值
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC 
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello World!
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 3: 0
        line 4: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"

14.3.图解方法执行流程


14.3.1.原始代码和字节码文件


原始代码

public class Demo3_1 {
    public static void main(String[] args) {
        int a = 10;
        int b = Short.MAX_VALUE + 1;
        int c = a + b;
        System.out.println(c);
    }
}

编译后的字节码文件

Classfile /D:/IdeaProjects/Study/wStudy2022/JVMStudies/chapter03/target/classes/Demo3_1.class
  Last modified 2022-8-31; size 583 bytes
  MD5 checksum 20278f2ddca7abcf4d365af690530fcf
  Compiled from "Demo3_1.java"
public class Demo3_1
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #7.#25         // java/lang/Object."<init>":()V
   #2 = Class              #26            // java/lang/Short
   #3 = Integer            32768
   #4 = Fieldref           #27.#28        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = Methodref          #29.#30        // java/io/PrintStream.println:(I)V
   #6 = Class              #31            // Demo3_1
   #7 = Class              #32            // java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               LocalVariableTable
  #13 = Utf8               this
  #14 = Utf8               LDemo3_1;
  #15 = Utf8               main
  #16 = Utf8               ([Ljava/lang/String;)V
  #17 = Utf8               args
  #18 = Utf8               [Ljava/lang/String;
  #19 = Utf8               a
  #20 = Utf8               I
  #21 = Utf8               b
  #22 = Utf8               c
  #23 = Utf8               SourceFile
  #24 = Utf8               Demo3_1.java
  #25 = NameAndType        #8:#9          // "<init>":()V
  #26 = Utf8               java/lang/Short
  #27 = Class              #33            // java/lang/System
  #28 = NameAndType        #34:#35        // out:Ljava/io/PrintStream;
  #29 = Class              #36            // java/io/PrintStream
  #30 = NameAndType        #37:#38        // println:(I)V
  #31 = Utf8               Demo3_1
  #32 = Utf8               java/lang/Object
  #33 = Utf8               java/lang/System
  #34 = Utf8               out
  #35 = Utf8               Ljava/io/PrintStream;
  #36 = Utf8               java/io/PrintStream
  #37 = Utf8               println
  #38 = Utf8               (I)V
{
  public Demo3_1(); # 构造方法
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 4: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LDemo3_1;

  public static void main(java.lang.String[]); # main 方法
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        10
         2: istore_1
         3: ldc           #3                  // int 32768
         5: istore_2
         6: iload_1
         7: iload_2
         8: iadd
         9: istore_3
        10: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        13: iload_3
        14: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
        17: return
      LineNumberTable:
        line 6: 0
        line 7: 3
        line 8: 6
        line 9: 10
        line 10: 17
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      18     0  args   [Ljava/lang/String;
            3      15     1     a   I
            6      12     2     b   I
           10       8     3     c   I
}
SourceFile: "Demo3_1.java"

14.3.2.常量池载入运行时常量池


注意运行时常量池也是方法区的组成部分,这里只为了区别突出运行时常量池,才在图中将其与方法区分开。

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

在这里插入图片描述

当 Java 代码被执行时,Java 虚拟机的类加载器,会对上面代码中的 main 方法所在的类进行类加载的操作。

  • Java 虚拟机把描述类的数据从 Class 文件加载到内存
  • 并对数据进行校验转换解析和初始化
  • 最终形成可以被虚拟机直接使用的 Java 类型
  • 该过程被称为虚拟机的类加载机制

字节码文件中的常量池的数据会放入运行时常量池中

Java 源码中一些较小的数值并不是存储在常量池的,而是跟着这个方法的字节码指令存在一起。

一旦这个数值的大小超过了这个 short 的最大值(32767),那么它就会存储在常量池中


14.3.3.方法字节码载入方法区


在这里插入图片描述


14.3.4.分配栈帧内存


main 线程开始运行,分配栈帧内存(stack=2, locals=4)

在这里插入图片描述


接下来就是执行引擎执行字节码的内容了

执行引擎执行字节码

14.3.5.a 赋值


  • bipush 将一个 byte 压入操作数栈(其长度会补齐 4 个字节)
  • sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)
  • ldc 将一个 int 压入操作数栈
  • ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节)
  • 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池

在这里插入图片描述


  • istore 1:将操作数栈顶数据弹出,存入局部变量表的 slot 1

在这里插入图片描述
在这里插入图片描述


14.3.6.b 赋值


  • ldc #3:从常量池加载 #3 数据到操作数栈
  • 注意Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算好的

在这里插入图片描述


  • istore 2:将操作数栈顶数据弹出,存入局部变量表的 slot 2

在这里插入图片描述

在这里插入图片描述


14.3.7.a + b =c


a + b 运算操作是要栈中完成的,在局部变量表中不能完成运算


  • iload 1:将局部变量表中 1 号位置的数据读取出来放到操作数栈中

在这里插入图片描述


  • iload 2:将局部变量表中 2 号位置的数据读取出来放到操作数栈中

在这里插入图片描述


  • iadd:弹出操作数栈中的两个元素并相加,之后将运算结果压入操作数栈中

在这里插入图片描述
在这里插入图片描述


  • istore 3:将操作数栈顶数据弹出,存入局部变量表的 slot 3

在这里插入图片描述
在这里插入图片描述


14.3.8.System.out


  • getstatic #4
    • 在常量池中找到成员变量的引用 #4,发现其为对象引用(对象位于堆中)
    • 获取该对象的引用地址,并放入操作数栈中

在这里插入图片描述
在这里插入图片描述


14.3.9.打印


  • iload 3:将局部变量表中 slot 3 中的数据压入操作数栈中

在这里插入图片描述
在这里插入图片描述


  • invokevirtual #5
    • 找到常量池 #5
    • 定位到方法区 java/io/PrintStream.println:(I)V 方法
    • 生成新的栈帧(分配 locals、stack 等)
    • 传递参数,执行新栈帧中的字节码

在这里插入图片描述

  • 执行完毕,弹出栈帧
  • 清除 main 操作数栈内容

在这里插入图片描述


  • return:完成 main 方法调用,弹出 main 栈帧

最终整个程序结束


14.4.从字节码角度分析 i++


14.4.1.源码


/**
 * 从字节码角度分析 a ++ 相关题目
 */
public class Demo3_2 {
    public static void main(String[] args) {
        int a = 10;
        int b = a++ + ++a + a--;
        System.out.println(a);
        System.out.println(b);
    }
}

14.4.2.字节码


public static void main(java.lang.String[]);
	descriptor: ([Ljava/lang/String;)V
	flags: (0x0009) ACC_PUBLIC, ACC_STATIC
	Code:
		stack=2, locals=3, args_size=1
			0: bipush 10
			2: istore_1
			3: iload_1
			4: iinc 1, 1
			7: iinc 1, 1
			10: iload_1
			11: iadd
			12: iload_1
			13: iinc 1, -1
			16: iadd
			17: istore_
			17: istore_2
			18: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
			21: iload_1
			22: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
			25: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
			28: iload_2
			29: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
			32: return
		LineNumberTable:
			line 8: 0
			line 9: 3
			line 10: 18
			line 11: 25
			line 12: 32
		LocalVariableTable:
			Start Length Slot Name Signature
				0     33    0 args [Ljava/lang/String;
				3 	  30    1   a  I
				18 	  15    2   b  I

14.4.3.分析时的注意事项


  • 注意iinc 指令是直接在局部变量 slot 上进行运算,该指令为局部变量自增指令
  • a++++a 的区别是先执行 ①iload 还是先执行 ②iinc
    • a++ 是先①后②,a-- 是先②后①
# 接下来我们分析的主要是下面这段字节码,后面的打印部分的字节码就不看了
	Code:
		stack=2, locals=3, args_size=1
			0: bipush 10
			2: istore_1
			3: iload_1
			4: iinc 1, 1
			7: iinc 1, 1
			10: iload_1
			11: iadd
			12: iload_1
			13: iinc 1, -1
			16: iadd
			17: istore_
			17: istore_2

14.4.4.a 赋值


bipush 10

在这里插入图片描述


istore_1

在这里插入图片描述

自此,a = 10 操作完成


14.4.5.a++


iload 1

在这里插入图片描述


iinc 1,1:该指令有两个参数,第一个 1 是代表要执行该指令的槽位,第二个 1 是要自增的值

在这里插入图片描述


14.4.6.++a


iinc 1,1

在这里插入图片描述


iload 1

在这里插入图片描述


14.4.7.第一次运算


a++ + ++a

a++++a 的操作之前已经演示过了,故此处直接演示 iadd


iadd

在这里插入图片描述


14.4.8.a–


iload 1

在这里插入图片描述


iinc 1,-1

在这里插入图片描述


14.4.9.第二次运算


a++ + ++a + a--

a++ + ++a 的运算和 a– 的自减运算,前面都已经做过了,故此处直接演示第二次运算


iadd

在这里插入图片描述


14.4.10.b 赋值


istore 2

在这里插入图片描述


14.5.条件判断


几点说明

  • byte,short,char 都会按 int 比较,因为操作数栈都是 4 字节
  • goto 用来进行跳转到指定行号的字节码

更多指令可查阅:https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.lcmp


源码

public class Demo3_3 {
    public static void main(String[] args) {
        int a = 0;
        if (a == 0) {
            a = 10;
        } else {
            a = 20;
        }
    }
}

字节码

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=2, args_size=1
         0: iconst_0			# iconst 可以将 -1~5 之间的常数加载至操作数栈
         1: istore_1
         2: iload_1
         3: ifne          12	# 判断条件(此处为 a=0),条件不成立则跳转到下面的 12 行(12:bipush 20),条件成立则继续往下
         6: bipush        10
         8: istore_1
         9: goto          15
        12: bipush        20
        14: istore_1
        15: return

14.6.循环控制


  • while 循环
public class Demo3_4 {
    public static void main(String[] args) {
        int a = 0;
        while (a < 10) {
            a++;
        }
    }
}
Code:
  stack=2, locals=2, args_size=1
     0: iconst_0
     1: istore_1
     2: iload_1
     3: bipush        10
     5: if_icmpge     14 # 判断条件是否成立,成立则继续向下执行;否则,直接跳转到 14 行(14:return)
     8: iinc          1, 1
    11: goto          2
    14: return

  • do while 循环
public class Demo3_5 {
    public static void main(String[] args) {
        int a = 0;
        do {
            a++;
        } while (a < 10);
    }
}
Code:
  stack=2, locals=2, args_size=1
     0: iconst_0
     1: istore_1
     2: iinc          1, 1
     5: iload_1
     6: bipush        10
     8: if_icmplt     2
    11: return

  • for 循环
public class Demo3_6 {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
        }
    }
}
Code:
  stack=2, locals=2, args_size=1
     0: iconst_0
     1: istore_1
     2: iload_1
     3: bipush        10
     5: if_icmpge     14
     8: iinc          1, 1
    11: goto          2
    14: return

比较 while 和 for 的字节码,发现它们是一模一样的,殊途也能同归


14.7.分析 a = 0


public class Demo3_7 {
    public static void main(String[] args) {
        int i = 0;
        int x = 0;
        while (i < 10) {
        	// x 先在局部变量表上的槽位 自增(a 变为 1)
        	// 之后将操作数栈的值(也就是初始值 0)赋值给 x
        	// 故 x 值并未改变,依旧为 0
            x = x++; 
            
            i++;
        }
        System.out.println(x); // 结果是 0
    }
}

复习回顾相关的指令(以下命令中 i 开头的都是指 int 类型)

  • iconst:将一个常量加载到操作数栈
  • iload:将一个局部变量加载到操作数栈
  • istore:将一个数值从操作数栈加载到局部变量表中
  • bipush:将一个 byte 类型的局部变量加载到操作数栈中,本质上是使用相应的对 int 类型作为运算类型来进行的
  • ifcpmge:条件分支判断
  • iinc:局部变量自增(在局部变量表的槽上进行该操作)
  • gestatic:访问类字段(static 字段,或者称为类变量)
  • invokevirtual:用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),是 Java 语言中最常见的方法分派方式

Code:
     stack=2, locals=3, args_size=1		# 操作数栈分配 2 个空间,局部变量表分配 3 个空间,参数的长度是 1
        0: iconst_0						# 准备一个常数 0(将一个常量加载到操作数栈)
        1: istore_1						# 将常数 0 放入局部变量表的 1 号槽位,此时 i = 0
        									# 将操作数栈的栈顶元素存储到局部变量表中
        2: iconst_0						# 准备一个常数 0
        3: istore_2						# 将常数 0 放入局部变量的 2 号槽位 x = 0	
        4: iload_1						# 将局部变量表 1 号槽位的数放入操作数栈中
        5: bipush        10				# 将数字 10 放入操作数栈中,此时操作数栈中有 2 个数
        7: if_icmpge     21				# 比较操作数栈中的两个数,如果下面的数大于上面的数,就跳转到 21 。
        									# 这里的比较是将两个数做减法。
        									# 因为涉及运算操作,所以会将两个数弹出操作数栈来进行运算。
        									# 运算结束后操作数栈为空
       10: iload_2						# 将局部变量 2 号槽位的数放入操作数栈中,放入的值是 0 
       11: iinc          2, 1			# 将局部变量 2 号槽位的数加 1 ,自增后,槽位中的值变为了 1 
       14: istore_2						# 将操作数栈中的数放入到局部变量表的 2 号槽位,2 号槽位的值又变为了 0
       15: iinc          1, 1 			# 1 号槽位的值自增 1 
       18: goto          4 				# 跳转到第 4 条指令
       21: getstatic     #2             // Field java/lang/System.out:Ljava/io/PrintStream;
       24: iload_2
       25: invokevirtual #3             // Method java/io/PrintStream.println:(I)V
       28: return

14.8.构造方法


14.8.1.cinit


public class Demo3_8_1 {

    static int i = 10;

    static {
        i = 20;
    }

    static {
        i = 30;
    }
	
	//最终 i 的值是 30
}

编译器会按从上至下的顺序,收集所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方法 <cinit>()V

<cinit>()V 是每个类的构造方法

Code:
  stack=1, locals=0, args_size=0
     0: bipush        10
     2: putstatic     #2                  // Field i:I
     5: bipush        20
     7: putstatic     #2                  // Field i:I
    10: bipush        30
    12: putstatic     #2                  // Field i:I
    15: return

<cinit>()V 方法会在类加载的初始化阶段被调用


14.8.2.init


<init>()V:每个实例对象的构造方法

public class Demo3_8_2 {

    private String a = "s1";

    {
        b = 20;
    }

    private int b = 10;

    {
        a = "s2";
    }

    public Demo3_8_2(String a, int b) {
        this.a = a;
        this.b = b;
    }

    public static void main(String[] args) {
        Demo3_8_2 d = new Demo3_8_2("s3", 30);
        System.out.println(d.a); // s3
        System.out.println(d.b); // 30
    }

}

编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法

但原始构造方内的代码也会附加到新的构造方法中,且总是在最后

public cn.itcast.jvm.t3.bytecode.Demo3_8_2(java.lang.String, int);
	descriptor: (Ljava/lang/String;I)V
	flags: ACC_PUBLIC
	Code:
		stack=2, locals=3, args_size=3
			0: aload_0
			1: invokespecial 			#1 		// super.<init>()V
			4: aload_0
			5: ldc           			#2 		// <- "s1"
			7: putfield 				#3 		// -> this.a
			10: aload_0
			11: bipush 					20 		// <- 20
			13: putfield 				#4 		// -> this.b
			16: aload_0
			17: bipush 					10 		// <- 10
			19: putfield 				#4		// -> this.b
			22: aload_0
			23: ldc 					#5 		// <- "s2"
			25: putfield 				#3 		// -> this.a
			28: aload_0 						// ------------------------------
			29: aload_1 						// <- slot 1(a) "s3" 			|
			30: putfield			 	#3 		// -> this.a 					|
			33: aload_0 														|
			34: iload_2 						// <- slot 2(b) 30 				|
			35: putfield 				#4 		// -> this.b --------------------
			38: return
		LineNumberTable: ...
		LocalVariableTable:
			Start  Length  Slot  Name  Signature
			0 	   39 	   0    this   Lcn/itcast/jvm/t3/bytecode/Demo3_8_2;
			0 	   39 	   1    a 	   Ljava/lang/String;
			0 	   39 	   2    b 	   I
	MethodParameters: ...

14.8.3.概念补充


参考书籍《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明

实例构造器 <init>() 方法类构造器 <clinit>() 方法 就是在字节码生成阶段被添加到语法树之中的。

请注意这里的实例构造器并不等同于默认构造函数

如果用户代码中没有提供任何构造函数

  • 那编译器将会添加一个没有参数的、可访问性(publicprotectedprivate<package>)与当前类型一致的默认构造函数
  • 这个工作在填充符号表阶段中就已经完成

<init>()<clinit>() 这两个构造器的产生实际上是一种代码收敛的过程,编译期会进行如下操作:

  • 语句块初始化(对于实例构造器而言是 “{}” 块,对于类构造器而言是 “static{}” 块)
  • 变量初始化(实例变量和类变量)
  • 调用父类的实例构造器等操作收敛到 <init>()<clinit>() 方法之中
    • 仅仅是实例构造器
    • <clinit>() 方法中无须调用父类的 <clinit>() 方法,Java 虚拟机会自动保证父类构造器的正确执行
    • 但在 <clinit>() 方法中经常会生成调用 java.lang.Object<init>() 方法的代码
  • 并且保证无论源码中出现的顺序如何,都一定是按先执行父类的实例构造器然后初始化变量最后执行语句块的顺序进行
  • 上面所述的动作由 Gen::normalizeDefs() 方法来实现。

除了生成构造器以外,还有其他的一些代码替换工作用于优化程序某些逻辑的实现方式。

如把字符串的加操作替换为 StringBufferStringBuilder(取决于目标代码的版本是否大于或等于 JDK 5)的 append() 操作,等等。


14.9.方法调用


看一下几种不同的方法调用对应的字节码指令


public class Demo3_9 {

    public Demo3_9() { }

    private void test1() { }

    private final void test2() { }

    public void test3() { }

    public static void test4() { }

    @Override
    public String toString() { return super.toString(); }

    public static void main(String[] args) {
        Demo3_9 d = new Demo3_9();
        d.test1();
        d.test2();
        d.test3();
        d.test4();
        Demo3_9.test4();
        d.toString();
    }
    
}

public static void main(java.lang.String[]);
  descriptor: ([Ljava/lang/String;)V
  flags: ACC_PUBLIC, ACC_STATIC
  Code:
    stack=2, locals=2, args_size=1
       0: new           #3                  // class bytecode/Demo3_9
       										# new 指令实际上做了两部操作
       											# 1.在堆空间分配内存
       											# 2.分配成功后会把对象的引用放入操作数栈
       3: dup								# 复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶
       4: invokespecial #4                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: invokespecial #5                  // Method test1:()V
      12: aload_1
      13: invokespecial #6                  // Method test2:()V
      16: aload_1
      17: invokevirtual #7                  // Method test3:()V
      20: aload_1
      21: pop								# 将操作数栈的栈顶一个或两个元素出栈
	      										# 此处执行 pop 指令,是因为 test4() 是静态方法
	      										# 静态方法不需要对象来调用,所以静态方法一调用后就直接出栈了
	      										# 所以平时也不要使用对象来调用静态方法了
	      										# 不然会多产生一些不必要的指令。比如此处的 aload 和 pop
      22: invokestatic  #8                  // Method test4:()V
      25: invokestatic  #8                  // Method test4:()V
      28: aload_1
      29: invokevirtual #9                  // Method toString:()Ljava/lang/String;
      32: pop
      33: return

  • new 是创建 对象,给对象分配堆内存,执行成功会将 对象引用 压入操作数栈
  • dup 是赋值操作数栈栈顶的内容
    • 本例即为 对象引用。为什么需要两份引用呢?
      • 一个是要配合 invokespecial 调用该对象的构造方法 "<init>":()V (会消耗掉栈顶一个引用)
      • 另一个要配合 astore_1 赋值给局部变量
  • 最终方法(final),私有方法(private),构造方法都是由 invokespecial 指令来调用,属于静态绑定
  • 还有一个执行 invokespecial 的情况是通过 super 调用父类方法
  • 普通成员方法是由 invokevirtual 调用,属于动态绑定,即支持多态
  • 成员方法与静态方法调用的另一个区别是,执行方法前是否需要 对象引用
  • 比较有意思的是 d.test4(); 是通过 对象引用 调用一个静态方法
    • 可以看到在调用 invokestatic 之前执行了 pop 指令,把 对象引用 从操作数栈弹掉了

参考书籍《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明

这里仅列举以下五条指令用于方法调用:

  • invokevirtual:用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这是 Java 语言中最常见的方法分派方式
  • invokeinterface:用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用
  • invokespecial:用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法
  • invokestatic:用于调用类静态方法(static 方法)
  • invokedynamic:用于在运行时动态解析出调用点限定符所引用的方法。并执行该方法

前面四条调用指令的分派逻辑都固化在 Java 虚拟机内部,用户无法改变。

invokedynamic 指令的分派逻辑是由用户所设定的引导方法决定的。

方法调用指令与数据类型无关。

而方法返回指令是根据返回值的类型区分的

  • 包括 ireturn(当返回值是 booleanbytecharshortint 类型时使用)、lreturnfreturndreturnareturn
  • 另外还有一条 return 指令供声明为 void 的方法、实例初始化方法、类和接口的类初始化方法使用。

14.10.多态的原理


14.10.1.运行代码


import java.io.IOException;

/**
 * 演示多态原理,注意加上下面的 JVM 参数,禁用指针压缩
 * -XX:-UseCompressedOops -XX:-UseCompressedClassPointers
 */
public class Demo3_10 {
    public static void test(Animal animal) {
        animal.eat();
        System.out.println(animal.toString());
    }

    public static void main(String[] args) throws IOException {
        test(new Cat());
        test(new Dog());
        System.in.read();
    }
}

abstract class Animal {
    public abstract void eat();

    @Override
    public String toString() {
        return "我是" + this.getClass().getSimpleName();
    }
}

class Dog extends Animal {
    @Override
    public void eat() {
        System.out.println("啃骨头");
    }
}

class Cat extends Animal {
    @Override
    public void eat() {
        System.out.println("吃鱼");
    }
}

上述代码块中使用了 System.in.read() 方法,旨在此处暂停程序运行,此时可以运行 jps 获取进程 id

在这里插入图片描述


14.10.2.运行 HSDB 工具


进入 JDK 安装目录(可以用 java -verbose 输出信息,最后一行即是 jdk 安装位置),执行下面的命令

java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB

进入图形界面 attach 进程 id

在这里插入图片描述


关于 HSDB 连不上进程的解决办法

若 idea 的控制台上报如下的错误,可以根据控制台的输出信息来找到解决办法

C:\Program Files\Java\jdk1.8.0_144>java -cp .\lib\sa-jdi.jar sun.jvm.hotspot.HSDB
Exception in thread "main" java.lang.UnsatisfiedLinkError: Can't load library: C:\Program Files\Java\jre1.8.0_144\bin\sawindbg.dll
        at java.lang.ClassLoader.loadLibrary(Unknown Source)
        at java.lang.Runtime.load0(Unknown Source)
        at java.lang.System.load(Unknown Source)
        at sun.jvm.hotspot.debugger.windbg.WindbgDebuggerLocal.<clinit>(WindbgDebuggerLocal.java:661)
        at sun.jvm.hotspot.HotSpotAgent.setupDebuggerWin32(HotSpotAgent.java:567)
        at sun.jvm.hotspot.HotSpotAgent.setupDebugger(HotSpotAgent.java:335)
        at sun.jvm.hotspot.HotSpotAgent.go(HotSpotAgent.java:304)
        at sun.jvm.hotspot.HotSpotAgent.attach(HotSpotAgent.java:156)
        at sun.jvm.hotspot.HSDB.attach(HSDB.java:1236)
        at sun.jvm.hotspot.HSDB.run(HSDB.java:432)
        at sun.jvm.hotspot.HSDB.main(HSDB.java:55)

根据控制台的输出信息,提示:Can't load library: C:\Program Files\Java\jre1.8.0_144\bin\sawindbg.dll

可以去这个目录看看情况。发现该目录下并没有 sawindbg.dll 文件。

我自己是在 C:\Program Files\Java\jdk1.8.0_144\jre\bin 的目录下发现的 sawindbg.dll 文件。

复制该文件到之前无法加载的 C:\Program Files\Java\jre1.8.0_144\bin\ 的目录下就行了。

之后重新输入:java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB 这个命令就行了。

在这里插入图片描述


14.10.3.查找某个对象


打开 Tools -> Find Object By Query

输入 select d from cn.itcast.jvm.t3.bytecode.Dog d 点击 Execute 执行,查出对象的内存地址。

上面的 select 语句根据自己的目录(Path From Source Root)就行,我的语句就是:select d from bytecode.Dog d


14.10.4.查看对象内存结构


点击超链接可以看到对象的内存结构

此对象没有任何属性,因此只有对象头的 16 字节

前 8 字节是 MarkWord,后 8 字节就是对象的 Class 指针

但目前看不到它的实际地址

在这里插入图片描述


补充介绍

对象在堆内存中的存储布局可以划分为三个部分:对象头(Header实例数据(Instance Data)对齐填充(Padding

HotSpot 虚拟机对象的对象头部分包括两类信息。

  • 第一类是用于存储对象自身的运行时数据
    • 如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等
    • 这部分数据的长度在 32 位和 64 位的虚拟机(未开启压缩指针)中分别为 32 个比特和 64 个比特,官方称它为 “Mark Word”。
  • 对象头的另外一部分是类型指针
    • 即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。

实例数据部分是对象真正存储的有效信息

  • 即我们在程序代码里面所定义的各种类型的字段内容
  • 无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。

对象的第三部分是对齐填充

  • 这并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。
  • HotSpot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,即任何对象的大小都必须是 8 字节的整数倍。
  • 对象头部分已经被精心设计成正好是 8 字节的倍数(1 倍或者 2 倍)
  • 因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。

14.10.5.查看对象 Class 的内存地址


可以通过 Windows -> Console 进入命令行模式,执行下面的命令

mem 0x00000001d38d9500 2

mem 有两个参数,参数 1 是对象地址,参数 2 是查看 2 行(即 16 字节)

输出的结果中,第一行是 mark word,中第二行 0x0000000025734028 即为 Class 的内存地址

在这里插入图片描述


14.10.6.查看类的 vtable


方法1:Alt + R 进入 Inspector 工具,输入刚才的 Class 内存地址(0x0000000025734028

下面的界面是对象结构的完整表示,处于方法区

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

在这里插入图片描述


方法2:或者 Tools -> Class Browser 输入 Dog 查找,可以得到相同的结果

在这里插入图片描述


无论通过哪种方法,都可以找到 Dog Class 的 vtable 长度为 6,意思就是 Dog 类有 6 个虚方法(多态相关的,final,static 不会列入)

那么这 6 个方法都是谁呢?从 Class 的起始地址开始算,偏移 0x1b8 就是 vtable 的起始地址,进行计算得到:0x00000000257341e0

0x0000000025734028
			   1b8 +
---------------------
0x00000000257341e0

通过 Windows -> Console 进入命令行模式,执行

在这里插入图片描述

mem 0x00000000257341e0 6

输出结构即为 Dog 类所有的支持重写方法的入口地址

0x00000000257341e0: 0x0000000025331b10 
0x00000000257341e8: 0x00000000253315e8 
0x00000000257341f0: 0x00000000257335e8 
0x00000000257341f8: 0x0000000025331540 
0x0000000025734200: 0x0000000025331678 
0x0000000025734208: 0x0000000025733fa8 

14.10.7.验证方法地址


通过 Tools -> Class Browser 查看每个类的方法定义


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


比较各个类的方法定义可知

public void eat() @0x0000000025733fa8;

public java.lang.String toString() @0x00000000257335e8;

protected void finalize() @0x0000000025331b10;
public boolean equals(java.lang.Object) @0x00000000253315e8;
public native int hashCode() @0x0000000025331540;
protected native java.lang.Object clone() @0x0000000025331678;

对号入座,发现如下情况

  • eat() 方法是 Dog 类自己的
  • toString() 方法是继承 String 类的
  • finalize()equals()hashCode()clone() 都是继承 Object 类的

14.1.0.8.小结


因为普通成员方法需要在运行时才能确定具体的内容,所以虚拟机需要调用 invokevirtual 指令

在执行 invokevirtual 指令时,经历了以下几个步骤

  1. 先通过栈帧中对象的引用找到对象
  2. 分析对象头,找到对象实际的 Class
  3. Class 结构中有 vtable(虚方法表),它在类加载的链接阶段就已经根据方法的重写规则生成好了
  4. 查询 vtable 找到方法的具体地址
  5. 执行方法的字节码

14.11.异常处理


14.11.1.try-catch


public class Demo3_11_1 {
    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        } catch (Exception e) {
            i = 20;
        }
    }
}

public static void main(java.lang.String[]);
  descriptor: ([Ljava/lang/String;)V
  flags: ACC_PUBLIC, ACC_STATIC
  Code:
    stack=1, locals=3, args_size=1
       0: iconst_0
       1: istore_1
       2: bipush        10
       4: istore_1
       5: goto          12
       8: astore_2											# 把异常对象的引用地址存储到局部变量表中的 2 号槽位上(即 e)
       9: bipush        20
      11: istore_1
      12: return
    Exception table:										# 异常表
       from    to  target type								# 此处的 target 和上面的行号对应
           2     5     8   Class java/lang/Exception		# 异常检测范围:[from, to)
    LineNumberTable:										# 用于描述 Java 源码行号与字节码行号(字节码偏移量)之间的对应关系
    	... ...						
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          9       3     2     e   Ljava/lang/Exception;
          0      13     0  args   [Ljava/lang/String;
          2      11     1     i   I
    StackMapTable: ... ...
    	... ...

可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开的检测范围

一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型。

如果 type 匹配异常类型一致,进入 target 所指示行号 8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置


14.11.2.多个 single-catch 块


public class Demo3_11_2 {
    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        } catch (ArithmeticException e) {
            i = 30;
        } catch (NullPointerException e) {
            i = 40;
        } catch (Exception e) {
            i = 50;
        }
    }
}

public static void main(java.lang.String[]);
  descriptor: ([Ljava/lang/String;)V
  flags: ACC_PUBLIC, ACC_STATIC
  Code:
    stack=1, locals=3, args_size=1
       0: iconst_0
       1: istore_1
       2: bipush        10
       4: istore_1
       5: goto          26
       8: astore_2
       9: bipush        30
      11: istore_1
      12: goto          26
      15: astore_2
      16: bipush        40
      18: istore_1
      19: goto          26
      22: astore_2
      23: bipush        50
      25: istore_1
      26: return
    Exception table:
       from    to  target type
           2     5     8   Class java/lang/ArithmeticException
           2     5    15   Class java/lang/NullPointerException
           2     5    22   Class java/lang/Exception
    LineNumberTable:
      line 5: 0
      line 7: 2
      line 14: 5
      line 8: 8
      line 9: 9
      line 14: 12
      line 10: 15
      line 11: 16
      line 14: 19
      line 12: 22
      line 13: 23
      line 15: 26
    LocalVariableTable:
      Start  Length  Slot  Name   Signature								# 几个 Exception 都放到了 2 号槽位里(复用)
      																	# 这是因为这些 Exception 同一时刻只能发生一种错误 
      																	# 所以完全没有必要创建多个槽位来存储异常对象
          9       3     2     e   Ljava/lang/ArithmeticException;
         16       3     2     e   Ljava/lang/NullPointerException;
         23       3     2     e   Ljava/lang/Exception;
          0      27     0  args   [Ljava/lang/String;
          2      25     1     i   I
    StackMapTable: number_of_entries = 4
      frame_type = 255 /* full_frame */
        offset_delta = 8
        locals = [ class "[Ljava/lang/String;", int ]
        stack = [ class java/lang/ArithmeticException ]
      frame_type = 70 /* same_locals_1_stack_item */
        stack = [ class java/lang/NullPointerException ]
      frame_type = 70 /* same_locals_1_stack_item */
        stack = [ class java/lang/Exception ]
      frame_type = 3 /* same */

因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用

即这些异常同一时刻只能发生一种,所以没必要创建多个槽位来存储异常对象


14.11.3.multi-catch 的情况


import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Demo3_11_3 {

    public static void main(String[] args) {
        try {
            Method test = Demo3_11_3.class.getMethod("test");
            test.invoke(null);
        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    public static void test() {
        System.out.println("ok");
    }
}

public static void main(java.lang.String[]);
  descriptor: ([Ljava/lang/String;)V
  flags: ACC_PUBLIC, ACC_STATIC
  Code:
    stack=3, locals=2, args_size=1
       0: ldc           #2                  // class bytecode/Demo3_11_3
       2: ldc           #3                  // String test
       4: iconst_0
       5: anewarray     #4                  // class java/lang/Class
       8: invokevirtual #5                  // Method java/lang/Class.getMethod:
       										// 	(Ljava/lang/String[Ljava/lang/Class;)Ljava/lang/reflect/Method;
      11: astore_1
      12: aload_1
      13: aconst_null
      14: iconst_0
      15: anewarray     #6                  // class java/lang/Object
      18: invokevirtual #7                  // Method java/lang/reflect/Method.invoke:
      										// 	(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
      21: pop
      22: goto          30
      25: astore_1
      26: aload_1
      27: invokevirtual #11                 // Method java/lang/ReflectiveOperationException.printStackTrace:()V
      30: return
    Exception table:
       from    to  target type
           0    22    25   Class java/lang/NoSuchMethodException
           0    22    25   Class java/lang/IllegalAccessException
           0    22    25   Class java/lang/reflect/InvocationTargetException
    LineNumberTable:
      line 10: 0
      line 11: 12
      line 14: 22
      line 12: 25
      line 13: 26
      line 15: 30
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
         12      10     1  test   Ljava/lang/reflect/Method;
         26       4     1     e   Ljava/lang/ReflectiveOperationException;
          0      31     0  args   [Ljava/lang/String;
    StackMapTable: number_of_entries = 2
      frame_type = 89 /* same_locals_1_stack_item */
        stack = [ class java/lang/ReflectiveOperationException ]
      frame_type = 4 /* same */

相当于多个 single-catch 的优化,把平级的异常都写在一起,让这些异常的入口是一样的

Exception table 中的 target 属性的值都一样


14.11.4.finallly


public class Demo3_11_4 {
    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        } catch (Exception e) {
            i = 20;
        } finally {
            i = 30;
        }
    }
}

public static void main(java.lang.String[]);
	descriptor: ([Ljava/lang/String;)V
	flags: ACC_PUBLIC, ACC_STATIC
	Code:
		stack=1, locals=4, args_size=1						# 这里之所以有 4 个槽位而不是 3 个槽位
															# 因为存在 catch 不到 Exception 的情况
															# 多加一个槽位是字节码指令的一个保障(在异常表中多捕获一个异常)
			0: iconst_0
			1: istore_1 									// 0 -> i
			2: bipush 10 									// try --------------------------------------
			4: istore_1 									// 10 -> i 									|
			5: bipush 30 									// finally 									|
			7: istore_1 									// 30 -> i									|
			8: goto 27 										// return -----------------------------------
			
			11: astore_2 									// catch Exceptin -> e ----------------------
			12: bipush 20 									// 											|
			14: istore_1 									// 20 -> i 									|
			15: bipush 30 									// finally 									|
			17: istore_1			 						// 30 -> i 									|
			18: goto 27 									// return -----------------------------------
			
															# 其实 Exception 并不能捕获所有的异常
															# 或许会抛出 Exception 的父类型或者平级类型
															# 即使 Excpetion 捕获不到异常
															# finally 部分的代码仍要被执行
															
			21: astore_3 									// catch any -> slot 3 ----------------------
			22: bipush 30 									// finally 									|
			24: istore_1 									// 30 -> i 									|
			25: aload_3 									// <- slot 3 								|
			26: athrow 										// throw ------------------------------------
			
			27: return
			
		Exception table:
			from to target type
			2 	 5  11     Class java/lang/Exception
			2 	 5  21 	   any 								// 剩余的异常类型,比如 Error
			11 	 15  21    any 								// 剩余的异常类型,比如 Error
		LineNumberTable: ...
		LocalVariableTable:
			Start Length Slot Name Signature
				12 3 2 e Ljava/lang/Exception;
				0 28 0 args [Ljava/lang/String;
				2 26 1 i I
		StackMapTable: ...
	MethodParameters: ...

可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程

注意:虽然从字节码指令看来,每个块中都有 finally 块,但是 finally 块中的代码只会被执行一次


14.11.5.finally 面试题(return)


public class Demo3_12_1 {
    public static void main(String[] args) {
        int result = test();
        System.out.println(result);
    }

    public static int test() {
        try {
            return 10;
        } finally {
            return 20;
        }
    }
}

程序运行后,在控制台上输出的信息是 20

public static int test();
  descriptor: ()I
  flags: ACC_PUBLIC, ACC_STATIC
  Code:
    stack=1, locals=2, args_size=0
       0: bipush        10				#  <- 10 放入栈顶(压入操作数栈中)
       2: istore_0						# 10 -> slot 0 (从栈顶移除了)
       
       3: bipush        20				#  <- 20 放入栈顶(fianlly)
       5: ireturn						# 返回栈顶 int(20)
       
       6: astore_1						# catch any -> slot 1(finally)
       7: bipush        20				#  <- 20 放入栈顶
       9: ireturn						# 返回栈顶 int(20)
    Exception table:
       from    to  target type
           0     3     6   any
  • 由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯定以 finally 的为准
  • 至于字节码中第 2 行,似乎没啥用,且留个伏笔,看下个例子
  • 跟上例中的 finally 相比,发现没有 athrow 了,这告诉我们:如果在 finally 中出现了 return,会吞掉异常

下方的代码块在执行时,就不会出现任何异常(因为异常已经被吞掉了)

public class Demo3_12_1S {
    public static void main(String[] args) {
        int result = test();
        System.out.println(result); // 20
    }

    public static int test() {
        try {
            int i = 1 / 0;
            return 10;
        } finally {
            return 20;
        }
    }
}

14.11.6.finally 面试题(对返回值的影响)


public class Demo3_12_2 {
    public static void main(String[] args) {
        int result = test();
        System.out.println(result); // 10
    }

    public static int test() {
        int i = 10;
        try {
            return i;
        } finally {
            i = 20;
        }
    }
}

public static int test();
  descriptor: ()I
  flags: ACC_PUBLIC, ACC_STATIC
  Code:
    stack=1, locals=3, args_size=0
      # try 模块
	  0: bipush 10 					# <- 10 放入栈顶
	  2: istore_0 					# 10 -> i
	  3: iload_0 					# <- i(10)
	  
	  4: istore_1 					# 10 -> slot 1,暂存至 slot 1,目的是为了 [固定] 返回值
	  
	  # finally 模块
	  5: bipush 20 					# <- 20 放入栈顶
	  7: istore_0 					# 20 -> i

	  8: iload_1 					# <- slot 1(10) 载入 slot 1 暂存的值
	  9: ireturn 					# 返回栈顶的 int(10)
	  
      10: astore_2					# try 模块代码中间如果出现了异常会跳到这一步
      11: bipush        20
      13: istore_0
      14: aload_2
      15: athrow					# 存在 athrow,如果有异常,会报错
    Exception table:
       from    to  target type
           3     5    10   any

我们发现如果在 try 模块中存在 return 变量,那么即使 finally 模块中的变量发生了变化,返回的依旧是 try 模块中的变量值

这是因为我们可以从字节码指令看到 try 中的变量会先被备份一次用来返回

但是如果 fianlly 模块中也存在 return 的话,那就优先执行 finally 模块中的 return

而且 finally 模块中存在 return 的时候会吞掉异常


14.12.synchronized


回顾知识

  • JAVA 以提供 synchronized 关键字的形式,为防止资源冲突提供了内置支持
  • 当任务要执行被 synchronized 关键字保护的代码片段的时候它将检查锁是否可用然后获取锁执行代码释放锁

同步代码块解决数据安全问题

  • 为什么出现问题?(这也是我们判断多线程程序是否会有数据安全问题的标准
    • 是否是多线程环境
    • 是否有共享数据
    • 是否有多条语句操作共享语句
  • 如何解决多线程安全问题
    • 基本思想让程序没有安全问题的环境
  • 怎么实现
    • 把多条语句操作的代码给锁起来让任意时刻只能有一个线程执行即可
  • 同步代码块
    • 锁多条语句操作共享数据可以使用同步代码块实现
  • 格式synchronized(任意对象){ 多条语句操作共享数据的代码 }
    • synchronized(任意对象) 就相当于给代码加锁了任意对象都可以看成是一把锁
  • 同步的好处解决了多线程的数据安全问题
  • 同步的弊端
    • 当线程很多时因为每个线程都会上去判断同步上的锁
    • 这是很耗费资源的无形中会降低程序的运行效率

public class Demo3_13 {
    public static void main(String[] args) {
        Object lock = new Object();
        synchronized (lock) {
            System.out.println("ok");
        }
    }
}

public static void main(java.lang.String[]);
  descriptor: ([Ljava/lang/String;)V
  flags: ACC_PUBLIC, ACC_STATIC
  Code:
    stack=2, locals=4, args_size=1
       # #####################################################################################################
       # 第一行代码:Object lock = new Object();
       0: new           #2                  // class java/lang/Object 
       										# 即 new 了一个对象
       										
       3: dup								# 这里是复制对象的引用,因为栈顶会消耗一份该对象的引用
       
       4: invokespecial #1                  // Method java/lang/Object."<init>":()V
       										# 此处调用了构造方法
       										
       7: astore_1							# [lock 对象]的引用 —> lock
       										# 将第二份 对象的引用 赋值 给局部变量表中的 lock
       # 至此,第一行代码执行完毕
       
       # #####################################################################################################
       8: aload_1							# <- [lock 对象]的引用(synchronized 开始)
       										# 此处进入了 synchronized 代码块,这里需要把对象的引用加载到操作数栈
       										
       9: dup								# 复制 lock 对象的引用,现在我们就有 lock 对象的两个引用了
       										# 分别对应着 monitorenter 和 monitorexit 两个指令使用的阶段
       
      10: astore_2							# 将刚刚新创建出来的对象引用存储到局部变量表的 2 号槽位上
      
      11: monitorenter						# monitorenter(lock 引用)
      										# 该指令会消耗掉栈顶元素(lock 对象的引用)
      										# 并对 lock 对象加锁
      
      # #####################################################################################################
      # 现在可以开始安全的执行 synchronized 代码块中的语句了(System.out.println("ok");)						
      12: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
      15: ldc           #4                  // String ok
      17: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      # 打印结束
      
      # #####################################################################################################
	  # 怎么确保锁一定解开呢?这里利用了异常处理的操作
	  # #####################################################################################################
	  # 1.没有出现异常的情况
	  # #####################################################################################################
      20: aload_2							# <- slot 2(lock 引用)
      										# 这里会把之前暂存于 slot 2 的 lock 对象的引用再次加载到栈顶
      										# 即将局部变量表中的 2 号槽位的数据 加载至 操作数栈中
      										
      21: monitorexit						# monitorexit(lock 引用)
      										# 给 lock 对象解锁
      # #####################################################################################################
      # 2.如果期间出现了异常的情况,则会利用到异常表
      # #####################################################################################################										
      22: goto          30
      
      25: astore_3							# any -> slot 3
      										# 将异常对象的引用抛到局部变量表中的 3 号槽位上
      										
      26: aload_2							# <- slot 2(lock 引用)
											# 加载之前暂存在局部变量表上的 2 号槽位的对象的引用
											
      27: monitorexit						# monitorexit(lock 引用)
      										# 给 lock 对象解锁
      										
      28: aload_3							# 加载之前在 3 号槽位上的异常对象

      29: athrow							# 抛出异常对象
      # #####################################################################################################
      30: return
    Exception table:
       from    to  target type
          12    22    25   any
          25    28    25   any
    LineNumberTable:
    	... ...
    LocalVariableTable:
		... ...
    StackMapTable: ... ...

注意:方法级别的 synchronized 不会在字节码指令中有所体现


15.编译期处理


语法糖

  • 其实就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成和转换的一些代码
  • 主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利

注意

  • 以下代码的分析,借助了 javap 工具,idea 的反编译功能,idea 插件 jclasslib 等工具。
  • 另外,编译器转换的结果直接就是 class 字节码
  • 只是为了便于阅读,给出了几乎等价 的 java 源码方式(实际上还是伪代码),并不是编译器还会转换出中间的 java 源码,切记。

15.1.默认构造器


public class Candy1 { }

编译成 class 后的代码(前提是在该类中没有额外实现任何构造器的情况下)

public class Candy1 {
   //这个无参构造器是 java 编译器帮我们加上的
   public Candy1() {
      //即调用父类 Object 的无参构造方法,即调用 java/lang/Object." <init>":()V
      super();
   }
}

15.2.自动拆装箱


拆装箱:基本类型和包装类型之间来回转换


这个特性是 JDK 5 开始加入的,下面为代码块 1

public class Candy2 {
   public static void main(String[] args) {
      Integer x = 1;
      int y = x;
   }
}

上面的代码块 1 在 JDK 5 之前的版本是无法编译通过的,必须改写为如下所示的代码块 2

public class Candy2 {
   public static void main(String[] args) {
      //装箱:基本类型转换为给包装类型
      Integer x = Integer.valueOf(1);  

      //拆箱:包装类型转换为基本类型
      int y = x.intValue();
   }
}

显然之前版本的代码太麻烦了,需要在基本类型和包装类型之间来回转换(尤其是集合类中操作的都是包装类型)

因此这些转换的事情在 JDK 5 以后都由编译器在编译阶段完成。

即 代码块 1 都会在编译阶段被转换为 代码块 2


参考博客《Integer 和 int 的区别》

  1. Integerint 的包装类,int 则是 java 的一种基本数据类型
  2. Integer 变量必须实例化才能使用,int 变量不需要实例化
  3. Integer 的默认值是 null,而 int 的默认值是 0
  4. Integer 实际是一个对象的引用
    • new 一个 Integer 对象时,实际是生成一个指针指向该对象;
    • int 是基本数据类型,直接存储数值
  5. 在使用 Integer 时,对于 -128127 之间的数,会进行缓存。
    • 例如:Integer i1 = 127 时,会将 127 进行缓存
    • 下次再写 Integer i2 = 127 时,就会直接从缓存中取,不会新 new 一个 Integer
    • 所以 i1i2== 进行比较时,会为 true
  6. 自动拆箱和装箱(java 提供了自动拆装箱)
    • 装箱:把基本数据类型装换为对应的包装类类型
    • 拆箱:把包装类类型装换为对应的基本数据类型
    • 注意:在使用包装类数据时,如果做操作,最好先判断是否为 null
    • 推荐:只要是对象,在使用前就必须进行不为 null 的判断

15.3.泛型集合取值


泛型也是在 JDK 5 开始加入的特性

但 java 在编译泛型代码后会执行 泛型擦除 的动作

即泛型信息在编译为字节码之后就丢失了实际的类型都当做了 Object 类型来处理


public class Candy3 {
   public static void main(String[] args) {
      List<Integer> list = new ArrayList<>();
      list.add(10);  // 实际上是调用 List.add(Object o)
      Integer x = list.get(0); // 实际上是调用 Object bbj = List.get(int index);
   }
}

所以在取值时,编译器真正生成的字节码中,还要额外做一个类型转换的操作

// 需要将 Object 转为 Integer
Integer x = (Integer)list.get(0);

如果前面的 x 变量类型修改为 int 基本类型那么最终生成的字节码是

// 需要将 Object 转为 Integer, 并执行拆箱操作
int x = ((Integer)list.get(0)).intValue();

还好这些麻烦事都不用自己做。


[擦除] 的是 [字节码] 上的 [泛型] 信息,可以看到 [LocalVariableTypeTable] 仍然保留了方法参数泛型的信息

Code:
    stack=2, locals=3, args_size=1
       0: new           #2                  			// class java/util/ArrayList
       3: dup
       4: invokespecial #3                  			// Method java/util/ArrayList."<init>":()V
       7: astore_1
       8: aload_1
       9: bipush        10
      11: invokestatic  #4                  			// Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      													# 装箱
      
      //这里进行了 [泛型擦除],实际调用的是 add(Objcet o)
      14: invokeinterface #5,  2            			// InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z

      19: pop											# 将操作数栈的栈顶元素出栈
      
      20: aload_1
      
      21: iconst_0										# iconst 是一个入栈指令
      													# 其作用是用来将 int 类型的数字、取值在 -1 到 5 之间的整数压入栈中
      													# 此处是设置下标(方便 List.get(int index); 获取数据)
      											
      # 这里也进行了 [泛型擦除],实际调用的是 get(Object o)   
      22: invokeinterface #6,  2            			// InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
      
	  # 这里进行了 [类型转换],将 Object 转换成了 Integer
      27: checkcast     #7                  			// class java/lang/Integer

      30: astore_2
      
      31: returnCopy
	LineNumberTable:									# Java 源码的行号与字节码指令的对应关系
		line 8: 0
		line 9: 8
		line 10: 20
		line 11: 31
	LocalVariableTable:									# 局部变量表(方法的局部变量的描述)
		Start  Length  Slot  Name  Signatur
			0      32     0  args  [Ljava/lang/String;
			8      24     1  list  Ljava/util/List;
	LocalVariableTypeTable: 							# 局部变量类型表
		Start  Length  Slot  Name  Signature
			8 	   24     1  list  Ljava/util/List<Ljava/lang/Integer;>;

使用反射,仍然能够获得这些信息

缺陷是只能得到方法参数上泛型信息,和返回值上的泛型信息

public Set<Integer> test(List<String> list, Map<Integer, Object> map) { }
Method test = Candy3.class.getMethod("test", List.class, Map.class);

Type[] types = test.getGenericParameterTypes();

for (Type type : types) {
    if (type instanceof ParameterizedType) {
        ParameterizedType parameterizedType = (ParameterizedType) type;
        System.out.println("原始类型 - " + parameterizedType.getRawType());
        Type[] arguments = parameterizedType.getActualTypeArguments();
        for (int i = 0; i < arguments.length; i++) {
            System.out.printf("泛型参数[%d] - %s\n", i, arguments[i]);
        }
    }
}

控制台输出结果

原始类型 - interface java.util.List
泛型参数[0] - class java.lang.String
原始类型 - interface java.util.Map
泛型参数[0] - class java.lang.Integer
泛型参数[1] - class java.lang.Object

15.4.可变参数


可变参数也是 JDK 5 开始加入的新特性

public class Candy4 {
   public static void foo(String... args) {

      String[] arr = args; // 直接赋值
      System.out.println(arr.length);
   }
 
   public static void main(String[] args) {
      foo("hello", "world");
   }
}

可变参数 String… args 其实是一个 String[] args ,从代码中的赋值语句中就可以看出来。

同样 java 编译器会在编译期间将上述代码变换为

public class Candy4 {
   public Demo4 {}
 
    
   public static void foo(String[] args) {
      String[] arr = args;
      System.out.println(arr.length);
   }
 
   public static void main(String[] args) {
      foo(new String[]{"hello", "world"});
   }
}

注意:如果调用了 foo() 则等价代码为 foo(new String[]{}) ,创建了一个长度为 0 的空的数组,而不会传递 null 进去


15.5.foreach 循环


仍是 JDK 5 开始引入的语法糖,数组的循环

public class Candy5_1 {
	public static void main(String[] args) {
        //数组赋初值的简化写法也是法糖
		int[] arr = {1, 2, 3, 4, 5};
		for(int x : arr) {  //编译之后就是 for 循环
			System.out.println(x);
		}
	}
}

会被编译器转换为

public class Candy5_1 {
    public Candy5_1 {}
 
	public static void main(String[] args) {
		int[] arr = new int[]{1, 2, 3, 4, 5};
		for(int i=0; i<arr.length; ++i) {
			int x = arr[i];
			System.out.println(x);
		}
	}
}

而集合的循环

public class Candy5_2 {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        for (Integer i : list) {
            System.out.println(i);
        }
    }
}

实际被编译器转换为对迭代器的调用

public class Candy5_2 {
    public Candy5_2 {}
    
   public static void main(String[] args) {
      List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
      
      //获得该集合的迭代器
      Iterator<Integer> iterator = list.iterator();
      
      while(iterator.hasNext()) {
         Integer x = iterator.next();
         System.out.println(x);
      }
   }
}

注意

  • foreach 循环写法,能够配合数组,以及所有实现了 Iterable 接口的集合类一起使用
  • 其中 Iterable 用来获取集合的迭代器Iterator

15.6.switch 字符串


JDK 7 开始,switch 可以作用于字符串和枚举类,这个功能也是语法糖

public class Canydy6_1 {
   public static void main(String[] args) {
      String str = "hello";
      switch (str) {
         case "hello" :
            System.out.println("h");
            break;
         case "world" :
            System.out.println("w");
            break;
         default:
            break;
      }
   }
}

注意switch 配合 String 和枚举使用时,变量不能为 null,原因分析完语法糖转换后的代码应当自然清楚


编译期会将上述代码转换为下面的代码块中的情况

public class Candy6_1 {
    public Candy6_1() { }

    public static void choose(String str) {
        byte x = -1;
        
        switch (str.hashCode()) {
            case 99162322: // hello 的 hashCode
                if (str.equals("hello")) {
                    x = 0;
                }
                break;
            case 113318802: // world 的 hashCode
                if (str.equals("world")) {
                    x = 1;
                }
        }
        
        switch (x) {
            case 0:
                System.out.println("h");
                break;
            case 1:
                System.out.println("w");
        }
        
    }
}

可以看到,执行了两遍 switch

  • 第一遍是根据字符串的 hashCodeequals 将字符串的转换为相应 byte 类型
  • 第二遍才是利用 byte 执行进行比较。

为什么第一遍时必须既比较 hashCode,又利用 equals 比较呢?

  • hashCode 是为了提高效率,尽可能的减少比较次数
  • equals 是为了防止 hashCode 冲突

例如 BMC. 这两个字符串的 hashCode 值都是 2123

如果有如下代码

public class Candy6_2 {
    public static void choose(String str) {
        switch (str) {
            case "BM": {
                System.out.println("h");
                break;
            }
            case "C.": {
                System.out.println("w");
                break;
            }
        }
    }
}

上面的代码会被编译期转换为下面的情况(伪代码)

public class Candy6_2 {
    public Candy6_2() {
    }

    public static void choose(String str) {
        byte x = -1;
        switch (str.hashCode()) {
            case 2123: // hashCode 值可能相同,需要进一步用 equals 比较
                if (str.equals("C.")) {
                    x = 1;
                } else if (str.equals("BM")) {
                    x = 0;
                }
            default:
                switch (x) {
                    case 0:
                        System.out.println("h");
                        break;
                    case 1:
                        System.out.println("w");
                }
        }
    }
}

15.7.switch 枚举


switch 枚举的例子

enum Sex {
    MALE, FEMALE;
}
public class Candy7 {
    public static void foo(Sex sex) {
        switch (sex) {
            case MALE:
                System.out.println("男");
                break;
            case FEMALE:
                System.out.println("女");
                break;
        }
    }
}

编译转换后的伪代码

public class Candy7 {
    /**
     * 定义一个 [合成类](仅 jvm 使用,对我们不可见)
     * 用来映射枚举的 ordinal 与数组元素的关系
     * 枚举的 ordinal 表示枚举对象的序号,从 0 开始
     * 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1
     */
    static class $MAP {
        // 数组大小即为枚举元素个数,里面存储 case 用来对比的数字
        static int[] map = new int[2];

        static {
            map[Sex.MALE.ordinal()] = 1;
            map[Sex.FEMALE.ordinal()] = 2;
        }
    }

    public static void foo(Sex sex) {
        int x = $MAP.map[sex.ordinal()];
        switch (x) {
            case 1:
                System.out.println("男");
                break;
            case 2:
                System.out.println("女");
                break;
        }
    }
}

15.8.枚举类


JDK 7 新增了枚举类,以前面的性别枚举为例

enum Sex {
	/* 下面一行就是 class 的两个对象,它们和普通对象的区别是:
	 * * 枚举类的实例是有限的
	 * * 而不同类的实例对象是无限的(可以通过 new 关键字来不断创建)
	 */
    MALE, FEMALE; // 两个静态常量
}

编译转换后的伪代码

public final class Sex extends Enum<Sex> { // 此处被 final 修饰,表示枚举类不能被继承
    public static final Sex MALE;
    public static final Sex FEMALE;
    private static final Sex[] $VALUES;

    static {
    	// 调用构造函数,传入枚举元素的值及 ordinal
        MALE = new Sex("MALE", 0);
        FEMALE = new Sex("FEMALE", 1);
        $VALUES = new Sex[]{MALE, FEMALE};
    }

    /**
     * Sole constructor. Programmers cannot invoke this constructor.
     * It is for use by code emitted by the compiler in response to
     * enum type declarations.
     *
     * @param name    - The name of this enum constant, which is the identifier
     *                used to declare it.
     * @param ordinal - The ordinal of this enumeration constant (its position
     *                in the enum declaration, where the initial constant is
     *                assigned
     */
    private Sex(String name, int ordinal) {
        super(name, ordinal);
    }

    public static Sex[] values() {
        return $VALUES.clone();
    }

    public static Sex valueOf(String name) {
        return Enum.valueOf(Sex.class, name);
    }
}

15.9.try-with-resources


JDK 7 开始新增了对需要关闭的资源处理的特殊语法 try-with-resources

try(资源变量 = 创建资源对象){
	//......
} catch( ) {
	//......
}

其中资源对象需要实现 AutoCloseable 接口

  • 例如 InputStreamOutputStreamConnectionStatementResultSet 等接口都实现了 AutoCloseable
  • 它们都可以使用 try-with-resources,都可以不用写 finally 语句块,因为编译器会帮助生成关闭资源代码

例如:

public class Candy9 {
    public static void main(String[] args) {
        try (InputStream is = new FileInputStream("d:\\1.txt")) {
            System.out.println(is);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

会被转换为(伪代码)

public class Candy9 {
    public Candy9() {
    }

    public static void main(String[] args) {
        try {
            InputStream is = new FileInputStream("d:\\1.txt");
            Throwable t = null;
            try {
                System.out.println(is);
            } catch (Throwable e1) {
                // t 是我们代码出现的异常
                t = e1;
                throw e1;
            } finally {
                // 判断了资源不为空
                if (is != null) {
                    // 如果我们代码有异常
                    if (t != null) {
                        try {
                            is.close();
                        } catch (Throwable e2) {
                            // 如果 close 出现异常,作为被压制异常添加
                            t.addSuppressed(e2);
                        }
                    } else {
                        // 如果我们代码没有异常,close 出现的异常就是最后 catch 块中的 e
                        is.close();
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

为什么要设计一个 addSuppressed(Throwable e) (添加被压制异常)的方法呢?

是为了防止异常信息的丢失(想想 try-with-resources 生成的 fianlly 中,即关闭资源的时候,抛出了异常)

public class Test6 {
    public static void main(String[] args) {
        try (MyResource resource = new MyResource()) {
            int i = 1 / 0;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class MyResource implements AutoCloseable {
    public void close() throws Exception {
        throw new Exception("close 异常");
    }
}

输出

java.lang.ArithmeticException: / by zero
	at test.Test6.main(Test6.java:7)
	Suppressed: java.lang.Exception: close 异常
		at test.MyResource.close(Test6.java:18)
		at test.Test6.main(Test6.java:6)

如以上代码所示,两个异常信息都不会丢。


15.10.方法重写时的桥接方法


我们都知道,方法重写时对返回值分两种情况:

  • 父子类的返回值完全一致
  • 子类返回值可以是父类返回值的子类(比较绕口,见下面的例子)
class A {
    public Number m() {
        return 1;
    }
}

class B extends A {
    @Override
    // 子类 m 方法的返回值是 Integer 是父类 m 方法返回值 Number 的子类
    public Integer m() {
        return 2;
    }
}

对于子类,java 编译器会做如下处理(下面代码块其实是便于我们理解的伪代码)

class B extends A {

    public Integer m() {
        return 2;
    }
    
    // 此方法才是真正重写了父类 public Number m() 方法
    public synthetic bridge Number m() { 
    	// [synthetic bridge]:java 编译器生成的合成方法,在 JVM 内部使用。对我们是不可见的
        // 调用 public Integer m()
        return m();
    }
    
}

其中桥接方法比较特殊,仅对 java 虚拟机可见,并且与原来的 public Integer m() 没有命名冲突

可以用下面反射代码来验证:

for (Method m : B.class.getDeclaredMethods()) {
	System.out.println(m);
}

输出

public java.lang.Integer test.candy.B.m()
public java.lang.Number test.candy.B.m()

15.11.匿名内部类


源代码

public class Candy11 {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("ok");
            }
        };
    }
}

转换后的情况

// 额外生成的类
final class Candy11$1 implements Runnable {
    Candy11$1() { }

    public void run() {
        System.out.println("ok");
    }
}
public class Candy11 {
    public static void main(String[] args) {
    	//用额外创建的类来创建匿名内部类对象
        Runnable runnable = new Candy11$1();
    }
}

引用局部变量的匿名内部类,源代码

public class Candy11 {
    public static void test(final int x) {
    	/* 变量是引用类型的时候,final 修饰它
    	 * 指的是引用类型的地址值不能发生改变
    	 * 但地址里面的内容可以发生改变 
    	 */
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("ok:" + x);
            }
        };
    }
}

转换后

// 额外生成的类
final class Candy11$1 implements Runnable {
    int val$x;

    Candy11$1(int x) {
        this.val$x = x;
    }

    public void run() {
        System.out.println("ok:" + this.val$x);
    }
}
public class Candy11 {
    public static void test(final int x) {
        Runnable runnable = new Candy11$1(x);
    }
}
  • 注意:这同时解释了为什么匿名内部类引用局部变量时,局部变量必须是 final 的:
    • 因为在创建 Candy11$1 对象时,将 x 的值赋值给了 Candy11$1 对象的 val$x 属性,所以 x 不应该再发生变化了
    • 如果变化,那么 val$x 属性没有机会再跟着一起变化

16.类加载阶段


16.1.加载阶段


将类的字节码载入方法区中,内部采用 instanceKlassC++ 的一种数据结构)描述 java 类

  • 它的重要 field 有:
    • _java_mirror 即 java 的类镜像,起到了一个桥梁的作用(C++Java
      • 例:对 String 来说,就是 String.class,作用是把 klass 暴露给 Java 使用
    • _super 即父类
    • _fields 即成员变量
    • _methods 即方法
    • _constants 即常量池
    • _class_loader 即类加载器
    • _vtable 虚方法表
    • _itable 接口方法表
  • 如果这个类还有父类没有加载,先加载父类
  • 加载和链接可能是交替运行的

JDK 8 以后,方法区位于元空间中,而元空间又位于本地内存中

在这里插入图片描述
类对象和 instanceKlass 之间的关系

  • JDK 8 及以后的版本中,类的字节码加载到方法区,是会加载到元空间中的,构成了 instanceKlass 这样的数据结构
    • 可以使用 HSDB 工具来查看这些信息
  • 加载的同时,instanceKlass 又会在堆内存中生成类对象(如图中的 Person.class
  • 堆中的类对象(如图中的 Person.class)持有了 instanceKlass 的指针地址
  • 反过来,instanceKlass 中的属性 _java_mirror 也持有堆中的类对象(如图中的 Person.class) 的指针地址
    • 注:_java_mirror 就是 java 的类镜像

如果以后通过 new 创建了一系列的 实例对象,那么它们之间是如何联系的呢?

  • 每个实例对象都有自己的对象头(16 个字节),其中的 8 个字节是对应着对象的 class 地址
  • 如果想通过实例对象来获取 class 信息
    • 首先会访问实例对象(Person p 中的 p)的对象头来找到类对象(Person.class
    • 再通过类对象(Person.class)来访问元空间中的对应的 instanceKlass
    • 之后的相关调用(比如 p.getMethods()p.getFields()),都是去元空间获取具体信息的

注意instanceKlass 这样的元数据是存储在方法区(1.8 后的元空间内),类对象是存储在堆中的


16.2.连接阶段


16.2.1.验证阶段


验证阶段验证类是否符合 JVM 规范,安全性检查

可以使用用 UE 等支持二进制的编辑器修改 HelloWorld.class 的魔数

修改完成后在控制台运行,发现报错

E:\git\jvm\out\production\jvm>java cn.itcast.jvm.t5.HelloWorld
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.ClassFormatError: Incompatible magic value 3405691578 in class file cn/itcast/jvm/t5/HelloWorld
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
	at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
	at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
	at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)

16.2.2.准备阶段


准备阶段static 变量分配空间,设置默认值

  • JDK 8 中,静态变量是存储在堆中的
  • static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾
  • static 变量分配空间和赋值是两个步骤,分配空间准备阶段完成,赋值初始化阶段完成
  • 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
  • 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成
// 演示 final 对静态变量的影响
public class Load8 {
    static int a;
    static int b = 10;
    static final int c = 20;
    static final String d = "hello";
    static final Object e = new Object();
}

使用 javac Load8.java 编译上述代码

之后再借助 javap 工具(javap -v -p Load8.class)查看字节码信息

static int a;
  descriptor: I
  flags: ACC_STATIC

static int b;
  descriptor: I
  flags: ACC_STATIC
#####################################################################################
static final int c;
  descriptor: I
  flags: ACC_STATIC, ACC_FINAL
  ConstantValue: int 20					
#####################################################################################
static final java.lang.String d;
  descriptor: Ljava/lang/String;
  flags: ACC_STATIC, ACC_FINAL
  ConstantValue: String hello
#####################################################################################			
static final java.lang.Object e;
  descriptor: Ljava/lang/Object;
  flags: ACC_STATIC, ACC_FINAL
public zOthers.Load8();
  descriptor: ()V
  flags: ACC_PUBLIC
  Code:
    stack=1, locals=1, args_size=1
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
    LineNumberTable:
      line 4: 0
static {};
  descriptor: ()V
  flags: ACC_STATIC
  Code:
    stack=2, locals=0, args_size=0
      #####################################################################################
       0: bipush        10
       2: putstatic     #2                  // Field b:I
      #####################################################################################
       5: new           #3                  // class java/lang/Object
       8: dup
       9: invokespecial #1                  // Method java/lang/Object."<init>":()V
      12: putstatic     #4                  // Field e:Ljava/lang/Object;
      #####################################################################################
      15: return
    LineNumberTable:
      line 6: 0
      line 9: 5

16.2.3.解析阶段


解析阶段将常量池中的符号引用解析为直接引用

参考博客《JVM 类加载机制 超详细学习笔记(三)》

  • 因为符号引用仅仅就是一个符号引用,JVM 不知道它的具体含义是什么
  • 但是经过实际解析后 JVM 就可以知道这个类,方法在内存中实实在在的位置了
  • 未解析时:常量池中的看到的对象仅是符号,未真正的存在于内存中
  • 解析以后:会将常量池中的符号引用解析为直接引用
package zOthers;

import java.io.IOException;

/**
 * 解析的含义
 */
public class Load2 {
    public static void main(String[] args) throws ClassNotFoundException, IOException {
        ClassLoader classloader = Load2.class.getClassLoader();
        
        // loadClass 方法不会导致类的解析和初始化
        Class<?> c = classloader.loadClass("zOthers.C");
        
        // new C();
        System.in.read();
    }
}

class C { D d = new D();}

class D { }

启动上述代码块的程序后,使用 HSDB 工具查看地址信息

(懒得贴图演示了,知道那么个意思就行)


16.3.初始化阶段


16.3.1.cinit 方法


初始化即调用 <cinit>()V ,虚拟机会保证这个类的『构造方法』的线程安全

  • clinit() 方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{} 块)中的语句合并产生的
  • 编译器收集的顺序是由代码语句在源文件中出现的顺序决定的
    • 静态语句块中只能访问到定义在静态语句块之前的变量
    • 定义在静态语句块之后的变量,可以在前面的静态语句块中赋值,但是不能访问

所以验证类是否被初始化,可以看该类的静态代码块是否被执行


16.3.2.发生的时机


概括得说,类初始化是懒惰的

  • main 方法所在的类,总会被首先初始化
  • 首次访问这个类的静态变量或静态方法时
  • 子类初始化,如果父类还没初始化,会引发
  • 子类访问父类的静态变量,只会触发父类的初始化
  • Class.forName
  • new 会导致初始化

不会导致类初始化的情况

  • 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
  • 类对象.class 不会触发初始化
  • 创建该类的数组不会触发初始化
  • 类加载器的 loadClass 方法
  • Class.forName 的参数 2 为 false 时

package load;

import java.io.IOException;

public class Load3 {
    static {
        System.out.println("main init");
    }

    public static void main(String[] args) throws ClassNotFoundException, IOException {
    
        /* 不会触发的情况 */
        // 1. 静态常量不会触发初始化
        //System.out.println(B.b);
        // 2. 类对象.class 不会触发初始化
        //System.out.println(B.class);
        // 3. 创建该类的数组不会触发初始化
        //System.out.println(new B[0]);
        // 4. 不会初始化类 B,但会加载 B、A
        //ClassLoader cl = Thread.currentThread().getContextClassLoader();
        //cl.loadClass("load.B");
        // 5. 不会初始化类 B,但会加载 B、A
        //ClassLoader c2 = Thread.currentThread().getContextClassLoader();
        //Class.forName("load.B", false, c2);
        //System.in.read();


        /* 会触发初始化的情况 */
        // 1. 首次访问这个类的静态变量或静态方法时
        //System.out.println(A.a);
        // 2. 子类初始化,如果父类还没初始化,会引发
        //System.out.println(B.c);
        // 3. 子类访问父类静态变量,只触发父类初始化
        //System.out.println(B.a);
        // 4. 会初始化类 B,并先初始化类 A
        //Class.forName("load.B");

    }
}

class A {
    static int a = 0;

    static {
        System.out.println("a init");
    }
}

class B extends A {
    final static double b = 5.0;
    static boolean c = false;

    static {
        System.out.println("b init");
    }
}

具体演示还是看视频吧:https://www.bilibili.com/video/BV1yE411Z7AP?p=147


16.3.3.补充内容


参考书籍《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明


类的初始化阶段是类加载过程的最后一个步骤。

之前介绍的几个类加载的动作里

  • 除了在加载阶段用户应用程序可以通过自定义类加载器的方式局部参与外,其余动作都完全由 Java 虚拟机来主导控制。

直到初始化阶段,Java 虚拟机才真正开始执行类中编写的 Java 程序代码,将主导权移交给应用程序。


进行准备阶段时,变量已经赋过一次系统要求的初始零值。

而在初始化阶段,则会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源。

我们也可以从另外一种更直接的形式来表达:初始化阶段就是执行类构造器 <clinit>() 方法的过程。

<clinit>() 并不是程序员在 Java 代码中直接编写的方法,它是 Javac 编译器的自动生成物。

但我们非常有必要了解这个方法具体是如何产生的,以及 <clinit>() 方法执行过程中各种可能会影响程序运行行为的细节。

这部分比起其他类加载过程更贴近于普通的程序开发人员的实际工作。

注意:这里的讨论只限于 Java 语言编译产生的 Class 文件,不包括其他 Java 虚拟机语言。


<clinit>() 方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{} 块)中的语句合并产生的。

编译器收集的顺序是由语句在源文件中出现的顺序决定的。

静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。


<clinit>() 方法与类的构造函数(即在虚拟机视角中的实例构造器 <init>() 方法)不同

  • 它不需要显式地调用父类构造器
  • Java 虚拟机会保证在子类的 <clinit>() 方法执行前,父类的 <clinit>() 方法已经执行完毕。

因此在 Java 虚拟机中第一个被执行的 <clinit>() 方法的类型肯定是 java.lang.Object

由于父类的 <clinit>() 方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。


<clinit>() 方法对于类或接口来说并不是必需的。

如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成 <clinit>() 方法。


接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成 <clinit>() 方法。

但接口与类不同的是,执行接口的 <clinit>() 方法不需要先执行父接口的 <clinit>() 方法

  • 因为只有当父接口中定义的变量被使用时,父接口才会被初始化。
  • 此外,接口的实现类在初始化时也一样不会执行接口的 <clinit>() 方法。

Java 虚拟机必须保证一个类的 <clinit>() 方法在多线程环境中被正确地加锁同步。

如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的 <clinit>() 方法,

其他线程都需要阻塞等待,直到活动线程执行完毕 <clinit>() 方法。

如果在一个类的 <clinit>() 方法中有耗时很长的操作,那就可能造成多个进程阻塞,在实际应用中这种阻塞往往是很隐蔽的。

注意

  • 在上述的情况中,其他线程虽然会被阻塞
  • 但如果执 <clinit>() 方法的那条线程退出 <clinit>() 方法后
  • 其他线程唤醒后则不会再次进入 <clinit>() 方法。
  • 同一个类加载器下,一个类型只会被初始化一次

16.3.1.练习一


问题:从字节码分析,使用 a,b,c 这三个常量是否会导致 E 初始化?

public class Load4 {
    public static void main(String[] args) {
        System.out.println(E.a);
        System.out.println(E.b);
        System.out.println(E.c);
    }
}

class E {
    public static final int a = 10;
    public static final String b = "hello";
    public static final Integer c = 20;  // Integer.valueOf(20)

	static {
        System.out.println("init E");
    }
}

答案

  • a、b 都是 static final 静态常量,a 是基本类型,b 是字符串常量,都不会发生类初始化
  • c 是 Integer,有自动装箱操作:Integer.valueof(20),故会发生类初始化

这里我们只需要看 E.class 的字节码信息即可(javap -v E.class

public static final int a;
  descriptor: I
  flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
  ConstantValue: int 10

public static final java.lang.String b;
  descriptor: Ljava/lang/String;
  flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
  ConstantValue: String hello

public static final java.lang.Integer c;
  descriptor: Ljava/lang/Integer;
  flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: bipush        20
         2: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         5: putstatic     #3                  // Field c:Ljava/lang/Integer;
         8: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        11: ldc           #5                  // String init E
        13: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        16: return

16.3.2.练习二


典型应用:完成懒惰加载初始化单例模式

public class Load9 {
    public static void main(String[] args) {
        // Singleton.test();
        Singleton.getInstance();
    }
}

class Singleton {

    public static void test() {
        System.out.println("test");
    }

    private Singleton() { }

    private static class LazyHolder {
        private static final Singleton SINGLETON = new Singleton();

        static { System.out.println("lazy holder init"); }
    }

    public static Singleton getInstance() {
        return LazyHolder.SINGLETON;
    }
}

以上的实现特点是:

  • 懒惰实例化
  • 初始化时的线程安全是有保障的

参考博客https://blog.csdn.net/weixin_53142722/article/details/125423522

  • 类加载的特性就是只有第一次使用这个类才会去加载这个类,触发类的加载链接
  • 只有访问这个内部类才会去加载这个类
    • 即你第一次去调用这个 getInstance 方法的时候才会导致内部类加载和初始化其静态成员INSTANCE);
  • 这种方式是线程安全性的由类加载器来保证这个单例的线程安全性

17.类加载器


17.1.基本概念


JDK 8为例

名称 加载哪的类 说明
Bootstrap ClassLoader JAVA_HOME/jre/lib 无法直接访问
Extension ClassLoader JAVA_HOME/jre/lib/ext 上级为 Bootstrap显示为 null
Application ClassLoader classpath 上级为 Extension
自定义类加载器 自定义 上级为 Application

参考书籍《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明


Java 虚拟机设计团队有意把类加载阶段中的 “通过一个类的全限定名来获取描述该类的二进制字节流” 这个动作,

放到 Java 虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。

实现这个动作的代码被称为 “类加载器”(Class Loader)。


类加载器虽然只用于实现类的加载动作,但它在 Java 程序中起到的作用却远超类加载阶段。

对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在 Java 虚拟机中的唯一性。

每一个类加载器,都拥有一个独立的类名称空间。

这句话可以表达得更通俗一些:比较两个类是否 “相等”只有在这两个类是由同一个类加载器加载的前提下才有意义

否则,即使这两个类来源于同一个 Class 文件被同一个 Java 虚拟机加载只要加载它们的类加载器不同那这两个类就必定不相等


这里所指的 “相等”

  • 包括代表类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果
  • 也包括了使用 instanceof 关键字做对象所属关系判定等各种情况。

如果没有注意到类加载器的影响,在某些情况下可能会产生具有迷惑性的结果。


站在 Java 虚拟机的角度来看,只存在两种不同的类加载器:

  • 一种是启动类加载器(BootstrapClassLoader),这个类加载器使用 C++ 语言实现,是虚拟机自身的一部分;
  • 另外一种就是其他所有的类加载器,这些类加载器都由 Java 语言实现
    • 独立存在于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader

站在 Java 开发人员的角度来看,类加载器就应当划分得更细致一些。

JDK 1.2 以来,Java 一直保持着三层类加载器、双亲委派的类加载架构。

尽管这套架构在 Java 模块化系统出现后有了一些调整变动,但依然未改变其主体结构。


17.2.启动类加载器


Bootstrap 类加载器加载类

public class F {
    static {
        System.out.println("bootstrap F init");
    }
}

执行

public class Load5_1 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.F");
        System.out.println(aClass.getClassLoader()); 
    }
}

输出

E:\git\jvm\out\production\jvm>java -Xbootclasspath/a:. cn.itcast.jvm.t3.load.Load5
bootstrap F init
null				# null 说明是启动类加载器加载的
  • -Xbootclasspath 表示设置 bootclasspath
  • 其中 /a:. 表示将当前目录追加至 bootclasspath 之后
  • 可以用这个办法替换核心类
    • java -Xbootclasspath:<new bootclasspath>
    • java -Xbootclasspath/a:<追加路径>
    • java -Xbootclasspath/p:<追加路径>

参考书籍《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明

启动类加载器Bootstrap Class Loader

  • 这个类加载器负责加载存放在 <JAVA_HOME>\lib 目录,或者被 -Xbootclasspath 参数所指定的路径中存放的,
  • 而且是 Java 虚拟机能够识别的类库加载到虚拟机的内存中。
    • (按照文件名识别,如 rt.jartools.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)
  • 启动类加载器无法被 Java 程序直接引用。
  • 用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器去处理,那直接使用 null 代替即可

17.3.扩展类加载器


public class G {
    static {
        //System.out.println("ext G init");
        System.out.println("classpath G init");
    }
}
/**
 * 演示 扩展类加载器
 * 在 C:\Program Files\Java\jdk1.8.0_91 下有一个 my.jar
 * 里面也有一个 G 的类,观察到底是哪个类被加载了
 */
public class Load5_2s {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.G");
        System.out.println(aClass.getClassLoader());
    }
}

先运行一遍代码,之后重写 G.java

public class G {
    static {
        System.out.println("ext G init");
        //System.out.println("classpath G init");
    }
}

输出

classpath G init
sun.misc.Launcher$AppClassLoader@18b4aac2 # 显然,这是应用程序类加载器加载的

再打个 jar 包(jar -cvf my.jar cn\itcast\jvm\t3\load\G.class

E:\git\jvm\out\production\jvm>jar -cvf my.jar cn\itcast\jvm\t3\load\G.class
已添加清单
正在添加: cn/itcast/jvm/t3/load/G.class(输入 = 481) (输出 = 322)(压缩了 33%)

jar 包拷贝到 JAVA_HOME/jre/lib/ext

之后重新执行 Load5_2.java

再输出

ext G init
sun.misc.Launcher$ExtClassLoader@29453f44  # 显然,加载的是扩展类加载器的 G

结论

  • 如果 classpathJAVA_HOME/jre/lib/ext 下有同名类,加载时会使用扩展类加载器加载。
  • 当应用程序类加载器发现拓展类加载器已将该同名类加载过了,则不会再次加载

参考书籍《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明

扩展类加载器Extension Class Loader

  • 这个类加载器是在类 sun.misc.Launcher$ExtClassLoader 中以 Java 代码的形式实现的。
  • 它负责加载 <JAVA_HOME>\lib\ext 目录中,或者被 java.ext.dirs 系统变量所指定的路径中所有的类库。
  • 根据 “扩展类加载器” 这个名称,就可以推断出这是一种 Java 系统类库的扩展机制,
  • JDK 的开发团队允许用户将具有通用性的类库放置在 ext 目录里以扩展 Java SE 的功能。
  • JDK 9 之后,这种扩展机制被模块化带来的天然的扩展能力所取代。
  • 由于扩展类加载器是由 Java 代码实现的,开发者可以直接在程序中使用扩展类加载器来加载 Class 文件。

17.4.应用类加载器


public class G {
    static {
        //System.out.println("ext G init");
        System.out.println("classpath G init");
    }
}
public class Load5_2 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.G");
        System.out.println(aClass.getClassLoader());
    }
}

输出

classpath G init
sun.misc.Launcher$AppClassLoader@18b4aac2 # 显然,这是应用程序类加载器加载的

参考书籍《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明

应用程序类加载器Application Class Loader

  • 这个类加载器由 sun.misc.Launcher$AppClassLoader 来实现。
  • 由于应用程序类加载器是 ClassLoader 类中的 getSystem.ClassLoader() 方法的返回值,所以有些场合中也称它为“系统类加载器”。
  • 它负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。
  • 如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

17.4.双亲委派机制


所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则

注意:这里的双亲,翻译为上级似乎更为合适,因为它们并没有继承关系


参考书籍《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明

  • 双亲委派模型的工作过程是
    • 如果一个类加载器收到了类加载的请求它首先不会自己去尝试加载这个类
    • 而是把这个请求委派给父类加载器去完成每一个层次的类加载器都是如此
    • 因此所有的加载请求最终都应该传送到最顶层的启动类加载器中
    • 只有当父加载器反馈自己无法完成这个加载请求它的搜索范围中没有找到所需的类
    • 子加载器才会尝试自己去完成加载
  • 使用双亲委派模型来组织类加载器之间的关系有一个显而易见的好处Java 中的类随着它的类加载器一起具备了一种带有优先级的层次关系
    • 例如类 java.lang.Object它存放在 rt.jar 之中
      • 无论哪一个类加载器要加载这个类最终都是委派给处于模型最顶端的启动类加载器进行加载
      • 因此 Object 类在程序的各种类加载器环境中都能够保证是同一个类
    • 反之如果没有使用双亲委派模型都由各个类加载器自行去加载的话
      • 如果用户自己也编写了一个名为 java.lang.Object 的类并放在程序的 ClassPath
      • 那系统中就会出现多个不同的 ObjectJava 类型体系中最基础的行为也就无从保证应用程序将会变得一片混乱
  • 双亲委派模型对于保证 Java 程序的稳定运作极为重要但它的实现却异常简单
    • 用以实现双亲委派的代码只有短短十余行全部集中在 java.lang.ClassLoaderloadClass() 方法之中
    • 该方法的代码的逻辑清晰易懂
      • 先检查请求加载的类型是否已经被加载过
        • 若没有则调用父加载器的 loadClass() 方法
        • 若父加载器为空则默认使用启动类加载器作为父加载器
      • 假如父类加载器加载失败抛出 ClassNotFoundException 异常的话才调用自己的 findClass() 方法尝试进行加载

源码

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 1.检查该类是否已经加载
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    // 2.有上级的话,则委派上级 loadclass
                    c = parent.loadClass(name, false);
                } else {
                    // 3.如果没有上级了(ExtClassLoader),则委派 BootstrapClassLoader
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found from the non-null parent class loader
            }
 
            if (c == null) {
                long t1 = System.nanoTime();
                // 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载
                c = findClass(name);
 
                // 5.记录耗时
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

演示

public class Load5_3 {
    public static void main(String[] args) throws ClassNotFoundException {
        System.out.println(Load5_3.class.getClassLoader());
        Class<?> aClass = Load5_3.class.getClassLoader().loadClass("cn.itcast.jvm.t3.load.H");
        System.out.println(aClass.getClassLoader());
    }
}

通过 Idea 的 Debug 工具来查看执行流程为:

  1. sun.misc.Launcher$AppClassLoader // 1 处, 开始查看已加载的类,结果没有
  2. sun.misc.Launcher$AppClassLoader // 2 处,委派上级 sun.misc.Launcher$ExtClassLoader.loadClass()
  3. sun.misc.Launcher$ExtClassLoader // 1 处,查看已加载的类,结果没有
  4. sun.misc.Launcher$ExtClassLoader // 3 处,没有上级了,则委派 BootstrapClassLoader 查找
  5. BootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 H 这个类,显然没有
  6. sun.misc.Launcher$ExtClassLoader // 4 处,调用自己的 findClass 方法,是在 JAVA_HOME/jre/lib/ext 下找 H 这个类,显然没有。
    回到 sun.misc.Launcher$AppClassLoader 的 // 2 处
  7. 继续执行到 sun.misc.Launcher$AppClassLoader // 4 处,调用它自己的 findClass 方法,在 classpath 下查找,找到了

17.5.线程上下文类加载


我们在使用 JDBC 时,都需要加载 Driver 驱动,不知道你注意到没有,不写 Class.forName("com.mysql.jdbc.Driver")

也是可以让 com.mysql.jdbc.Driver 正确加载的,你知道是怎么做的吗?

追踪一下源码

public class DriverManager {
	// 注册驱动的集合
	private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
	
	// 初始化驱动
	static {
		loadInitialDrivers();
		println("JDBC DriverManager initialized");
	}
	
	//... ...
}

不看别的,看看 DriverManager 的类加载器

System.out.println(DriverManager.class.getClassLoader());

打印结果是 null

这表示它的类加载器是 Bootstrap ClassLoader,会到 JAVA_HOME/jre/lib 下搜索类

JAVA_HOME/jre/lib 下显然没有 mysql-connector-java-5.1.47.jar

这样问题来了,在 DriverManager 的静态代码块中,怎么能正确加载 com.mysql.jdbc.Driver 呢?

继续查看 loadInitilaDrivers 方法

private static void loadInitialDrivers() {
    String drivers;
    try {
        drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
            public String run() {
                return System.getProperty("jdbc.drivers");
            }
        });
    } catch (Exception ex) {
        drivers = null;
    }

	// 1.使用 ServiceLoader 机制加载驱动,即 SPI */
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {

            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            Iterator<Driver> driversIterator = loadedDrivers.iterator();

            try{
                while(driversIterator.hasNext()) {
                    driversIterator.next();
                }
            } catch(Throwable t) {
            // Do nothing
            }
            return null;
        }
    });

    println("DriverManager.initialize: jdbc.drivers = " + drivers);

	// 2.使用 jdbc.drivers 定义的驱动名加载驱动
    if (drivers == null || drivers.equals("")) {
        return;
    }
    String[] driversList = drivers.split(":");
    println("number of Drivers:" + driversList.length);
    for (String aDriver : driversList) {
        try {
            println("DriverManager.Initialize: loading " + aDriver);
            // 这里的 ClassLoader.getSystemClassLoader() 就是应用程序类加载器
            Class.forName(aDriver, true, ClassLoader.getSystemClassLoader());
        } catch (Exception ex) {
            println("DriverManager.Initialize: load failed: " + ex);
        }
    }
}

先看 2 处,发现它最后是使用 Class.forName 完成类的加载和初始化,关联的是应用程序类加载器,因此可以顺利完成类加载

再看 1 处,它就是大名鼎鼎的 Service Provider InterfaceSPI

SPI 约定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名为文件,文件内容是实现类名称

在这里插入图片描述

这样就可以使用下面的代码块来得到实现类

ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class);

Iterator<接口类型> iter = allImpls.iterator();

while(iter.hasNext()) { iter.next(); }

这里体现的是 面向接口编程 + 解耦 的思想,许多框架中都用到了该思想

  • JDBCServelt 初始化器、Spring 容器、Dubbo(对 SPI 进行了扩展)

接着看 ServiceLoader.load 方法

public static <S> ServiceLoader<S> load(Class<S> service) {
	// 获取线程上下文类加载器
	ClassLoader cl = Thread.currentThread().getContextClassLoader();
	return ServiceLoader.load(service, cl);
}

线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器

它内部又是由 Class.forName 调用了线程上下文类加载器完成类加载

具体代码在 ServiceLoader 的内部类 LazyIterator

private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
        c = Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
        fail(service,
             "Provider " + cn + " not found");
    }
    if (!service.isAssignableFrom(c)) {
        fail(service,
             "Provider " + cn  + " not a subtype");
    }
    try {
        S p = service.cast(c.newInstance());
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
        fail(service,
             "Provider " + cn + " could not be instantiated",
             x);
    }
    throw new Error();          // This cannot happen
}

参考书籍《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明


线程上下文类加载器Thread Context ClassLoader)。

  • 这个类加载器可以通过 java.lang.Thread 类的 setContext-ClassLoader() 方法进行设置
  • 如果创建线程时还未设置,它将会从父线程中继承一个
  • 如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

有了线程上下文类加载器,程序就可以做一些 “舞弊” 的事情了。

JNDI 服务使用这个线程上下文类加载器去加载所需的 SPI 服务代码

  • 这是一种父类加载器去请求子类加载器完成类加载的行为,
  • 这种行为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器,
  • 已经违背了双亲委派模型的一般性原则,但也是无可奈何的事情。

Java 中涉及 SPI 的加载基本上都采用这种方式来完成,例如 JNDIJDBCJCEJAXB 和 JBI 等。

不过,当 SPI 的服务提供者多于一个的时候,代码就只能根据具体提供者的类型来硬编码判断

为了消除这种极不优雅的实现方式,JDK 6JDK 提供了 java.util.ServiceLoader

  • META-INF/services 中的配置信息辅以责任链模式

这才算是给 SPI 的加载提供了一种相对合理的解决方案。


17.6.自定义类加载器


什么时候需要自定义类加载器?

  1. 想加载非 classpath 随意路径中的类文件
  2. 都是通过接口来使用实现,希望解耦时,常用在框架设计
  3. 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器

步骤:

  1. 继承 ClassLoader 父类
  2. 要遵从双亲委派机制,重写 findClass 方法
    • 注意不是重写 loadClass 方法,否则不会走双亲委派机制
  3. 读取类文件的字节码
  4. 调用父类的 defineClass 方法来加载类
  5. 使用者调用该类加载器的 loadClass 方法

准备好两个类文件放入 E:\myclasspath,它实现了 java.util.Map 接口,可以先反编译看一下

E:lmyclasspath>javap MapInp11.c1ass
Compi1ed from"MapInp11.java"
public class MapImp11 extends java.uti1.AbstractMap imp1ements java.uti1.Map{
	pub1ic MapInp11();
	public java.uti1.Set<java.uti1.Map$Entry> entrySet();
	pub1ic java.1ang.String toString();
	static {};
}

Java 代码

package cn.itcast.jvm.t3.load;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

public class Load7 {
    public static void main(String[] args) throws Exception {
        MyClassLoader classLoader = new MyClassLoader();
        Class<?> c1 = classLoader.loadClass("MapImpl1");
        Class<?> c2 = classLoader.loadClass("MapImpl1");
        System.out.println(c1 == c2); // true

        MyClassLoader classLoader2 = new MyClassLoader();
        Class<?> c3 = classLoader2.loadClass("MapImpl1");
        /* 比较两个类是否 “相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义。
		 * 否则,即使这两个类来源于同一个 Class 文件,被同一个 Java 虚拟机加载
		 * * 只要加载它们的类加载器不同,那这两个类就必定不相等。
		 * */
        System.out.println(c1 == c3); // false
		
        c1.newInstance();
    }
}

class MyClassLoader extends ClassLoader {
    @Override // name 就是类名称
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String path = "e:\\myclasspath\\" + name + ".class";

        try {
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            Files.copy(Paths.get(path), os);

            // 得到字节数组
            byte[] bytes = os.toByteArray();

            // byte[] -> *.class
            return defineClass(name, bytes, 0, bytes.length);

        } catch (IOException e) {
            e.printStackTrace();
            throw new ClassNotFoundException("类文件未找到", e);
        }
    }
}

打印结果

ture
false
Map impl1 init...

17.7.破坏双亲委派机制


参考博客:JVM学习


怎么做打才能破双亲委派模型?

  • 自定义类加载器,继承 ClassLoader 类,重写 loadClass 方法和 findClass 方法。

列举一些你知道的打破双亲委派机制的例子,为什么要打破?

  • JNDIJava Naming and Directory Interface)通过引入线程上下文类加载器
    • 可以在 Thread.setContextClassLoader 方法设置,默认是应用程序类加载器,来加载 SPI 的代码。
    • 有了线程上下文类加载器,就可以完成父类加载器请求子类加载器完成类加载的行为。
    • 打破的原因,是为了 JNDI 服务的类加载器是启动器类加载,为了完成高级类加载器请求子类加载器(即线程上下文加载器)加载类。
  • Tomcat,应用的类加载器优先自行加载应用目录下的 class,并不是先委派给父加载器,加载不了才委派给父加载器。
  • OSGi,实现模块化热部署,为每个模块都自定义了类加载器,需要更换模块时,模块与类加载器一起更换。
    • 其类加载的过程中,有平级的类加载器加载行为。
    • 打破的原因是为了实现模块热替换
  • JDK 9Extension ClassLoaderPlatform ClassLoader 取代
    • 当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前
    • 要先判断该类是否能够归属到某一个系统模块中
    • 如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。
    • 打破的原因,是为了添加模块化的特性。

Tomcat 之所以造了一堆自己的 classloader,大致是出于下面三类目的:

  • 对于各个 webapp 中的 classlib,需要相互隔离,不能出现一个应用中加载的类库会影响另一个应用的情况
    • 而对于许多应用,需要有共享的 lib 以便不浪费资源。
  • JVM一样的安全性问题。
    • 使用单独的 classloader 去装载 tomcat 自身的类库,以免其他恶意或无意的破坏。
  • 热部署。

Tomcat 类加载器如下图

在这里插入图片描述


参考博客https://blog.csdn.net/weixin_53142722/article/details/125423522

双亲委派模型的第一次 “被破坏” 其实发生在双亲委派模型出现之前(即 JDK1.2 面世以前的 “远古” 时代)

  • 建议用户重写 findClass() 方法,在类加载器中的 loadClass() 方法中也会调用该方法

双亲委派模型的第二次 “被破坏” 是由这个模型自身的缺陷导致的

  • 如果有基础类型又要调用回用户的代码,此时也会破坏双亲委派模式

双亲委派模型的第三次 “被破坏” 是由于用户对程序动态性的追求而导致的

  • 这里所说的“动态性”指的是一些非常“热”门的名词:代码热替换Hot Swap)、模块热部署Hot Deployment)等

18.运行期优化


18.1.即时编译


18.1.1.分层编译


分层编译TieredCompilation

先跑一段代码,查看控制台输出情况,发现所耗时间有明显的分层现象

public class JIT1 {
    // -XX:+PrintCompilation -XX:-DoEscapeAnalysis
    public static void main(String[] args) {
        for (int i = 0; i < 200; i++) {
            long start = System.nanoTime();
            for (int j = 0; j < 1000; j++) {
                new Object();
            }
            long end = System.nanoTime();
            System.out.printf("%d\t%d\n",i,(end - start));
        }
    }
}

JVM 将执行状态分成了 5 个层次

  • 0 层,解释执行(Interpreter
  • 1 层,使用 C1 即时编译器编译执行(不带 profiling
  • 2 层,使用 C1 即时编译器编译执行(带基本的 profiling
  • 3 层,使用 C1 即时编译器编译执行(带完全的 profiling
  • 4 层,使用 C2 即时编译器编译执行

profiling 是指在运行过程中收集一些程序执行状态的数据

  • 例如方法的调用次数循环的回边次数

即时编译器(JIT)与解释器的区别

  • 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
  • JIT 是将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需再编译
  • 解释器是将字节码解释为针对所有平台都通用的机器码
  • JIT 会根据平台类型,生成平台特定的机器码

对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;

另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。

执行效率上简单比较一下 Interpreter < C1 < C2

  • 总的目标是发现热点代码(Hotspot 名称的由来),并加以优化。

C1 可以提升 5 倍左右到效率,C2 可以提高大约 10-100 倍的效率


刚才的一种优化手段称之为 逃逸分析,发现新建的对象是否逃逸。

逃逸分析简单来讲就是

  • Hotspot 虚拟机可以分析新创建对象的使用范围
  • 并决定是否在 Java 堆上分配内存的一项技术

例如上述的情况:

  • 1000 个 Object 对象循环创建,但是从来没用过
  • 就会在一段时间后发生逃逸,修改字节码,后续使它实际上没有被创建

可以使用 -XX:-DoEscapeAnalysis 关闭逃逸分析,再运行刚才的示例观察结果

可以发现后续的速度虽然变快了,但是没有明显的多次分层现象了

参考资料:https://docs.oracle.com/en/java/javase/12/vm/java-hotspot-virtual-machine-performance-enhancements.html


参考书籍《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明


逃逸分析(Escape Analysis)是目前 Java 虚拟机中比较前沿的优化技术。

它与类型继承关系分析一样,并不是直接优化代码的手段,而是为其他优化措施提供依据的分析技术。


逃逸分析的基本原理是

  • 分析对象动态作用域,当一个对象在方法里面被定义后,它可能被外部方法所引用
    • 例如作为调用参数传递到其他方法中,这种称为方法逃逸
    • 甚至还有可能被外部线程访问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸
    • 从不逃逸方法逃逸线程逃逸,称为对象由低到高的不同逃逸程度。

如果能证明一个对象不会逃逸到方法或线程之外(换句话说是别的方法或线程无法通过任何途径访问到这个对象),

或者逃逸程度比较低(只逃逸出方法而不会逃逸出线程),则可能为这个对象实例采取不同程度的优化。

如:栈上分配、标量替换、同步消除


如果有需要,或者确认对程序运行有益,

用户也可以使用参数 -XX:+DoEscapeAnalysis 来手动开启逃逸分析,

开启之后可以通过参数 -XX:+PrintEscapeAnalysis 来查看分析结果。


有了逃逸分析支持之后,

用户可以使用参数 -XX:+EliminateAllocations 来开启标量替换,

使用 +XX:+EliminateLocks 来开启同步消除,

使用参数 -XX:+PrintEliminateAllocations 查看标量的替换情况。


18.1.2.方法内联


方法内联Inlining


private static int square(final int i) {
	return i * i;
}
System.out.println(square(9));

如果发现 square 是热点方法,并且长度不太长时,会进行内联。

所谓的内联就是把方法内代码拷贝、粘贴到调用者的位置:

System.out.println(9 * 9);

还能够进行常量折叠(constant folding)的优化

System.out.println(81);

实验

public class JIT2 {
    // -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining -XX:CompileCommand=dontinline,*JIT2.square
    // -XX:+PrintCompilation
    public static void main(String[] args) {

        int x = 0;
        for (int i = 0; i < 500; i++) {
            long start = System.nanoTime();
            for (int j = 0; j < 1000; j++) {
                x = square(9);
            }
            long end = System.nanoTime();
            System.out.printf("%d\t%d\t%d\n",i,x,(end - start));
        }
    }

    private static int square(final int i) {
        return i * i;
    }
}

18.1.3.字段优化


JMH 基准测试请参考:http://openjdk.java.net/projects/code-tools/jmh/

创建 maven 工程,添加依赖如下

<dependency>
	<groupId>org.openjdk.jmh</groupId>
	<artifactId>jmh-core</artifactId>
	<version>1.0</version>
</dependency>

<dependency>
	<groupId>org.openjdk.jmh</groupId>
	<artifactId>jmh-generator-annprocess</artifactId>
	<version>1.0</version>
	<scope>provided</scope>
</dependency>

编写基准测试代码

package test;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;

@Warmup(iterations = 2, time = 1)
@Measurement(iterations = 5, time = 1)
@State(Scope.Benchmark)
public class Benchmark1 {

    int[] elements = randomInts(1_000);

    private static int[] randomInts(int size) {
        Random random = ThreadLocalRandom.current();
        int[] values = new int[size];
        for (int i = 0; i < size; i++) {
            values[i] = random.nextInt();
        }
        return values;
    }

    @Benchmark
    public void test1() {
        for (int i = 0; i < elements.length; i++) {
            doSum(elements[i]);
        }
    }

    @Benchmark
    public void test2() {
        int[] local = this.elements;
        for (int i = 0; i < local.length; i++) {
            doSum(local[i]);
        }
    }

    @Benchmark
    public void test3() {
        for (int element : elements) {
            doSum(element);
        }
    }

    static int sum = 0;

    @CompilerControl(CompilerControl.Mode.DONT_INLINE)
    static void doSum(int x) {
        sum += x;
    }


    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(Benchmark1.class.getSimpleName())
                .forks(1)
                .build();

        new Runner(opt).run();
    }
}

具体分析见视频运行期优化_字段优化_1运行期优化_字段_2


首先启用 doSum 的方法内联,测试结果如下(每秒吞吐量,分数越高的更好)

Benchmark         		Mode   		Samples 	Score 			Score error 	Units
t.Benchmark1.test1      thrpt		5 			2420286.539 	390747.467 		ops/s
t.Benchmark1.test2 		thrpt 		5 			2544313.594 	91304.136 		ops/s
t.Benchmark1.test3 		thrpt 		5 			2469176.697 	450570.647 		ops/s

接下来禁用 doSum 方法内联

@CompilerControl(CompilerControl.Mode.DONT_INLINE)
static void doSum(int x) {
	sum += x;
}

测试结果如下

Benchmark Mode 				Samples 	Score 			Score error 	Units
t.Benchmark1.test1 thrpt 	5 			296141.478 		63649.220 		ops/s
t.Benchmark1.test2 thrpt 	5 			371262.351 		83890.984 		ops/s
t.Benchmark1.test3 thrpt 	5 			368960.847 		60163.391 		ops/s

分析

  • 在刚才的示例中,doSum 方法是否内联会影响 elements 成员变量读取的优化:
  • 如果 doSum 方法内联了,刚才的 test1 方法会被优化成下面的样子(伪代码)
@Benchmark
public void test1() {
	// elements.length 首次读取会缓存起来 -> int[] local
	for (int i = 0; i < elements.length; i++) { // 后续 999 次 求长度 <- local
		sum += elements[i]; // 1000 次取下标 i 的元素 <- local
	}
}

可以节省 1999 次 Field 读取操作

但如果 doSum 方法没有内联,则不会进行上面的优化


18.2.反射优化


import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Reflect1 {
    public static void foo() {
        System.out.println("foo...");
    }
    
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {
        Method foo = Reflect1.class.getMethod("foo");
        for (int i = 0; i <= 16; i++) {
            System.out.printf("%d\t", i);
            foo.invoke(null);
        }
        System.in.read();
    }
}

具体分析请见视频:反射优化_1反射优化_2


foo.invoke 前面 0 ~ 15 次调用使用的是 MethodAccessorNativeMethodAccessorImpl 实现

package sun.reflect;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import sun.reflect.misc.ReflectUtil;

class NativeMethodAccessorImpl extends MethodAccessorImpl {
    private final Method method;
    private DelegatingMethodAccessorImpl parent;
    private int numInvocations;

    NativeMethodAccessorImpl(Method var1) {
        this.method = var1;
    }
	
	// inflationThreshold 膨胀阈值,默认 15
    public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
        if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
        	// 使用 ASM 动态生成的新实现代替本地实现,速度较本地实现快 20 倍左右
            MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());
            this.parent.setDelegate(var3);
        }
		
		// 调用本地实现
        return invoke0(this.method, var1, var2);
    }

    void setParent(DelegatingMethodAccessorImpl var1) {
        this.parent = var1;
    }

    private static native Object invoke0(Method var0, Object var1, Object[] var2);
}

当调用到第 16 次(从 0 开始算)时,会采用运行时生成的类代替掉最初的实现

可以通过 debug 得到类名为 sun.reflect.GeneratedMethodAccessor1

可以使用阿里的 arthas 工具

java -jar arthas-boot.jar
[INFO] arthas-boot version: 3.1.1
[INFO] Found existing java process, please choose one and hit RETURN.
* [1]: 13065 cn.itcast.jvm.t3.reflect.Reflect1

选择 1,回车,表示分析该进程


1
[INFO] arthas home: /root/.arthas/lib/3.1.1/arthas
[INFO] Try to attach process 13065
[INFO] Attach process 13065 success.
[INFO] arthas-client connect 127.0.0.1 3658
  , --- .  ,------.  ,--------.,--. ,--.   , --- .    ,---.
 /   O   \ |  .--.  ''--.  .--'|  '--'  | /   O   \  '   .-'
|  . - .  ||  '--'. '   |  |   |  .--.  ||   .-.   | `.   `-.
|  |   |  ||    |\ \    |  |   |  |  |  ||  |   |  | .-'     |
`--'   `--'` -- ' '--'  `--'   `--'  `--'`--'   `--' `-----'

wiki https://alibaba.github.io/arthas
tutorials https://alibaba.github.io/arthas/arthas-tutorials
version 3.1.1
pid 13065
time 2019-06-10 12:23:54

再输入 jad + 类名 来进行反编译

$ jad sun.reflect.GeneratedMethodAccessor1

ClassLoader:

+-sun.reflect.DelegatingClassLoader@15db9742

	+-sun.misc.Launcher$AppClassLoader@4e0e2f2a
	
		+-sun.misc.Launcher$ExtClassLoader@2fdb006e


Location:


/*
 * Decompiled with CFR 0_132.
 *
 * Could not load the following classes:
 * cn.itcast.jvm.t3.reflect.Reflect1
*/
package sun.reflect;

import cn.itcast.jvm.t3.reflect.Reflect1;
import java.lang.reflect.InvocationTargetException;
import sun.reflect.MethodAccessorImpl;

public class GeneratedMethodAccessor1 extends MethodAccessorImpl {
	/*
	* Loose catch block
	* Enabled aggressive block sorting
	* Enabled unnecessary exception pruning
	* Enabled aggressive exception aggregation
	* Lifted jumps to return sites
	*/
	public Object invoke(Object object, Object[] arrobject) throws InvocationTargetException {
		// 比较奇葩的做法,如果有参数,那么抛非法参数异常
		block4 : {
			if (arrobject == null || arrobject.length == 0) break block4;
			throw new IllegalArgumentException();
		}
		try {
		// 可以看到,已经是直接调用了
		Reflect1.foo();
		// 因为没有返回值
		return null;
		}catch (Throwable throwable) {
			throw new InvocationTargetException(throwable);
		}catch (ClassCastException | NullPointerException runtimeException) {
		throw new IllegalArgumentException(Object.super.toString());
		}
	}
}

Affect(row-cnt:1) cost in 1540 ms.

注意

  • 通过查看 ReflectionFactory 源码可知 sun.reflect.noInflation
    • 可以用来禁用膨胀(直接生成 GeneratedMethodAccessor1,但首次生成比较耗时,如果仅反射调用一次,不划算)
    • sun.reflect.inflationThreshold 可以修改膨胀阈值

18.3.本章节吐槽


这个学习视频的 运行期优化 的部分讲的是非常的浅显的 (内容太少了)

在这里插入图片描述

若是需要更深入了解运行期优化部分的话,还请多多看书:《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》

这里的运行期优化的内容大致对应着书中的第四部分:程序编译与代码优化(主要是书中第 11 章)

在这里插入图片描述


两者对比,发现这段教程(p159 ~ p164)的内容确实蛮少的(缺的内容太多了)。所以强烈建议看完教程后,仍然要多多看书



网站公告

今日签到

点亮在社区的每一天
去签到