JVM类加载与字节码技术

  • 获得更好的阅读体验,可以移步至我的个人博客:https://cyborg2077.github.io/
    1. JVM导学:https://cyborg2077.github.io/2023/03/26/JvmPart1/
    2. JVM内存结构:https://cyborg2077.github.io/2023/03/27/JvmPart2/
    3. JVM垃圾回收:https://cyborg2077.github.io/2023/04/01/JvmPart3/
    4. JVM类加载与字节码技术:https://cyborg2077.github.io/2023/04/05/JvmPart4/
    5. JVM内存模型:https://cyborg2077.github.io/2023/04/11/JvmPart5/
    6. JVM相关面试题:https://cyborg2077.github.io/2023/04/15/JvmPart6/

类文件结构

  • 一个简单的HelloWorld.java
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}
  • 编译后查看其二进制字节码文件的内容
od -t xC target/classes/com/demo/HelloWorld.class

  • 根据JVM规范,类文件结构如下
ClassFile {
    u4 magic;                  // 魔数,用于标识文件类型
    u2 minor_version;          // Java虚拟机的次版本号
    u2 major_version;          // Java虚拟机的主版本号
    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]; // 类或接口的附加属性信息数组
}

魔数

  • 以下面的字节码文件,按顺序逐个进行分析
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
  • 0-3字节,表示它是否是class类型的文件
    • 0000000 ca fe ba be 00 00 00 34 00 22 0a 00 06 00 14 09
  • 在Java中,所有的.class文件都以魔数ca fe ba be开头,这个魔数的前4个字节用于识别该文件是否为Java类文件,如果这个魔数不匹配,那么Java虚拟机将无法加载该文件。
    {% note info no-icon %}
  • 关于cafebabe这个魔数的由来并没有具体的官方解释,但有一些有趣的猜测和传说。
    • 一种说法是,这个魔数是由Java的创造者之一、现任谷歌高管James Gosling取的。据说Gosling是个爱好咖啡的人,他认为Java这个名字也与咖啡有关,所以他将cafebabe取作魔数来向咖啡致敬。另外,有一种传说认为这个魔数是来自于一个好莱坞电影中的经典台词,cafe babe(咖啡宝贝?)。
    • 然而,无论是什么样的由来,cafebabe这个魔数现在已经成为Java世界中的一个标志,每一个Java程序员都能够轻松地辨认出这个魔数,这也是Java文件格式稳定性的一个体现。
      {% endnote %}

版本

  • 4-7字节,表示类的版本 00 34(52)对应十进制为52,表示的是Java 8

常量池

Constant Type Value
CONSTANT_Utf8 1
CONSTANT_Integer 3
CONSTANT_Float 4
CONSTANT_Long 5
CONSTANT_Double 6
CONSTANT_Class 7
CONSTANT_String 8
CONSTANT_Fieldref 9
CONSTANT_Methodref 10
CONSTANT_InterfaceMethodref 11
CONSTANT_NameAndType 12
CONSTANT_MethodHandle 15
CONSTANT_MethodType 16
CONSTANT_InvokeDynamic 18
  • 8-9字节,表示常量池长度
    • 0000000 ca fe ba be 00 00 00 34 00 22 0a 00 06 00 14 09
    • 00 22(34),表示常量池有#1-#33项,注意#0项不计入,也没有值
  1. 第#1项 0a 表示一个 Method 信息,00 06 和 00 14(20) 表示它引用了常量池中 #6 和 #20 项来获得这个方法的所属类方法名

    • 0000000 ca fe ba be 00 00 00 34 00 22 0a 00 06 00 14 09
  2. 第#2项 09 表示一个 Field 信息,00 16(22)和 00 17(23) 表示它引用了常量池中 #22 和 # 23 项来获得这个成员变量的所属类成员变量名

    • 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
  3. 第#3项 08 表示一个字符串常量名称,00 18(24)表示它引用了常量池中 #24 项

    • 0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07
  4. 第#4项 0a 表示一个 Method 信息,00 19(25) 和 00 1a(26) 表示它引用了常量池中 #25 和 #26项来获得这个方法的所属类方法名

    • 0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07
  5. 第#5项 07 表示一个 Class 信息,00 1b(27) 表示它引用了常量池中 #27 项

    • 0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07
  6. 第#6项 07 表示一个 Class 信息,00 1c(28) 表示它引用了常量池中 #28 项

    • 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
  7. 第#7项 01 表示一个 utf8 串,00 06 表示长度,3c 69 6e 69 74 3e 是<init>

    • 0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29
  8. 第#8项 01 表示一个 utf8 串,00 03 表示长度,28 29 56 是()V其实就是表示无参、无返回值

    • 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
  9. 第#9项 01 表示一个 utf8 串,00 04 表示长度,43 6f 64 65 是Code

    • 0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e
  10. 第#10项 01 表示一个 utf8 串,00 0f(15) 表示长度,4c 69 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65 是LineNumberTable

    扫描二维码关注公众号,回复: 15210194 查看本文章
    • 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
  11. 第#11项 01 表示一个 utf8 串,00 12(18) 表示长度,4c 6f 63 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65是LocalVariableTable

    • 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
  12. 第#12项 01 表示一个 utf8 串,00 04 表示长度,74 68 69 73 是this

    • 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
  13. 第#13项 01 表示一个 utf8 串,00 1d(29) 表示长度,是Lcn/itcast/jvm/t5/HelloWorld;

    • 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
  14. 第#14项 01 表示一个 utf8 串,00 04 表示长度,74 68 69 73 是main

    • 0000200 57 6f 72 6c 64 3b 01 00 04 6d 61 69 6e 01 00 16
  15. 第#15项 01 表示一个 utf8 串,00 16(22) 表示长度,是([Ljava/lang/String;)V其实就是参数为字符串数组,无返回值

    • 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
  16. 第#16项 01 表示一个 utf8 串,00 04 表示长度,是args

    • 0000240 69 6e 67 3b 29 56 01 00 04 61 72 67 73 01 00 13
  17. 第#17项 01 表示一个 utf8 串,00 13(19) 表示长度,是[Ljava/lang/String;

    • 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
  18. 第#18项 01 表示一个 utf8 串,00 10(16) 表示长度,是MethodParameters

    • 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
  19. 第#19项 01 表示一个 utf8 串,00 0a(10) 表示长度,是SourceFile

    • 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
  20. 第#20项 01 表示一个 utf8 串,00 0f(15) 表示长度,是HelloWorld.java

    • 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
  21. 第#21项 0c 表示一个 名+类型,00 07 00 08 引用了常量池中 #7 #8 两项

    • 0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e
  22. 第#22项 07 表示一个 Class 信息,00 1d(29) 引用了常量池中 #29 项

    • 0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e
  23. 第#23项 0c 表示一个 名+类型,00 1e(30) 00 1f (31)引用了常量池中 #30 #31 两项

    • 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
  24. 第#24项 01 表示一个 utf8 串,00 0f(15) 表示长度,是hello world

    • 0000400 00 1f 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64
  25. 第#25项 07 表示一个 Class 信息,00 20(32) 引用了常量池中 #32 项

    • 0000420 07 00 20 0c 00 21 00 22 01 00 1b 63 6e 2f 69 74
  26. 第#26项 0c 表示一个 名+类型,00 21(33) 00 22(34)引用了常量池中 #33 #34 两项

    • 0000420 07 00 20 0c 00 21 00 22 01 00 1b 63 6e 2f 69 74
  27. 第#27项 01 表示一个 utf8 串,00 1b(27) 表示长度,是cn/itcast/jvm/t5/HelloWorld

    • 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
  28. 第#28项 01 表示一个 utf8 串,00 10(16) 表示长度,是java/lang/Object

    • 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
  29. 第#29项 01 表示一个 utf8 串,00 10(16) 表示长度,是java/lang/System

    • 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
  30. 第#30项 01 表示一个 utf8 串,00 03 表示长度,是out

    • 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
  31. 第#31项 01 表示一个 utf8 串,00 15(21) 表示长度,是Ljava/io/PrintStream;

    • 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
  32. 第#32项 01 表示一个 utf8 串,00 13(19) 表示长度,是java/io/PrintStream

    • 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
  33. 第#33项 01 表示一个 utf8 串,00 07 表示长度,是println

    • 0000620 01 00 07 70 72 69 6e 74 6c 6e 01 00 15 28 4c 6a
  34. 第#34项 01 表示一个 utf8 串,00 15(21) 表示长度,是(Ljava/lang/String;)V

    • 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

访问标识与继承信息

  • 访问标识符:21表示class是一个类,公共的
    • 0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
  • 当前类或接口的索引:05表示根据常量池中的#5找到本类的全限定名
    • 0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
  • 当前类的超类(父类)索引:06表示根据常量池中的#6找到父类全限定名
    • 0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
  • 接口数量:本类为0
    • 0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
Flag Name Value Interpretation
ACC_PUBLIC 0x0001 Declared public; may be accessed from outside its package.
ACC_FINAL 0x0010 Declared final; no subclasses allowed.
ACC_SUPER 0x0020 Treat superclass methods specially when invoked by the invokespecial instruction.
ACC_INTERFACE 0x0200 Is an interface, not a class.
ACC_ABSTRACT 0x0400 Declared abstract; must not be instantiated.
ACC_SYNTHETIC 0x1000 Declared synthetic; not present in the source code.
ACC_ANNOTATION 0x2000 Declared as an annotation type.
ACC_ENUM 0x4000 Declared as an enum type.

Field信息

  • 字段数量(成员变量数量),本类为0
    • 0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

Method信息

  • 方法数量:本类为2,构造方法和main方法

    • 0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
  • 一个方法由访问修饰符、名称、参数描述、方法苏属性数量、方法属性组成

  • 00 01表示访问修饰符(本类中为public)

    • 0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
  • 00 07表示引用了常量池中的#07项作为方法名称

    • 0000700 00 07 00 08 00 01 00 09 00 00 00 2f 00 01 00 01
  • 00 08表示引用了常量池中的#08项作为方法参数描述

    • 0000700 00 07 00 08 00 01 00 09 00 00 00 2f 00 01 00 01
  • 01表示引方法属性数量,本方法是1

    • 0000700 00 07 00 08 00 01 00 09 00 00 00 2f 00 01 00 01
  • 00 09表示引用常量池#09项,发现是code属性

    • 0000700 00 07 00 08 00 01 00 09 00 00 00 2f 00 01 00 01
  • 00 00 00 2f表示此属性的长度是47

    • 0000700 00 07 00 08 00 01 00 09 00 00 00 2f 00 01 00 01
  • 00 01表示操作数栈最大深度

    • 0000700 00 07 00 08 00 01 00 09 00 00 00 2f 00 01 00 01
  • 00 01表示局部变量最大槽(slot)数

    • 0000700 00 07 00 08 00 01 00 09 00 00 00 2f 00 01 00 01
  • 00 00 00 05表示字节码长度,本例为5

    • 0000720 00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a 00
  • 2a b7 00 01 b1 是字节码指令

    • 0000720 00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a 00
  • 00 00 00 02 表示方法细节属性数量,本例为2

    • 0000720 00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a 00
  • 00 0a表示引用了常量池#10项,发现是LineNumberTable属性

    • 0000720 00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a 00
    • 00 00 00 06表示此属性的总长度,本例是6
      • 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
    • 00 01表示LineNumberTable长度
      • 0000740 00 00 06 00 01 00 00 00 04 00 0b 00 00 00 0c 00
    • 00 00 表示字节码行号
      • 0000740 00 00 06 00 01 00 00 00 04 00 0b 00 00 00 0c 00
    • 00 04表示Java源码行号
      • 0000740 00 00 06 00 01 00 00 00 04 00 0b 00 00 00 0c 00
  • 00 0b表示引用了常量池#11项,发现是LocalVariableTable属性

    • 0000740 00 00 06 00 01 00 00 00 04 00 0b 00 00 00 0c 00
    • 00 00 00 0c 表示此属性总长度,本例为12
      • 0000740 00 00 06 00 01 00 00 00 04 00 0b 00 00 00 0c 00
    • 00 01 表示LocalVariableTable长度
      • 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
    • 00 00 表示局部变量生命周期开始,相对于字节码的偏移量
        • 0000760 01 00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e 00
    • 00 05 表示局部变量覆盖的范围长度
      • 0000760 01 00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e 00
    • 00 0c 表示局部变量的名称,引用常量池#12项
      • 0000760 01 00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e 00
    • 00 0d 表示局部变量的类型,本例引用了常量池 #13 项,是Lcn/itcast/jvm/t5/HelloWorld;
      • 0000760 01 00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e 00
    • 00 00 表述局部变量占有的槽位(slot)编号,本例是0
      • 0000760 01 00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e 00
  • 00 09代表访问修饰符(本类中是 public static)

  • 00 0e 代表引用了常量池 #14 项作为方法名称

  • 00 0f 代表引用了常量池 #15 项作为方法参数描述

  • 00 02 代表方法属性数量,本方法是 2

  • 其余代表方法属性(属性1)

    • 00 09 表示引用了常量池 #09 项,发现是Code属性
    • 00 00 00 37 表示此属性的长度是 55
    • 00 02 表示操作数栈最大深度
    • 00 01 表示局部变量表最大槽(slot)数
    • 00 00 00 05 表示字节码长度,本例是 9
    • b2 00 02 12 03 b6 00 04 b1 是字节码指令
    • 00 00 00 02 表示方法细节属性数量,本例是 2
    • 00 0a 表示引用了常量池 #10 项,发现是LineNumberTable属性
      • 00 00 00 0a 表示此属性的总长度,本例是 10
      • 00 02 表示LineNumberTable长度
      • 00 00 表示字节码行号 00 06 表示java 源码行号
      • 00 08 表示字节码行号 00 07 表示java 源码行号
  • 00 0b 表示引用了常量池 #11 项,发现是LocalVariableTable属性

    • 00 00 00 0c 表示此属性的总长度,本例是 12
    • 00 01 表示LocalVariableTable长度
    • 00 00 表示局部变量生命周期开始,相对于字节码的偏移量
    • 00 09 表示局部变量覆盖的范围长度
    • 00 10 表示局部变量名称,本例引用了常量池 #16 项,是args
    • 00 11 表示局部变量的类型,本例引用了常量池 #17 项,是[Ljava/lang/String;
    • 00 00 表示局部变量占有的槽位(slot)编号,本例是 0
  • 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

  • 红色代表方法属性(属性2)

    • 00 12 表示引用了常量池 #18 项,发现是MethodParameters属性
    • 00 00 00 05 表示此属性的总长度,本例是 5
    • 01 参数数量
    • 00 10 表示引用了常量池 #16 项,是args
    • 00 00 访问修饰符
    • 0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00

附加属性

  • 00 01 表示附加属性数量
  • 00 13 表示引用了常量池 #19 项,即SourceFile
  • 00 00 00 02 表示此属性的长度
  • 00 14 表示引用了常量池 #20 项,即HelloWorld.java
    • 0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00
    • 0001120 00 00 02 00 14

字节码指令

入门

  • 在上一小节,有两个字节码指令,我们没有细说,那现在就来具体看看
  • 一个是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 表示返回
  • 详情请参考官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5

javap工具

  • 自己分析类文件结构太麻烦了,Oracle提供了javap工具来反编译class文件
$ javap -v HelloWorld.class
Classfile /D:/Workspace/JVM/demo/target/classes/com/demo/HelloWorld.class
  Last modified 2023-4-5; size 551 bytes
  MD5 checksum 1389d939c65ba536eb81d1a5c61d99be
  Compiled from "HelloWorld.java"              
public class com.demo.HelloWorld               
  minor version: 0                             
  major version: 52                            
  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            // com/demo/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               Lcom/demo/HelloWorld;
  #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               com/demo/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 com.demo.HelloWorld();
    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
    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 5: 0
        line 6: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"

图解方法执行流程

  1. 原始Java代码
/**
 * 演示 字节码指令 和 操作数栈、常量池的关系
 */
public class Demo_20 {
    public static void main(String[] args) {
        int a = 10;
        int b = Short.MAX_VALUE + 1;
        int c = a + b;
        System.out.println(c);
    }
}
  1. 编译后的字节码文件
$ javap -v Demo_20.class
Classfile /D:/Workspace/JVM/demo/target/classes/com/demo/Demo_20.class
  Last modified 2023-4-7; size 601 bytes       
  MD5 checksum 0f9e41fb2a7334a69c89d2661540f4f1
  Compiled from "Demo_20.java"                 
public class com.demo.Demo_20                  
  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            // com/demo/Demo_20
   #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               Lcom/demo/Demo_20;
  #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               Demo_20.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               com/demo/Demo_20
  #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 com.demo.Demo_20();
    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 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/demo/Demo_20;

  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
         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 8: 0
        line 9: 3
        line 10: 6
        line 11: 10
        line 12: 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: "Demo_20.java"
  1. 常量池载入运行时常量池

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

  3. main线程开始运行、分配栈帧内存

    • stack=2, locals=4
      • 操作数栈的深度为2,也就是说,在执行该方法时,最多可以将两个值压入栈中进行操作。
      • 包含四个局部变量
  4. 执行引擎开始执行字节码

    • bipush 10
      • 将一个byte压入操作数栈(其长度会补齐为4个字节),类似的指令还有
      • sipush:将一个short压入操作数栈(其长度会补齐为4个字节)
      • ldc:将一个int压入操作数栈
      • ldc2_w:将一个long压入操作数栈(分两次压入,因为long占8个字节)
    • istore_1
      • 将操作数栈顶数据弹出,存入局部变量表slot 1
    • ldc #3
      • 从常量池加载#3数据到操作数栈
    • istore_2
      • 将操作数栈顶数据弹出,存入局部变量表slot 2
    • iload_1
      • 将局部变量表slot 1的值加载到操作数栈中
    • iload_2
      • 将局部变量表slot 2的值加载到操作数栈中
    • iadd
      • 从操作数栈顶部弹出两个int类型的数值,将这两个数值相加,并将其结果压入操作数栈顶部;
    • istore_3
      • 将操作数栈顶部数据弹出,存入局部变量表slot 3
    • getstatic #4
      • 从常量池加载#4静态字段到操作数栈
    • iload_3
      • 将局部变量表slot 3的值加载到操作数栈中
    • invokevirtual #5
      • 找到常量池#5项
      • 定位到方法区
      • 生成新的栈帧(分配locals、stack等)
      • 传递参数、执行新栈帧中的字节码
      • 执行完毕,弹出栈帧
      • 清除main操作数栈内容
    • return
      • 完成main方法调用,弹出main栈帧
      • 程序结束

分析 i++

  • 目的:从字节码角度分析a++相关题目
  • 原始Java代码
public class Demo_21 {
    public static void main(String[] args) {
        int a = 10;
        int b = a++ + ++a + a--;
        System.out.println(a);
        System.out.println(b);
    }
}
  • 编译后的字节码文件
$ javap -v Demo_21.class
Classfile /D:/Workspace/JVM/demo/target/classes/com/demo/Demo_21.class
  Last modified 2023-4-7; size 576 bytes
  MD5 checksum 5bc962752b10ca4b57350ca9814ec5b0
  Compiled from "Demo_21.java"
public class com.demo.Demo_21
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#22         // java/lang/Object."<init>":()V
   #2 = Fieldref           #23.#24        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref          #25.#26        // java/io/PrintStream.println:(I)V
   #4 = Class              #27            // com/demo/Demo_21
   #5 = Class              #28            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               LocalVariableTable
  #11 = Utf8               this
  #12 = Utf8               Lcom/demo/Demo_21;
  #13 = Utf8               main
  #14 = Utf8               ([Ljava/lang/String;)V
  #15 = Utf8               args
  #16 = Utf8               [Ljava/lang/String;
  #17 = Utf8               a
  #18 = Utf8               I
  #19 = Utf8               b
  #20 = Utf8               SourceFile
  #21 = Utf8               Demo_21.java
  #22 = NameAndType        #6:#7          // "<init>":()V
  #23 = Class              #29            // java/lang/System
  #24 = NameAndType        #30:#31        // out:Ljava/io/PrintStream;
  #25 = Class              #32            // java/io/PrintStream
  #26 = NameAndType        #33:#34        // println:(I)V
  #27 = Utf8               com/demo/Demo_21
  #28 = Utf8               java/lang/Object
  #29 = Utf8               java/lang/System
  #30 = Utf8               out
  #31 = Utf8               Ljava/io/PrintStream;
  #32 = Utf8               java/io/PrintStream
  #33 = Utf8               println
  #34 = Utf8               (I)V
{
  public com.demo.Demo_21();
    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 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/demo/Demo_21;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: 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_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 5: 0
        line 6: 3
        line 7: 18
        line 8: 25
        line 9: 32
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      33     0  args   [Ljava/lang/String;
            3      30     1     a   I
           18      15     2     b   I
}
SourceFile: "Demo_21.java"
  • 提示:iinc指令是直接在局部变量slot上进行运算,下面逐行分析字节码指令
 0: bipush        10        // 将一个byte压入操作数栈,此时就是将10压入操作数栈
 2: istore_1                // 将操作数栈顶部数据弹出,存入局部变量表 slot 1
 3: iload_1                 // 将局部变量表slot 1的值加载到操作数栈中,也就是将10加载到栈中
 4: iinc          1, 1      // 在当前局部变量上进行运算,自增1,此时局部变量 a = 11   至此 a++ 执行完毕
 7: iinc          1, 1      // 在当前局部变量上进行运算,自增1,此时局部变量 a = 12
10: iload_1                 // 将局部变量表slot 1的值加载到操作数栈中,也就是将12加载到栈中
11: iadd                    // 将栈内两个元素相加,10 + 12 = 22,将结果22加载到栈中
12: iload_1                 // 将局部变量表slot 1的值加载到操作数栈中,也就是将12加载到栈中
13: iinc          1, -1     // 在当前局部变量上进行运算,自减1,此时局部变量 a = 11
16: iadd                    // 将栈内两个元素相加,22 + 12 = 34,结果为34
17: istore_2                // 将操作数栈顶部数据弹出,存入局部变量表slot 2
18: getstatic     #2        // 下面就不分析了,就是输出a和b的值
21: iload_1
22: invokevirtual #3                  
25: getstatic     #2                  
28: iload_2
29: invokevirtual #3                  
32: return
  • 那么最终的结果a = 11b = 34
  • 从字节码指令中,我们可以看出,a++++a的区别为
    • a++是先执行iload,再执行iinc
    • ++a是先执行iinc,再执行iload

条件判断指令

指令 助记符 含义
0x99 ifeq 判断是否 == 0
0x9a ifne 判断是否 != 0
0x9b iflt 判断是否 < 0
0x9c ifge 判断是否 >= 0
0x9d ifgt 判断是否 > 0
0x9e ifle 判断是否 <= 0
0x9f if_icmpeq 两个int是否 ==
0xa0 if_icmpne 两个int是否 !=
0xa1 if_icmplt 两个int是否 <
0xa2 if_icmpge 两个int是否 >=
0xa3 if_icmpgt 两个int是否 >
0xa4 if_icmple 两个int是否 <=
0xa5 if_acmpeq 两个引用是否 ==
0xa6 if_acmpne 两个引用是否 !=
0xc6 ifnull 判断是否 == null
0xc7 ifnonnull 判断是否 != null
  • 原始Java代码
public class Demo_22 {
    public static void main(String[] args) {
        int a = 0;
        if (a == 0) {
            a = 10;
        } else {
            a = 20;
        }
    }
}
  • 编译后的字节码文件
 0: iconst_0             // 将整数常量值0(int类型)压入操作数栈中。
 1: istore_1             // 将栈顶数据存入局部变量表 slot 1
 2: iload_1              // 将局部变量表slot 1的值压入操作数栈
 3: ifne           12    // 判断不等于0,成立跳转至12行,不成立则执行下一行
 6: bipush         10    // 将10压入操作数栈
 8: istore_1             // 将栈顶数据存入局部变量表 slot 1
 9: goto           15    // 跳转至第15行
12: bipush        20     // 将20压入操作数栈,对应 a = 20
14: istore_1             // 将栈顶数据存入局部变量表 slot 1
15: return

循环控制指令

  • 原始Java代码
public class Demo_23 {
    public static void main(String[] args) {
        int a = 0;
        while (a < 10) {
            a++;
        }
    }
}
  • 编译后的字节码文件
 0: iconst_0                // 将整数常量值0(int类型)压入操作数栈中。
 1: istore_1                // 将栈顶数据存入局部变量表 slot 1
 2: iload_1                 // 将局部变量表slot 1的值压入操作数栈
 3: bipush        10        // 将10压入操作数栈
 5: if_icmpge     14        // 判断 i >= 10 ,成立则跳转到14行,不成立则执行下一行
 8: iinc          1, 1      // i自增
11: goto          2         // 跳转到第2行
14: return
  • 再比如do while循环
public class Demo_24 {
    public static void main(String[] args) {
        int i = 0;
        do {
            i++;
        } while (i < 10);
    }
}
  • 编译后的字节码文件
 0: iconst_0                // 将整数常量值0(int类型)压入操作数栈中。
 1: istore_1                // 将栈顶数据存入局部变量表 slot 1
 2: iinc          1, 1      // i自增
 5: iload_1                 // 将局部变量表slot 1加载到操作数栈
 6: bipush        10        // 将10加载到操作数栈
 8: if_icmplt     2         // 判断 i < 10,成立则跳转到第2行,不成立执行下一行
11: return
  • 最后再来看看for循环
public class Demo_25 {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {

        }
    }
}
  • 编译后的字节码文件
 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

{% note info no-icon %}
注意到while和for的字节码,它们是一模一样的,这就是所谓的殊途同归
{% endnote %}

判断结果

  • 从字节码的角度来分析下面程序的运行结果
public class Demo_26 {
    public static void main(String[] args) {
        int i = 0;
        int x = 0;
        while (i < 10) {
            x = x++;
            i++;
        }
        System.out.println(x);
    }
}
  • 最终x的结果是0
    • 执行x++时,先执行iload_x,将0加载到操作数栈中
    • 然后执行iinc,将局部变量表中的x自增,此时局部变量表中的x = 1
    • 此时又执行了一个赋值操作,istore_x,将操作数栈中的0,重新赋给了局部变量表中的x,导致x为0
  • 下面是对应的字节码
10: iload_2
11: iinc          2, 1
14: istore_2

构造方法

  1. <cinit>()V
    public class Demo_27 {
        static int i = 10;
    
        static {
            i = 20;
        }
    
        static {
            i = 30;
        }
    
        public static void main(String[] args) {
    
        }
    }
    
    • 编译后的字节码文件
     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
    
    • 编译器会按照从上至下的顺序,收集所有的static静态代码块和静态成员赋值的代码,合并成一个特殊的方法<cinit>()V
    • <cinit>()V方法会在类加载的初始化阶段被调用
  2. <init>()V
    public class Demo_28 {
        private String a = "s1";
    
        {
            b = 20;
        }
    
        private int b = 10;
    
        {
            a = "s2";
        }
    
        public Demo_28(String a, int b) {
            this.a = a;
            this.b = b;
        }
    
        public static void main(String[] args) {
            Demo_28 demo = new Demo_28("s3", 30);
            System.out.println(demo.a);
            System.out.println(demo.b);
        }
    }
    
    • 编译后的字节码文件
     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
    
    • 编译器会按照从上至下的顺序,收集所有代码块和所有成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是会在最后

方法调用

  • 看一下几种不同方法调用对应的字节码指令,私有方法,final方法,公共方法,静态方法
    • 其中静态方法包括对象调静态方法和类直接调静态方法
    public class Demo_29 {
        public Demo_29(){}
        private void test1(){}
        private final void test2(){}
        public void test3(){}
        public static void test4(){}
    
        public static void main(String[] args) {
            Demo_29 demo = new Demo_29();
            demo.test1();
            demo.test2();
            demo.test3();
            demo.test4();
            Demo_29.test4();
        }
    }
    
    • 编译后的字节码文件
     0: new           #2                  // class com/demo/Demo_29
     3: dup
     4: invokespecial #3                  // Method "<init>":()V
     7: astore_1
     8: aload_1
     9: invokespecial #4                  // Method test1:()V
    12: aload_1
    13: invokespecial #5                  // Method test2:()V
    16: aload_1
    17: invokevirtual #6                  // Method test3:()V
    20: aload_1
    21: pop
    22: invokestatic  #7                  // Method test4:()V
    25: invokestatic  #7                  // Method test4:()V
    28: return
    
    • new #2是创建Demo_29对象,给对象分配内存,执行成功会将对象引用压入操作数栈
    • dup是赋值操作数栈顶的内容,本例为对象引用。那为什么需要两份引用呢?
      • 一个是要配合invokespecial调用该对象的构造方法"<init>:()V",会消耗掉栈顶一个引用
      • 另一个要配合astore_1赋值给局部变量
    • final方法、私有方法、构造方法,都是由invokespecial指令来调用,属于静态绑定
    • 普通成员方法是由invokevirtual调用,属于动态绑定,即支持多态
    • 成员方法与静态方法调用的另一个区别是,执行方法前是否需要对象引用
    • 比较有意思的是,执行demo.test4()时,是通过对象引用调用的静态方法,可以看到在调用前执行了pop指令,把对象引用从操作数栈弹掉了,因为静态方法不需要对象引用来掉,通过这种方式,反而会增加两步无用的字节码指令
    20: aload_1
    21: pop
    22: invokestatic  #7                  // Method test4:()V
    

多态的原理

  • 原始Java代码
    • 定义了一个抽象类Animal,还有其两个子类Cat和Dog
import java.io.IOException;

/**
 * 添加VM参数:-XX:-UseCompressedOops -XX:-UseCompressedClassPointers
 */
public class Demo_30 {
    public static void test(Animal animal) {
        animal.eat();
        System.out.println(animal);
    }

    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("想吃小鱼干");
    }
}
  1. 运行代码
    • 会停在System.in.read()方法上(当然你也可以直接打断点),运行jps命令获取进程id
  2. 运行HSDB工具
    • 进入JDK安装目录,执行
    java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB
    
    • 进入图形界面attach进程id
  3. 查找某个对象
    • 打开Tools -> Find Object By Query,输入命令,点击Execute执行
    select d from com.demo.Dog d
    


4. 查看对象内存结构
- 点击超链接可以看到对象的内存结构,此对象没有任何属性,因此只有对象头的16字节,前8字节是MarkWord,后8字节就是对象的Class指针,但现在看不到它的实际地址
5. 查看对象Class的内存地址
- 可以通过Windows -> Console进入命令行模式,执行如下命令
BASH mem 0x000001f1676e77c8 2
- mem指令有两个参数,参数1是对象地址,参数2是查看2行(即16字节)
- 结果中第二行0x000001f1f48841a0即为Class的内存地址

6. 查看类的vtable
- 方法1:{% kbd ALT %} + {% kbd R %}进入Inspector工具输入刚才的Class内存地址
- 方法2:或者Tools -> Class Browser 输入Dog查找,可以得到相同的结果

- 无论通过哪种方法,都可以找到Dog Class的vtable长度为6,意思就是Dog类会有6个虚方法(多台相关的,final、static不会列入)
- 那么这6个虚方法都是谁呢?从Class的起始地址开始算,偏移0x1b8就是vtable的其实地址,进行计算得到
0x000001f1f48841a0 1b8 + -------------------- 0x000001f1f4884358
- 通过Windows -> Console进入命令行模式,执行如下命令,就得到了6个虚方法的入口地址
BASH mem 0x000001f1f4884358 6 0x000001f1f4884358: 0x000001f1f4481b10 0x000001f1f4884360: 0x000001f1f44815e8 0x000001f1f4884368: 0x000001f1f4883750 0x000001f1f4884370: 0x000001f1f4481540 0x000001f1f4884378: 0x000001f1f4481678 0x000001f1f4884380: 0x000001f1f4884148
7. 验证方法地址
- 通过Tools -> Class Browser 查看每个类的方法定义,比较可知
0x000001f1f4481b10 -> Object -- protected void finalize() @0x000001f1f4481b10; 0x000001f1f44815e8 -> Object -- public boolean equals(java.lang.Object) @0x000001f1f44815e8; 0x000001f1f4883750 -> Animal -- public java.lang.String toString() @0x000001f1f4883750; 0x000001f1f4481540 -> Object -- public native int hashCode() @0x000001f1f4481540; 0x000001f1f4481678 -> Object -- protected native java.lang.Object clone() @0x000001f1f4481678; 0x000001f1f4884148 -> Dog -- public void eat() @0x000001f1f4884148;
- 对号入座,发现
- eat()方法是Dog类自己的
- toString()方法是继承String类的
- finalize() ,equals(),hashCode(),clone() 都是继承 Object 类的
8. 小结
- 当执行invokevirtual指令时
1. 先通过栈帧中的对象引用找到对象
2. 分析对象头,找到对象的实际Class
3. Class结构中有vtable,它在类加载的链接阶段就已经根据方法的重写规则生成号了
4. 查表得到方法的具体地址
5. 执行方法的字节码

异常处理

  1. try-catch
    • 原始Java代码
    public class Demo_31 {
        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
        9: bipush        20
        11: istore_1
        12: return
        Exception table:
        from    to  target type
            2     5     8   Class java/lang/Exception
    
    • 可以看到多出来一个Exception table的结构,[from to)是左闭右开的检测范围,一旦这个范围内的字节码执行出现异常,则通过type匹配异常类型,如果一致,进入target所指示的行号,该例中是第8行,也就是执行catch代码块
    • 第8行的字节码指令astore_2是将异常对象存入局部变量表的slot 2的位置
  2. 多个catch块的情况
    • 原始Java代码
    public class Demo_32 {
        public static void main(String[] args) {
            int i = 0;
            try {
                i = 10;
            } catch (ArithmeticException e) {
                i = 20;
            } catch (NullPointerException e) {
                i = 30;
            } catch (Exception e) {
                i = 40;
            }
        }
    }
    
    • 编译后的字节码文件
    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        20
          11: istore_1
          12: goto          26
          15: astore_2
          16: bipush        30
          18: istore_1
          19: goto          26
          22: astore_2
          23: bipush        40
          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
              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
    
    • 因为异常出现时,只能进入一个Exception table的分支,所以局部变量表slot 2位置被共用
    • [from, to)的检测范围都相同,只不过target的行号不同,对应三个catch块
  3. multi-catch的情况
    • 原始Java代码
    public class Demo_33 {
        public static void main(String[] args) {
            int i = 0;
            try {
                i = 10;
            } catch (NoSuchMethodError | IllegalAccessError | Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    • 编译后的字节码文件
    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          13
            8: astore_2
            9: aload_2
            10: invokevirtual #5                  // Method java/lang/Throwable.printStackTrace:()V
            13: return
        Exception table:
            from    to  target type
                2     5     8   Class java/lang/NoSuchMethodError
                2     5     8   Class java/lang/IllegalAccessError
                2     5     8   Class java/lang/Exception
        LineNumberTable:
            line 5: 0
            line 7: 2
            line 10: 5
            line 11: 13
        LocalVariableTable:
            Start  Length  Slot  Name   Signature
                9       4     2     e   Ljava/lang/Throwable;
                0      14     0  args   [Ljava/lang/String;
                2      12     1     i   I
    
    
    • [from, to)的检测范围都相同,target的行号也相同
  4. finally
    • 原始Java代码
    public class Demo_34 {
        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
                0: iconst_0
                1: istore_1             // 0 -> i
                2: bipush       10      // try ---------------------------
                4: istore_1             // 10 -> i                       |
                5: bipush       30      //                               |
                7: istore_1             // 30 -> i                       |
                8: goto         27      // return ------------------------
                11: astore_2            // catch Exception -> e ----------
                12: bipush 20           //                               |
                14: istore_1            // 20 -> i                       |
                15: bipush      30      //                               |
                17: istore_1            // 30 -> i                       |
                18: goto        27      // return ------------------------
                21: astore_3            // catch any -> slot 3 -----------
                22: bipush      30      //                               |
                24: istore_1            // 30 -> i                       |
                25: aload_3             // <- slot 3                     |
                26: athrow              // throw -------------------------
                27: return
            Exception table:
                from to target type
                2 5 11 java/lang/Exception
                2 5 21 any
                11 15 21 any
            LineNumberTable: ...
            LocalVariableTable:
                Start Length Slot Name Signature
                0 28 0 args [Ljava/lang/String;
                2 26 1 i I
                12 3 2 e Ljava/lang/Exception;
    
    • 可以看到有3个[from, to)
      • 第一个[2, 5)是检测try块中是否有Exception异常,如果有则跳转至11行执行catch块
      • 第二个[2, 5)是检测try块中是否有其他异常(非Exception异常),如果有则跳转至21行执行finally块
      • 第三个[11, 15)是检测catch快中是否有其他异常,如果有则跳转至21行执行finally块
    • 结论:finally中的代码被复制了三分,分别放进try流程、catch流程以及catch剩余的异常类型流程

关于finally的面试题

  1. finally中出现了return
    • 原始Java代码,先自己试着想一下最终的结果是啥:{% psw 20 %}
    public class Demo_35 {
        public static void main(String[] args) {
            int result = test();
            System.out.println(result);
        }
    
        private static int test() {
            try {
                return 10;
            } finally {
                return 20;
            }
        }
    }
    
    • 编译后的字节码文件
      {% note warning no-icon %}
    • 注意这里要加上-p参数,才能显示私有方法的信息
    javap -v -p Demo_35.class
    
    {% endnote %}
    private static int test();
      descriptor: ()I
      flags: ACC_PRIVATE, ACC_STATIC
      Code:
        stack=1, locals=2, args_size=0
           0: bipush        10      // 将 int 10 压入栈顶
           2: istore_0              // 将栈顶的 int 10 存入到局部变量 slot 0 中,并从栈顶弹出
           3: bipush        20      // 将 int 20 压入栈顶
           5: ireturn               // 返回栈顶的 int 20
           6: astore_1              // 捕获任何异常
           7: bipush        20      // 将 int 20 压入栈顶
           9: ireturn
        Exception table:
           from    to  target type
               0     3     6   any
        LineNumberTable:
          line 11: 0
          line 13: 3
        StackMapTable: number_of_entries = 1
          frame_type = 70 /* same_locals_1_stack_item */
            stack = [ class java/lang/Throwable ]
    
    • 由于finally中的ireturn被插入了所有可能的流程,因此返回结果肯定以finally为准
    • 至于字节码中的第二行,目前看似没啥用,先留个伏笔,等下个例子来讲解
    • 之前的finally例子中,最后都会有一个athrow,这告诉我们,如果在finally中出现了return,那么就会吞掉异常,具体来看下面这个例子
    public class Demo_36 {
        public static void main(String[] args) {
            int result = test();
            System.out.println(result);
        }
    
        private static int test() {
            try {
                int i = 1 / 0;
                return 10;
            } finally {
                return 20;
            }
        }
    }
    
    • 运行上面的代码,不会出现任何异常,输出20,i = 1 / 0那个异常被吞掉了
  2. finally对返回值的影响
    • 原始Java代码,还是先试着想想结果会输出什么:{% psw 10 %}
    public class Demo_37 {
        public static void main(String[] args) {
            int result = test();
            System.out.println(result);
        }
    
        private static int test() {
            int i = 10;
            try {
                return i;
            } finally {
                i = 20;
            }
        }
    }
    
    • 编译后的字节码文件
    private static int test();
      descriptor: ()I
      flags: ACC_PRIVATE, ACC_STATIC
      Code:
        stack=1, locals=3, args_size=0
           0: bipush        10      // 将 10 放入栈顶
           2: istore_0              // 10 -> i
           3: iload_0               // <- i(10)
           4: istore_1              // 将 i(10) 暂存至 slot 1,目的是为了固定返回值
           5: bipush        20      // 将 20 放入栈顶
           7: istore_0              // 20 -> i
           8: iload_1               // 载入 slot 1 暂存的值 (10)
           9: ireturn               // 返回栈顶的值
          10: astore_2
          11: bipush        20
          13: istore_0
          14: aload_2
          15: athrow
        Exception table:
           from    to  target type
               3     5    10   any
        LineNumberTable:
          line 10: 0
          line 12: 3
          line 14: 5
          line 12: 8
          line 14: 10
          line 15: 14
        LocalVariableTable:
          Start  Length  Slot  Name   Signature
              3      13     0     i   I
        StackMapTable: number_of_entries = 1
          frame_type = 255 /* full_frame */
            offset_delta = 10
            locals = [ int ]
            stack = [ class java/lang/Throwable ]
    
    • 虽然在 finally 块中将 i 的值修改为 20,但是这不会影响 return 语句的返回值,因为在返回之前,i 的值已经被暂存到了 slot 1 中。在 finally 块中对 i 进行的修改不会影响 slot 1 中的值,因此 ireturn 指令返回的是 slot 1 中的值,即 10。

synchronized

  • synchronized代码块是对一个对象进行加锁操作,那么它是如何保障当synchronized代码块中出现了异常,还能正确的执行解锁操作呢?下面就从字节码的角度来分析一下底层原理
    • 原始Java代码
    public class Demo_38 {
        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
           0: new           #2                  // class java/lang/Object
           3: dup
           4: invokespecial #1                  // Method java/lang/Object."<init>":()V
           7: astore_1                          // lock引用 -> lock
           8: aload_1                           // <- lock (synchronized开始)
           9: dup
          10: astore_2                          // lock引用 -> slot 2
          11: monitorenter                      // monitorenter(lock引用)
          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
          20: aload_2                           // <- slot 2(lock引用)
          21: monitorexit                       // monitorexit(lock引用)
          22: goto          30
          25: astore_3                          // any -> slot 3
          26: aload_2                           // <- slot 2(lock引用)
          27: monitorexit                       // monitorexit(lock引用)
          28: aload_3
          29: athrow
          30: return
        Exception table:
           from    to  target type
              12    22    25   any
              25    28    25   any
        LineNumberTable:
          line 5: 0
          line 6: 8
          line 7: 12
          line 8: 20
          line 9: 30
        LocalVariableTable:
          Start  Length  Slot  Name   Signature
              0      31     0  args   [Ljava/lang/String;
              8      23     1  lock   Ljava/lang/Object;
    
    • [12, 22)是监测的释放锁的流程,如果出现了异常,则跳转到25行,将异常信息存储到slot 3,同时再次尝试释放锁
    • [25, 28)也是监测异常,如果有异常,

编译期处理

  • 所谓语法糖,其实就是指Java编译器把*.java编译为*.class字节码的过程中,自动生成的和转换的一些代码,主要是为了减轻程序员的负担,算是Java编译器给我们的一个额外福利
  • 下面的代码分析,借助了javap工具、idea的反编译功能、idea插件jclasslib等工具。另外,编译器转换的结果直接就是class字节码,只是为了便于阅读,给出了几乎等价的Java源码

默认构造器

  • 如果一个类没有声明任何构造函数,Java 编译器会自动为该类生成一个无参构造函数。
public class Candy01 {

}
  • 编译成class后的代码
public class Candy1 {

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

自动拆装箱

  • 这个特性是 JDK 5 开始加入的,代码片段1:
public class Candy02 {
    public static void main(String[] args) {
        Integer x = 1;
        int y = x;
    }
}
  • 但是这段代码在JDK 5之前是无法编译通过的,比如改写为如下形式,代码片段2
public class Candy02 {
    public static void main(String[] args) {
        Integer x = Integer.valueOf(1);
        int y = x.intValue();
    }
}   
  • 显然之前版本的代码太麻烦了,需要在基本类型和包装类型之间相互转换(尤其是集合类中的操作都是包装类型),因此这些转换的事情在JDK 5以后都由编译器在编译阶段完成。即代码片段1都会在编译阶段转换成代码片段2

泛型集合取值

  • 泛型也是JDK 5开始加入的特性,但Java在编译泛型后会执行泛型擦除的动作,即泛型信息在编译为字节码后就丢失了,实际的类型都当做Object类型来处理
public class Candy03 {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(10);               // 实际调用的是 List.add(Object e)
        Integer x = list.get(0);    // 实际调用的是 Object obj = List.get(int index);
    }
}
  • 所以在取值时,编译器真正生成的字节码中,还需要额外做一个类型转换的操作
// 需要将 Object 转为 Integer
Integer x = (Integer)list.get(0);
  • 如果前面的x遍历修改为int基本类型,那么最终生成的字节码为
int x = (Integer)list.get(0).intValue();
  • 还好这些麻烦事都不用自己做,要么叫语法糖呢
  • 擦除的是字节码上的泛型信息,可以看到LocalVariableTypeTable仍然保留了方法参数泛型的信息
    • 从下面字节码的第26行,我们可以清楚的看到add方法其实添加的是Object类型对象
    • 从下面字节码的第30行,我们可以清楚的看到get方法的返回值也是Object类型对象
    • 同时第31行是将类型强制转换为Integer
public com.demo.Candy03();
  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 6: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       5     0  this   Lcom/demo/Candy03;
public static void main(java.lang.String[]);
  descriptor: ([Ljava/lang/String;)V
  flags: ACC_PUBLIC, ACC_STATIC
  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;
      14: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      19: pop
      20: aload_1
      21: iconst_0
      22: invokeinterface #6,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
      27: checkcast     #7                  // class java/lang/Integer
      30: astore_2
      31: return
    LineNumberTable:
      line 8: 0
      line 9: 8
      line 10: 20
      line 11: 31
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0      32     0  args   [Ljava/lang/String;
          8      24     1  list   Ljava/util/List;
         31       1     2     x   Ljava/lang/Integer;
    LocalVariableTypeTable:
      Start  Length  Slot  Name   Signature
          8      24     1  list   Ljava/util/List<Ljava/lang/Integer;>;
  • 使用反射,能够获取到方法类型参数的泛型和方法返回值泛型的信息
public class Candy03 {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Method test = Candy03.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]);
                }
            }
        }
        System.out.println("返回类型 - " + test.getGenericReturnType());
    }

    public Set<Integer> test(List<String> list, Map<Integer, Object> map) {
        return null;
    }
}
  • 输出
原始类型 - interface java.util.List
泛型参数[0] - class java.lang.String
原始类型 - interface java.util.Map
泛型参数[0] - class java.lang.Integer
泛型参数[1] - class java.lang.Object
返回类型 - java.util.Set<java.lang.Integer>

可变参数

  • 可变参数也是JDK 5开始加入的新特性,示例代码如下
public class Candy04 {

    public static void main(String[] args) {
        foo("hello", "world");
    }

    private static void foo(String... args) {
        String[] array = args;
        System.out.println(array);
    }

}

  • 可变参数String… args 其实是一个String[] args,同样Java编译器会在编译期间将上述代码转换为
public class Candy04 {
    
    public static void main(String[] args) {
        foo(new String[]{"hello", "world"});
    }

    public static void foo(String[] args) {
        String[] array = args;          // 直接赋值
        System.out.println(array);
    }

}

{% note warning no-icon %}

  • 注意:如果调用foo()时没有提供任何参数,那么则等价为foo(new String),创建了一个空的数组,而不是传一个null进去
    {% endnote %}

foreach循环

  • 仍然是JDK 5开始引入的语法糖,数组的循环
public class Candy05 {
    public static void main(String[] args) {
        int[] array = {1, 2, 3, 4, 5};      // 数组的赋初值的简化,也是语法糖 new int[]{1, 2, 3, 4, 5}
        for (int a : array) {
            System.out.println(a);
        }
    }
}
  • 会被编译器转换为
public class Candy05 {
    public Candy05() {
    }
    public static void main(String[] args) {
        int[] array = new int[]{1, 2, 3, 4, 5};
        for(int i = 0; i < array.length; ++i) {
            int e = array[i];
            System.out.println(e);
    }
}
  • 而集合的循环
public class Candy06 {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        for (Integer integer : list) {
            System.out.println(integer);
        }
    }
}
  • 实际上会被编译器转换为对迭代器的调用
public class Candy06 {
    public Candy06() {

    }
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        Iterator iterator = list.iterator();
        while (iterator.hasNext()){
            Integer next = (Integer) iterator.next();
            System.out.println(next);
        }
    }
}

{% note warning no-icon %}

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

switch字符串

  • JDK 7开始,switch可以作用于字符串和枚举类,这个功能其实也是语法糖,例如
public class Candy07 {
    public static void choose(String str) {
        switch (str) {
            case "hello": {
                System.out.println("h");
                break;
            }
            case "world": {
                System.out.println("w");
                break;
            }
        }
    }
}

{% note warning no-icon %}

  • 注意:swtich配合Spring和枚举使用时,变量不能为null,原因分析完语法糖转换后的代码,自然就清楚了
    {% endnote %}
  • 会被编译器转换为
public class Candy07 {
    public Candy07() {
        
    }

    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,第一遍是根据字符串的hashCode和queals将字符串转换为相应byte类型,第二遍才是利用byte进行比较
  • 那为什么第一遍既要比较hashCode又利用equals比较呢?
    • hashCode是为了提高效率,减少可能的比较
    • 而equals是为了防止哈希冲突,例如BMC.这两个字符串的hashCode值都是2123,例如下面的代码
    public static void choose(String str) {
        switch (str) {
            case "BM": {
                System.out.println("h");
                break;
            }
            case "C.": {
                System.out.println("w");
                break;
            }
        }
    }
    
    • 会被编译器转换为
    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");
                }
        }
    }
    

switch枚举

  • switch枚举的例子,原始代码
enum Sex {
    MALE, FEMALE
}
public  static void foo(Sex sex){
    switch (sex){
        case MALE:
            System.out.println("男");
            break;
        case FEMALE:
            System.out.println("女");
            break;
    }
}
  • 转换后的代码
public class Candy08 {
    /**
     * 定义一个合成类(仅 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;
        }
    }
}

枚举类

  • JDK 7 新增了枚举类,以前面的性别枚举为例
enum Sex {
    MALE, FEMALE
}
  • 转换后的代码
    public final class Sex extends Enum<Sex> {
        public static final Sex MALE;
        public static final Sex FEMALE;
        private static final Sex[] $VALUES;
        static {
            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);
        }
    }
  • Sex被声明为一个final类,它继承了Enum<Sex>类,Enum是Java中定义枚举的抽象类。MALE和FEMALE是Sex类的两个枚举值,它们被定义为静态常量。
  • 除此之外,还有一个私有的、finalSex类型数组$VALUES,它用于存储Sex类的所有枚举值。在类的静态块中,$VALUES数组被初始化为一个包含MALEFEMALE的数组。
  • 构造函数Sex(String name, int ordinal)是私有的,这意味着无法在类的外部使用这个构造函数来创建Sex的实例。只有Java编译器生成的代码才能调用这个构造函数来创建Sex的实例。
  • values()valueOf(String name)是从Enum类继承的两个静态方法。values()方法返回一个包含Sex类所有枚举值的数组,valueOf(String name)方法返回指定名称的枚举值。
  • 当我们使用MALE或者FEMALE时,其实底层调用的是Enum.valueOf(Sex.class, "MALE")Enum.valueOf(Sex.class, "FEMALE")

try-with-resources

  • JDK 7 开始新增了对需要关闭的自愿处理的特殊语法 try-with-resources
try (资源变量 = 创建资源对象) {

} catch() {

}
  • 其中资源对象需要实现AutoCloseable接口,例如InputStream、OutputStream、Connection、Statement、ResultSet等接口都实现了AuthCloseable接口,使用try-with-resources 可以不用写finally语句块,编译器会帮助我们生成关闭资源代码,例如
public class Candy09 {
    public static void main(String[] args) {
        try (InputStream is = new FileInputStream("d:\\tmp.test")) {
            System.out.println(is);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • 会被编译器转换为
public class Candy09 {
    public Candy09() {
    }

    public static void main(String[] args) {
        try {
            InputStream is = new FileInputStream("d:\\tmp.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)(添加被压制异常)的方法呢?
    • 这是为了防止异常信息的丢失
    public class Test {
        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 com.demo.Test.main(Test.java:6)
        Suppressed: java.lang.Exception: close 异常
            at com.demo.MyResource.close(Test.java:14)
            at com.demo.Test.main(Test.java:7)
    

方法重写时的桥接方法

  • 方法重写时,对返回值分两种情况
    1. 父类与子类的返回值完全一致
    2. 子类返回值可以是父类返回值的子类(比较绕口,直接看下面的例子来理解)
      class A {
          public Number m() {
              return 1;
          }
      }
      class B extends A {
          @Override
          // 父类A方法的返回值是Number类型,子类B方法的返回值是Integer类型,Integer是Number的子类
          public Integer m() {
              return 2;
          }
      }
      
      • 那么对于子类,编译器会做如下处理
      class B extends A {
          public Integer m() {
              return 2;
          }
      
          // 此方法才是真正重写了父类 public Number m() 方法
          public synthetic bridge Number m() {
              // 调用 public Integer m()
              return m();
          }
      }
      
      • 其中的桥接方法比较特殊,仅对Java虚拟机课件,并且与原来的public Integer m()没有命名冲突

匿名内部类

  • 原始Java代码
public class Candy10 {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("ok");
            }
        };
    }
}
  • 转换后代码
// 额外生成的类
final class Candy10$1 implements Runnable {
    Candy10$1() {
    }
    public void run() {
        System.out.println("ok");
    }
}

public class Candy10 {
    public static void main(String[] args) {
        Runnable runnable = new Candy10$1();
    }
}
  • 对于匿名内部类,它的底层实现是类似于普通内部类的,只不过没有命名而已。在生成匿名内部类的class文件时,Java编译器会自动为该类生成一个类名,在原始类名上加后缀$1,如果有多个匿名内部类,则$2$3以此类推
  • 引用局部变量的匿名内部类,原始Java代码
public class Candy11 {
    public static void test(final  int x){
        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);
    }
}

{% note warning no-icon %}

  • 注意:这也解释了为什么匿名内部类引用局部变量时,局部变量必须为final的
    • 因为在创建Candy 11 对象时,将 x 的值赋给了 v a l 11对象时,将x的值赋给了val 11对象时,将x的值赋给了valx属性,所以x不应该再发生变化了
    • 如果变化,那么 v a l val valx属性没有机会再跟着一起变化
      {% endnote%}

类加载阶段

加载

  • 将类的字节码载入方法区中,内部采用C++的instanceKlass描述Java类,它的重要field有
    1. _java_mirror:Java的类镜像,例如对String来说,就是String.class,作用是把klass暴露给Java使用
    2. _super:父类
    3. _fields:成员变量
    4. _methods:方法
    5. _constants:常量池
    6. _class_loader:类加载器
    7. _vtable:需方发表
    8. _itable:接口方法表
  • 如果这个类还有父类没有加载,先加载父类
  • 加载和链接可能是交替运行的
    {% note warning no-icon %}
  • instanceKlass这样的元数据是存储在方法区(1.8后是在元空间内),但_java_mirror是存储在堆中
  • 可以通过HSDB工具查看
    {% endnote %}

链接

  1. 验证
    • 验证类是否符合JVM规范,安全性检查
    • 使用支持二进制的编辑器修改HelloWorld.class的魔数ca fe ba be,在控制台运行后悔报错
    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)
    
  2. 准备
    • 为static变量分配空间,设置默认值
      • static变量在JDK 7之前存储于instanceKlass末尾,从JDK 7开始,存储于_java_mirror末尾
      • static变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
      • 如果static遍历是final的基本类型以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
      • 如果static遍历是final的,但属于引用类型,那么赋值也会在初始化阶段完成
    • 原始Java代码
    public class Load01 {
        static int a;
        static int b = 10;
        static final int c = 20;
        static final String d = "Hello";
        static final Object e = new Object();
    }
    
    • 编译后的字节码文件
    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 com.demo.Load01();
      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 5: 0
        LocalVariableTable:
          Start  Length  Slot  Name   Signature
              0       5     0  this   Lcom/demo/Load01;
    
    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 7: 0
          line 10: 5
    
    • 变量a和b都是静态变量,但是只有变量b被赋予了初始值10,赋值操作在初始化阶段体现,也就是在 static 块中实现
    • 变量c和d都被声明为静态final变量,它们的值在编译时就已经确定了,分别是20和"Hello",并且在字节码中使用了 ConstantValue 指令来指定这些常量的值。
    • 变量e也是静态final变量,但它是一个引用类型变量,因此在初始化阶段才会被赋值,也就是在 static 块中实现。
  3. 解析
    • 将常量池中的符号引用解析为直接引用
    public class Load02 {
        public static void main(String[] args) throws ClassNotFoundException, IOException {
            ClassLoader classloader = Load02.class.getClassLoader();
            // loadClass 方法不会导致类的解析和初始化
            Class<?> c = classloader.loadClass("com.demo.C");
            // new C();
            System.in.read();
        }
    }
    
    class C {
        D d = new D();
    }
    
    class D {
    }
    
    • 默认情况下,类的加载都是懒惰式的,如果用到了类C,没有用到类D的话,那么类D是不会主动加载的
    • 使用loadClass方法不会导致类的解析和初始化
      • 可以看到类D现在是UnresolvedClass,也就是未经解析的类,在常量池中仅仅是一个符号
    • 使用new C()的方式会导致类的解析和初始化
      • 可以看到此时类D已经加载成功了,同时在类C的常量池中也可以解析类D的地址

初始化

  • 初始化即调用<cinit>()V 方法,虚拟机ui保证这个类的构造方法的线程安全
  • 发生的时机:总的来说,类的初始化是懒惰的
    1. main方法所在的类,总会被首先初始化
      public class Load03 {
          static {
              System.out.println("main init");
          }
      
          public static void main(String[] args) throws ClassNotFoundException {
      
          }
      }
      
      • 控制台会输出
      main init
      
    2. 首次访问这个类的静态变量或静态方法时,会进行初始化
      public class Load03 {
          static {
              System.out.println("main init");
          }
      
          public static void main(String[] args) throws ClassNotFoundException {
              System.out.println(A.a);
          }
      }
      class A {
          static int a = 0;
      
          static {
              System.out.println("a init");
          }
      }
      
      • 控制台输出
      main init
      a init
      0
      
    3. 子类初始化,如果父类还没未初始化,则父类也会进行初始化
      public class Load03 {
          static {
              System.out.println("main init");
          }
      
          public static void main(String[] args) throws ClassNotFoundException {
              System.out.println(B.c);
          }
      }
      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");
          }
      }
      
      • 控制台输出
      main init
      a init
      b init
      false
      
    4. 默认的Class.forName会导致初始化
      public class Load03 {
          static {
              System.out.println("main init");
          }
      
          public static void main(String[] args) throws ClassNotFoundException {
              Class.forName("com.demo.A");
          }
      }
      class A {
          static int a = 0;
      
          static {
              System.out.println("a init");
          }
      }
      
      • 控制台输出
      main init
      a init
      
    5. new对象会导致初始化
      public class Load03 {
          static {
              System.out.println("main init");
          }
      
          public static void main(String[] args) throws ClassNotFoundException {
              new A();
          }
      }
      class A {
          static int a = 0;
      
          static {
              System.out.println("a init");
          }
      }
      
      • 控制台输出
      main init
      a init
      
  • 不会导致类初始化的情况
    1. 访问类的 static final 静态常量(基本类型和字符串) 不会触发初始化
      public class Load03 {
          static {
              System.out.println("main init");
          }
      
          public static void main(String[] args) throws ClassNotFoundException {
              System.out.println(B.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");
          }
      }
      
      • 控制台输出
      main init
      5.0
      
    2. 调用类对象.class不会触发初始化
      public class Load03 {
          static {
              System.out.println("main init");
          }
      
          public static void main(String[] args) throws ClassNotFoundException {
              System.out.println(B.class);
          }
      }
      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");
          }
      }
      
      • 控制台输出
      main init
      class com.demo.B
      
    3. 类加载器的loadClass方法不会触发初始化
      public class Load03 {
          static {
              System.out.println("main init");
          }
      
          public static void main(String[] args) throws ClassNotFoundException {
              ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
              classLoader.loadClass("com.demo.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");
          }
      }
      
      • 控制台输出
      main init
      
    4. Class.forName的参数2为false时(initalize = false),不会触发初始化
      public class Load03 {
          static {
              System.out.println("main init");
          }
      
          public static void main(String[] args) throws ClassNotFoundException {
              ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
              Class.forName("com.demo.A", false, classLoader);
          }
      }
      class A {
          static int a = 0;
      
          static {
              System.out.println("a init");
          }
      }
      
      • 控制台输出
      main init
      

练习

  • 从字节码分析,使用a、b、c这三个常量,是否会导致E初始化
    public class Load04 {
        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;
        static {
            System.out.println("init E");
        }
    }
    
    • 结论:a和b不会导致E的初始化,c会导致E的初始化
    • a和b是基本类型和字符串常量,而c是包装类型,其底层还需要调用Integer.valueOf()方法来装箱,只能推迟到初始化阶段运行,字节码如下
    {
      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
    
      com.demo.E();
        descriptor: ()V
        flags:
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 10: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       5     0  this   Lcom/demo/E;
    
      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
          LineNumberTable:
            line 13: 0
            line 15: 8
            line 16: 16
    }
    
  • 典型应用 -> 完成懒惰初始化的单例模式
public final class Singleton {
    private Singleton() {
    }

    // 内部类中保存单例
    private static class LazyHolder {
        static final Singleton INSTANCE = new Singleton();
    }

    // 第一次调用 getInstance 方法,才会导致内部类加载和初始化其静态成员
    public static Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }
}
  • 以上的实现特点是:
    1. 懒惰实例化
    2. 初始化时的线程安全是有保障的

类加载器

  • JDK 8为例
名称 加载哪的类 说明
Bootstrap ClassLoader JAVA_HOME/jre/lib 无法直接访问
Extension ClassLoader JAVA_HOME/jre/lib/ext 上级为 Bootstrap,显示为 null
Application ClassLoader classpath 上级为 Extension
自定义类加载器 自定义 上级为 Application
  • 当JVM需要加载一个类时,它会首先委托父类加载器去加载这个类,如果父类加载器无法加载这个类,就会由当前类加载器来加载。如果所有的父类加载器都无法加载这个类,那么就会抛出ClassNotFoundException异常。

引导类加载器

  • Bootstrap ClassLoader是所有类加载器中最早的一个,负责加载JRE/lib下的核心类库,如java.lang.Object、java.lang.String等。
public class Load05 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("java.lang.Object");
        System.out.println(aClass.getClassLoader());
    }
}
  • 输出的结果是null,因为引导类加载器是由JVM的实现者用C/C++等语言编写的,而不是由Java编写的。在Java虚拟机的实现中,引导类加载器不是Java对象,也没有对应的Java类,因此它的ClassLoader属性为null。

扩展类加载器

  • 编写一个Tmp类
public class Tmp {
    static {
        System.out.println("classpath Tmp init");
    }
}
  • 加载Tmp类,并获取classLoader
public class Load06 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("com.demo.load.Tmp");
        System.out.println(aClass.getClassLoader());
    }
}
  • 输出如下,可以看到此时是由应用类加载器加载的
classpath Tmp init
sun.misc.Launcher$AppClassLoader@18b4aac2
  • 那我们现在写一个同名的Tmp类,将输出内容改为ext Tmp init
public class Tmp {
    
    
    static {
    
    
        System.out.println("ext Tmp init");
    }
}
  • 将其打成一个jar包,放到JAVA_HOME/jre/ext目录下
$ jar -cvf tmp.jar com/demo/load/Tmp.class
已添加清单
正在添加: Tmp.class(输入 = 479) (输出 = 321)(压缩了 32%)
  • 重新执行Load06,输出结果如下
ext Tmp init
sun.misc.Launcher$ExtClassLoader@29453f44
  • 此时就是从扩展类加载器加载的Tmp类了,因为当JVM需要加载一个类时,它会首先委托父类加载器去加载这个类

双亲委派机制

  • 所谓双亲委派机制,就是指调用类加载器的loadClass方法时,查找类的规则
    {% note warning no-icon %}感觉这个双亲翻译成上级更合适,因为它们之间并没有继承关系{% endnote %}
  • 我们来看看ClassLoader中的loadClass()方法的源码
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
  • 精简一下逻辑,双亲委派的核心思路如下
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // 检查类是否已经被加载
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                // 如果类没有被加载,则委托给父ClassLoader加载
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 如果父ClassLoader加载失败,则在自身查找类
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

线程上下文类加载器

  • 我们在使用JDBC时,都需要加载Driver驱动,但是我们好像并没有显示的调用Class.forName来加载Driver类
Class.forName("com.mysql.jdbc.Driver");
  • 那么实际上是如何加载这个驱动的呢?让我们来追踪一下源码,这里只看最核心的部分
public class DriverManager {

    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();

    ···

    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

    ···

}
  • 我们试着输出一下DirverManager的类加载器是谁
System.out.println(DriverManager.class.getClassLoader());
  • 输出的结果是null,那么说明它是由Bootstrap ClassLoader加载的,那么按理说应该是去JAVA_HOMT/jre/lib下搜索驱动类。
  • JAVA_HOMT/jre/lib显然没有mysql-connector-java-5.7.31.jar包,在DriverManager的静态代码块中,是如何正确加载com.mysql.jdbc.Driver的呢?
  • 继续来看看loadInitialDrivers()方法的源码
    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;
        }
        // If the driver is packaged as a Service Provider, load it.
        // Get all the drivers through the classloader
        // exposed as a java.sql.Driver.class service.
        // ServiceLoader.load() replaces the sun.misc.Providers()

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

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

                /* Load these drivers, so that they can be instantiated.
                 * It may be the case that the driver class may not be there
                 * i.e. there may be a packaged driver with the service class
                 * as implementation of java.sql.Driver but the actual class
                 * may be missing. In that case a java.util.ServiceConfigurationError
                 * will be thrown at runtime by the VM trying to locate
                 * and load the service.
                 *
                 * Adding a try catch block to catch those runtime errors
                 * if driver not available in classpath but it's
                 * packaged as service and that service is there in classpath.
                 */
                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);
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }
  • 先看2,它最后是使用的Class.forName完成类的加载和初始化,关联的是应用类加载器,因此可以顺利完成驱动类的加载
  • 在看1,它就是大名鼎鼎的Service Provider Interface(SPI)
    • 约定如下,在jar包的META-INF/services包下,以接口全限定名名为文件,文件内容是实现类名称
      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iIwmLPU6-1682411002919)(null)]
    • 这样就可以使用如下代码遍历来得到实现类
    ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class);
    Iterator<接口类型> iter = allImpls.iterator();
    while(iter.hasNext()){
        iter.next();
    }
    
    • 体现的是面向接口编程 + 解耦的思想,在下面的一些框架中都运用了此思想
      • JDBC
      • Servlet初始化器
      • 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 class LazyIterator
        implements Iterator<S>
    {
    
        Class<S> service;
        ClassLoader loader;
        Enumeration<URL> configs = null;
        Iterator<String> pending = null;
        String nextName = null;
    
        private LazyIterator(Class<S> service, ClassLoader loader) {
            this.service = service;
            this.loader = loader;
        }
    
        private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    fail(service, "Error locating configuration files", x);
                }
            }
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return false;
                }
                pending = parse(service, configs.nextElement());
            }
            nextName = pending.next();
            return true;
        }
    
        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
        }
    
        public boolean hasNext() {
            if (acc == null) {
                return hasNextService();
            } else {
                PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
                    public Boolean run() { return hasNextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }
    
        public S next() {
            if (acc == null) {
                return nextService();
            } else {
                PrivilegedAction<S> action = new PrivilegedAction<S>() {
                    public S run() { return nextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }
    
        public void remove() {
            throw new UnsupportedOperationException();
        }
    
    }
    

自定义类加载器

  • 先来思考一下:什么时候需要自定义类加载器
    1. 自定义类加载器可用于加载非 Classpath 路径中的类文件,例如外部配置文件夹、网络资源或其他自定义路径。这种需求在一些动态扩展或插件化的场景中比较常见。
    2. 在应用程序中使用的类可以通过接口来使用,而不是直接引用类。这种做法可以减少应用程序之间的依赖,从而提高代码的灵活性和可维护性。同时,这种做法也使得框架的设计更加清晰和可扩展。
    3. 在Tomcat容器中,每个Web应用程序都使用自己的类加载器,从而避免了不同Web应用程序之间的类冲突问题。
  • 步骤
    1. 继承ClassLoader类
    2. 遵从双亲委派机制,重写findClass方法
      • 注意不要重写loadClass方法,否则不会走双亲委派机制
    3. 读取类文件的字节码
    4. 调用父类的defineClass方法来加载类
    5. 使用者调用类加载器的loadClass方法
  • 示例
    1. 准备一个Tmp类,编译后将其.class文件放至D盘根目录下
    package myclasspath;
    
    public class Tmp {
        static {
            System.out.println("init myclasspath.Tmp");
        }
    
        public static void main(String[] args) {
            System.out.println();
        }
    }
    
    1. 自定义MyClassLoader类
    class MyClassLoader extends ClassLoader {
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            String path = "D:\\myclasspath\\" + name + ".class";
            try {
                ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                Files.copy(Paths.get(path), outputStream);
                byte[] bytes = outputStream.toByteArray();
                return defineClass(name, bytes, 0, bytes.length);
            } catch (IOException e) {
                e.printStackTrace();
                throw new ClassNotFoundException("类文件未找到:" + e);
            }
        }
    }
    
    1. 调用自定义的类加载器loadClass方法来加载Tmp类
    public class Load07 {
        public static void main(String[] args) throws Exception {
            MyClassLoader classLoader = new MyClassLoader();
            Class<?> aClass = classLoader.loadClass("myclasspath.Tmp");
            aClass.newInstance();
        }
    }
    
    • 控制台输出如下,成功加载Tmp类
    init myclasspath.Tmp
    

运行期优化

即时编译

分层编译(TieredComlilation)

  • 先来举个例子
public class JIT1 {
    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));
        }
    }
}
  • 输出结果如下
0	28300
1	27700
2	28500
3	26400
4	26400
5	26700
6	27200
7	27800
8	26200
9	26000
10	26200
11	28500
12	42900
13	26900
14	26900
15	26000
16	28300
17	25500
18	28500
19	26500
20	26100
21	27300
22	26600
23	26100
24	28300
25	25000
26	26400
27	26000
28	26500
29	26700
30	26400
31	26400
32	26100
33	26600
34	26300
35	26300
36	37600
37	26400
38	26000
39	28500
40	31700
41	43700
42	27000
43	26200
44	25600
45	30400
46	26400
47	26200
48	33800
49	26700
50	27700
51	26300
52	34100
53	26300
54	37400
55	33700
56	25100
57	28200
58	26000
59	41300
60	33500
61	26500
62	26300
63	26200
64	26500
65	26100
66	26300
67	26500
68	28800
69	26400
70	27100
71	27700
72	26500
73	16300
74	7000
75	8900
76	8800
77	13500
78	8300
79	9000
80	11900
81	9300
82	11700
83	9400
84	7700
85	10200
86	8800
87	6100
88	7300
89	7000
90	7200
91	5800
92	7100
93	7800
94	6800
95	5900
96	7300
97	6800
98	6900
99	5900
100	6800
101	8100
102	6700
103	6100
104	6700
105	6900
106	6700
107	5700
108	7100
109	13000
110	7000
111	6000
112	6700
113	7300
114	6700
115	6000
116	6700
117	6700
118	11400
119	5900
120	7000
121	6900
122	8400
123	6700
124	10100
125	9900
126	11500
127	8300
128	6700
129	7000
130	7000
131	6900
132	7500
133	6800
134	7800
135	7400
136	7000
137	7000
138	7000
139	7000
140	7100
141	7100
142	11400
143	10100
144	6800
145	7100
146	6800
147	6700
148	7000
149	6600
150	6600
151	6800
152	6700
153	9400
154	5700
155	7100
156	6600
157	7100
158	6000
159	7800
160	11800
161	6800
162	5800
163	6700
164	6600
165	7100
166	6800
167	7900
168	7000
169	10100
170	6900
171	6600
172	7200
173	10000
174	6700
175	51100
176	14900
177	300
178	300
179	300
180	300
181	300
182	300
183	300
184	200
185	300
186	200
187	200
188	300
189	300
190	300
191	300
192	300
193	300
194	300
195	300
196	300
197	200
198	300
199	300
  • 可以看到循环到73次附近时,速度明显加快了,循环到178次时,速度又明显加快了,这是为什么呢?
  • JVM将执行状态分为5个层次
    1. 0层:解释执行(Interpreter)
      • 在0层,JVM使用解释器来直接解释Java字节码,并执行程序。这种方式简单但效率较低,因为解释器需要逐条解释字节码指令,并执行它们,每次执行时都需要对字节码进行解析
    2. 1层:使用C1即时编译器编译执行(不带profilling)
      • 在1层,JVM会使用即时编译器(JIT)将Java字节码编译成本地机器码,然后直接执行机器码。这种方式相比于解释器,可以提供更高的执行速度。C1即时编译器适合编译执行热点代码,即被频繁执行的代码
    3. 2层:使用C1即时编译器编译执行(带基本的profilling)
      • 在2层,JVM会收集一些基本的执行状态数据,即profilling。例如方法的调用次数、循环的回边次数等,然后根据这些数据来决定哪些代码块需要被编译执行。这种方式可以更加精确地编译热点代码,从而提高程序的执行速度
    4. 3层:使用C1即时编译器编译执行(带完全的profilling)
      • 在3层,JVM会收集更加详细的执行状态数据,例如内联调用的次数、方法的参数类型等,以便更好地优化代码。这种方式可以进一步提高程序的执行速度,但同时也会增加编译的开销
    5. 4层:使用C2即时编译器编译执行
      • 在4层,JVM会使用更高级别的即时编译器(C2)来对代码进行优化,包括对循环、分支和递归等结构的优化。C2编译器的编译时间比C1场,但编译出来的代码执行速度更快。
        {% note info no-icon %}profilling是指在运行过程中手机一些程序执行的状态数据,例如方法的调用次数循环的回边次数{% endnote %}
  • 即时编译器(JIT)和解释器的区别
    • 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
    • JIT是将一些字节码编译为机器码,并存入Code Cache,下次遇到相同的代码,直接执行,无需再次编译
    • 解释器是将字节码解释为针对所有平台都通用的机器码
    • JIT会根据平台类型,生成平台特定的机器码
  • 对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采用解释器执行的方法运行;
  • 另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度
  • 执行效率上简单比较一下:Interceptor < C1 < C2
  • 上面代码中最后的耗时都在300附近,这是C2即时编译器做了逃逸分析,因为上面的代码中,我们仅仅是创建了Object对象,而并没有使用它,也就是没有逃逸出当前作用域
    • 在进行逃逸分析时,JVM会分析对象是否可能被线程外的代码引用,如果对象不会逃逸出当前方法的作用域,那么JVM会将对象的分配优化为栈上分配,从而避免了堆内存的分配和垃圾回收的压力。
  • 将对象分配在栈上的优点是:
    1. 快速分配和回收:栈内存的分配和回收都非常快,比堆内存要快得多。如果对象可以在栈上分配,那么它的分配和回收都可以更快,从而提高程序的性能。
    2. 减少垃圾回收:在Java中,对象的分配和回收是由垃圾回收器来完成的。如果对象可以在栈上分配,那么它就不会对堆内存的使用和垃圾回收产生影响,从而可以减少垃圾回收的频率和时间,提高程序的性能。
  • 我们可以添加VM参数-XX:-DoEscapeAnalysis关闭逃逸分析,然后再次执行代码,观察耗时情况
0	28100
1	28100
2	26400
3	26700
4	26700
5	26600
6	26600
7	26300
8	26400
9	26400
10	26500
11	26700
12	25900
13	39200
14	26700
15	26400
16	26600
17	35300
18	26400
19	26800
20	28600
21	28100
22	28700
23	28100
24	29900
25	33800
26	31300
27	29700
28	28500
29	26700
30	30900
31	30100
32	26700
33	30300
34	29700
35	26200
36	26200
37	28700
38	26800
39	29700
40	28600
41	30100
42	30700
43	28300
44	34000
45	26400
46	26100
47	28800
48	26800
49	28000
50	37800
51	27600
52	33700
53	36600
54	26900
55	25900
56	35500
57	26100
58	26100
59	26300
60	26000
61	29800
62	27600
63	30800
64	26900
65	26800
66	27100
67	11800
68	6800
69	7500
70	8500
71	7100
72	6900
73	6900
74	6800
75	6800
76	11300
77	8800
78	10200
79	10500
80	8400
81	6800
82	8400
83	7100
84	6700
85	7000
86	8100
87	6700
88	6700
89	7000
90	9100
91	12700
92	13000
93	11100
94	7700
95	5700
96	6900
97	8600
98	7100
99	7400
100	6700
101	13100
102	20000
103	9600
104	7100
105	7200
106	6900
107	6000
108	6900
109	6700
110	6800
111	7000
112	6700
113	6900
114	9500
115	6100
116	7200
117	7000
118	7000
119	7000
120	6600
121	6800
122	7100
123	6100
124	6900
125	6800
126	7100
127	7100
128	11700
129	11400
130	10300
131	10500
132	27200
133	11800
134	13200
135	73400
136	33800
137	8200
138	7500
139	6400
140	6200
141	6200
142	6200
143	13100
144	7400
145	6600
146	7100
147	6000
148	6200
149	6000
150	5200
151	6100
152	6000
153	6000
154	5200
155	9600
156	8800
157	6300
158	5600
159	6700
160	6200
161	7100
162	5800
163	6500
164	6200
165	6100
166	6000
167	6100
168	6200
169	6100
170	5900
171	7100
172	7900
173	6400
174	6400
175	6100
176	6300
177	6300
178	6300
179	6100
180	6900
181	6100
182	6500
183	5900
184	6300
185	6100
186	6300
187	6300
188	6100
189	6200
190	9100
191	8500
192	6300
193	6100
194	6000
195	6100
196	6300
197	6100
198	6200
199	5900

方法内联(Inlining)

  • 方法内联
private static int square(int i) {
    return i * i;
}

System.out.println(square(9));
  • 如果发现square是热点方法,且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝,病粘贴到调用者的位置
System.out.println(9 * 9);
  • 还能够进行常量折叠(constant folding)的优化
System.out.println(81);
  • 下面来验证一下,还是输出耗时
0	81	25200
1	81	24100
2	81	19800
3	81	20000
4	81	20200

···

73	81	19600
74	81	20000
75	81	9000
76	81	2300
77	81	2300
78	81	3400

···

267	81	3900
268	81	51900
269	81	15900
270	81	100
271	81	0
272	81	0
273	81	100
274	81	100
275	81	0
276	81	0

  • 最后耗时为0,就是进行了常量折叠的优化
  • 我们可以添加VM参数-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining打印内联信息,可以看到我们的square方法被标记为了热点代码
  • 同时也可以禁止某个方法的内联-XX:CompileCommand=dontinline,*JIT2.square,不能进行常量折叠优化了,速度不会到达0

···

495	81	3300
496	81	3700
497	81	3000
498	81	2900
499	81	2900

字段优化

  • JMH基准测试参考:https://openjdk.org/projects/code-tools/jmh/
  • 添加如下依赖
<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-core</artifactId>
    <version>1.32</version>
</dependency>
<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-generator-annprocess</artifactId>
    <version>1.32</version>
    <scope>provided</scope>
</dependency>
  • 编写基准测试代码
@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.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();
    }
}
  • @Warmup注解表示在基准测试运行之前需要进行预热,以使JVM达到最佳运行状态。在这个例子中,预热进行了2次,每次持续1秒钟。
  • @Measurement注解表示运行5次基准测试,每次持续1秒钟。
  • @State注解定义了Benchmark1类的实例作用域为Scope.Benchmark,表示这个类的实例可以在不同的测试方法之间共享,并保持在整个基准测试运行期间的状态。
  • 这个类包含了三个测试方法:test1、test2和test3。这些测试方法执行相同的操作,即对数组elements中的所有元素进行求和操作,但使用不同的方法来访问数组中的元素。test1使用了数组索引,test2使用了本地数组变量,而test3使用了foreach循环。
  • 启用doSum的方法内联@CompilerControl(CompilerControl.Mode.INLINE),测试结果如下
Benchmark          Mode  Cnt        Score       Error  Units
Benchmark1.test1  thrpt    5  2830851.513 ± 68534.850  ops/s
Benchmark1.test2  thrpt    5  2844317.417 ±  8097.137  ops/s
Benchmark1.test3  thrpt    5  2849940.840 ±  7190.091  ops/s
  • 我们这里重点关注的是Score,现在开启了doSum的内联,这三种遍历方式的性能没有显著差异

  • 那现在禁用doSum方法的内联@CompilerControl(CompilerControl.Mode.DONT_INLINE),测试结果如下

Benchmark          Mode  Cnt       Score       Error  Units
Benchmark1.test1  thrpt    5  313751.710 ± 34874.348  ops/s
Benchmark1.test2  thrpt    5  388759.125 ± 90456.387  ops/s
Benchmark1.test3  thrpt    5  394614.041 ± 50721.161  ops/s
  • 这三种遍历方式的性能与之前相比,都下降了一个数量级,test2和test3的性能差异不大,test1的性能明显要差一点,这是为什么呢?
  • 因为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
        }
    }
    
    • 如果doSum方法被内联,则循环中的每次对elements数组的访问都可以被优化,编译器可以将数组长度的读取操作提到循环外部,将elements数组的引用保存在本地变量中,从而避免了循环中每次访问数组引用的开销。这样,循环中只需要进行一次数组长度的读取,以及1000次对数组元素的访问操作,可以节省1999次对数组引用的访问。
    • 如果doSum方法没有被内联,则循环中的每次对elements数组的访问都需要通过方法调用来完成,这会导致每次循环中都需要进行一次对数组引用的读取操作,因此不能进行上述优化。

反射优化

  • 示例代码
import java.lang.reflect.Method;

public class Reflect1 {
    public static void foo() {
        System.out.println("foo...");
    }

    public static void main(String[] args) throws Exception {
        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();
    }
}
  • 首先定义了一个名为foo的静态方法,该方法只是简单地输出一条字符串。
  • 然后在main方法中,使用Reflect1类的getMethod方法获取名为fooMethod对象,以便之后进行反射调用。
  • 接着使用循环调用反射方法,循环次数从0到16,每次循环都调用反射获取的Method对象的invoke方法,传入null作为静态方法的调用者。因为foo方法是静态方法,所以调用者可以为null。
  • 最后使用System.in.read()方法暂停程序的运行,以便我们可以观察程序的输出结果。
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;
    }

    public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
        if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
            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);
}
  • 前15次调用使用的是NativeMethodAccessorImpl实现的MethodAccessor,该实现类使用JNI调用底层的C/C++代码实现方法调用。由于NativeMethodAccessorImpl的实现开销较大,因此前15次的反射调用的性能相对较差。
private static int inflationThreshold = 15;
  • 而第16次调用则采用了GeneratedMethodAccessor1实现的MethodAccessor,这个实现类通常是使用Java字节码动态生成的,因此方法调用的性能比NativeMethodAccessorImpl更好。这是因为在第15次调用时,生成了一个新的MethodAccessorImpl实现类(MethodAccessorGenerator),并在下一次方法调用时使用该实现类,即第16次调用。

猜你喜欢

转载自blog.csdn.net/qq_33888850/article/details/130368019
今日推荐