类文件结构

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

  • 根据JVM规范,类文件结构如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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]; // 类或接口的附加属性信息数组
}

魔数

  • 以下面的字节码文件,按顺序逐个进行分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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虚拟机将无法加载该文件。
  • 关于cafebabe这个魔数的由来并没有具体的官方解释,但有一些有趣的猜测和传说。
    • 一种说法是,这个魔数是由Java的创造者之一、现任谷歌高管James Gosling取的。据说Gosling是个爱好咖啡的人,他认为Java这个名字也与咖啡有关,所以他将cafebabe取作魔数来向咖啡致敬。另外,有一种传说认为这个魔数是来自于一个好莱坞电影中的经典台词,cafe babe(咖啡宝贝?)。
    • 然而,无论是什么样的由来,cafebabe这个魔数现在已经成为Java世界中的一个标志,每一个Java程序员都能够轻松地辨认出这个魔数,这也是Java文件格式稳定性的一个体现。

版本

  • 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

    • 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();构造方法的字节码指令
    1
    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[]); 主方法的字节码指令
    1
    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文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
$ 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代码
1
2
3
4
5
6
7
8
9
10
11
/**
* 演示 字节码指令 和 操作数栈、常量池的关系
*/
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. 编译后的字节码文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
$ 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代码
1
2
3
4
5
6
7
8
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);
}
}
  • 编译后的字节码文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
$ 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上进行运算,下面逐行分析字节码指令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 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代码
1
2
3
4
5
6
7
8
9
10
public class Demo_22 {
public static void main(String[] args) {
int a = 0;
if (a == 0) {
a = 10;
} else {
a = 20;
}
}
}
  • 编译后的字节码文件
1
2
3
4
5
6
7
8
9
10
 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代码
1
2
3
4
5
6
7
8
public class Demo_23 {
public static void main(String[] args) {
int a = 0;
while (a < 10) {
a++;
}
}
}
  • 编译后的字节码文件
1
2
3
4
5
6
7
8
 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循环
1
2
3
4
5
6
7
8
public class Demo_24 {
public static void main(String[] args) {
int i = 0;
do {
i++;
} while (i < 10);
}
}
  • 编译后的字节码文件
1
2
3
4
5
6
7
 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循环
1
2
3
4
5
6
7
public class Demo_25 {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {

}
}
}
  • 编译后的字节码文件
1
2
3
4
5
6
7
8
 0: iconst_0
1: istore_1
2: iload_1
3: bipush 10
5: if_icmpge 14
8: iinc 1, 1
11: goto 2
14: return

注意到while和for的字节码,它们是一模一样的,这就是所谓的殊途同归

判断结果

  • 从字节码的角度来分析下面程序的运行结果
1
2
3
4
5
6
7
8
9
10
11
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
  • 下面是对应的字节码
1
2
3
10: iload_2
11: iinc 2, 1
14: istore_2

构造方法

  1. <cinit>()V
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class Demo_27 {
    static int i = 10;

    static {
    i = 20;
    }

    static {
    i = 30;
    }

    public static void main(String[] args) {

    }
    }
    • 编译后的字节码文件
    1
    2
    3
    4
    5
    6
    7
     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
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    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);
    }
    }
    • 编译后的字节码文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
     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方法,公共方法,静态方法
    • 其中静态方法包括对象调静态方法和类直接调静态方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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();
    }
    }
    • 编译后的字节码文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
     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指令,把对象引用从操作数栈弹掉了,因为静态方法不需要对象引用来掉,通过这种方式,反而会增加两步无用的字节码指令
    1
    2
    3
    20: aload_1
    21: pop
    22: invokestatic #7 // Method test4:()V

多态的原理

  • 原始Java代码
    • 定义了一个抽象类Animal,还有其两个子类Cat和Dog
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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安装目录,执行
    1
    java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB
    • 进入图形界面attach进程id
  3. 查找某个对象
    • 打开Tools -> Find Object By Query,输入命令,点击Execute执行
    1
    select d from com.demo.Dog d


4. 查看对象内存结构
- 点击超链接可以看到对象的内存结构,此对象没有任何属性,因此只有对象头的16字节,前8字节是MarkWord,后8字节就是对象的Class指针,但现在看不到它的实际地址
5. 查看对象Class的内存地址
- 可以通过Windows -> Console进入命令行模式,执行如下命令

1
mem 0x000001f1676e77c8 2

- mem指令有两个参数,参数1是对象地址,参数2是查看2行(即16字节)
- 结果中第二行0x000001f1f48841a0即为Class的内存地址

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

- 无论通过哪种方法,都可以找到Dog Class的vtable长度为6,意思就是Dog类会有6个虚方法(多台相关的,final、static不会列入)
- 那么这6个虚方法都是谁呢?从Class的起始地址开始算,偏移0x1b8就是vtable的其实地址,进行计算得到
1
2
3
4
0x000001f1f48841a0
1b8 +
--------------------
0x000001f1f4884358

- 通过Windows -> Console进入命令行模式,执行如下命令,就得到了6个虚方法的入口地址
1
2
3
4
5
6
7
mem 0x000001f1f4884358 6
0x000001f1f4884358: 0x000001f1f4481b10
0x000001f1f4884360: 0x000001f1f44815e8
0x000001f1f4884368: 0x000001f1f4883750
0x000001f1f4884370: 0x000001f1f4481540
0x000001f1f4884378: 0x000001f1f4481678
0x000001f1f4884380: 0x000001f1f4884148

7. 验证方法地址
- 通过Tools -> Class Browser 查看每个类的方法定义,比较可知
1
2
3
4
5
6
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代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class Demo_31 {
    public static void main(String[] args) {
    int i = 0;
    try {
    i = 10;
    } catch (Exception e) {
    i = 20;
    }
    }
    }
    • 编译后的字节码文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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;
    }
    }
    }
    • 编译后的字节码文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    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代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class Demo_33 {
    public static void main(String[] args) {
    int i = 0;
    try {
    i = 10;
    } catch (NoSuchMethodError | IllegalAccessError | Exception e) {
    e.printStackTrace();
    }
    }
    }
    • 编译后的字节码文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    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代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class Demo_34 {
    public static void main(String[] args) {
    int i = 0;
    try {
    i = 10;
    } catch (Exception e) {
    i = 20;
    } finally {
    i = 30;
    }
    }
    }
    • 编译后的字节码文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    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代码,先自己试着想一下最终的结果是啥:20
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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;
    }
    }
    }
    • 编译后的字节码文件
    • 注意这里要加上-p参数,才能显示私有方法的信息
    1
    javap -v -p Demo_35.class
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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,那么就会吞掉异常,具体来看下面这个例子
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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代码,还是先试着想想结果会输出什么:10
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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;
    }
    }
    }
    • 编译后的字节码文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    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代码
    1
    2
    3
    4
    5
    6
    7
    8
    public class Demo_38 {
    public static void main(String[] args) {
    Object lock = new Object();
    synchronized (lock) {
    System.out.println("ok");
    }
    }
    }
    • 编译后的字节码文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    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 编译器会自动为该类生成一个无参构造函数。
1
2
3
public class Candy01 {

}
  • 编译成class后的代码
1
2
3
4
5
6
7
public class Candy1 {

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

自动拆装箱

  • 这个特性是 JDK 5 开始加入的,代码片段1:
1
2
3
4
5
6
public class Candy02 {
public static void main(String[] args) {
Integer x = 1;
int y = x;
}
}
  • 但是这段代码在JDK 5之前是无法编译通过的,比如改写为如下形式,代码片段2
1
2
3
4
5
6
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类型来处理
1
2
3
4
5
6
7
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);
}
}
  • 所以在取值时,编译器真正生成的字节码中,还需要额外做一个类型转换的操作
1
2
// 需要将 Object 转为 Integer
Integer x = (Integer)list.get(0);
  • 如果前面的x遍历修改为int基本类型,那么最终生成的字节码为
1
int x = (Integer)list.get(0).intValue();
  • 还好这些麻烦事都不用自己做,要么叫语法糖呢
  • 擦除的是字节码上的泛型信息,可以看到LocalVariableTypeTable仍然保留了方法参数泛型的信息
    • 从下面字节码的第26行,我们可以清楚的看到add方法其实添加的是Object类型对象
    • 从下面字节码的第30行,我们可以清楚的看到get方法的返回值也是Object类型对象
    • 同时第31行是将类型强制转换为Integer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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;>;
  • 使用反射,能够获取到方法类型参数的泛型和方法返回值泛型的信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;
}
}
  • 输出
1
2
3
4
5
6
原始类型 - 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开始加入的新特性,示例代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
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编译器会在编译期间将上述代码转换为
1
2
3
4
5
6
7
8
9
10
11
12
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);
}

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

foreach循环

  • 仍然是JDK 5开始引入的语法糖,数组的循环
1
2
3
4
5
6
7
8
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);
}
}
}
  • 会被编译器转换为
1
2
3
4
5
6
7
8
9
10
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);
}
}
  • 而集合的循环
1
2
3
4
5
6
7
8
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);
}
}
}
  • 实际上会被编译器转换为对迭代器的调用
1
2
3
4
5
6
7
8
9
10
11
12
13
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);
}
}
}
  • foreach循环写法,能够配合数组,以及所有实现了Iterable接口的集合类一起使用,其中Iterable用来获取集合的迭代器Iterator

switch字符串

  • JDK 7开始,switch可以作用于字符串和枚举类,这个功能其实也是语法糖,例如
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}
}
}
}
  • 注意:swtich配合Spring和枚举使用时,变量不能为null,原因分析完语法糖转换后的代码,自然就清楚了
  • 会被编译器转换为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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,例如下面的代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public static void choose(String str) {
    switch (str) {
    case "BM": {
    System.out.println("h");
    break;
    }
    case "C.": {
    System.out.println("w");
    break;
    }
    }
    }
    • 会被编译器转换为
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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枚举的例子,原始代码
1
2
3
enum Sex {
MALE, FEMALE
}
1
2
3
4
5
6
7
8
9
10
public  static void foo(Sex sex){
switch (sex){
case MALE:
System.out.println("男");
break;
case FEMALE:
System.out.println("女");
break;
}
}
  • 转换后的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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 新增了枚举类,以前面的性别枚举为例
1
2
3
enum Sex {
MALE, FEMALE
}
  • 转换后的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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
1
2
3
4
5
try (资源变量 = 创建资源对象) {

} catch() {

}
  • 其中资源对象需要实现AutoCloseable接口,例如InputStream、OutputStream、Connection、Statement、ResultSet等接口都实现了AuthCloseable接口,使用try-with-resources 可以不用写finally语句块,编译器会帮助我们生成关闭资源代码,例如
1
2
3
4
5
6
7
8
9
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();
}
}
}
  • 会被编译器转换为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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)(添加被压制异常)的方法呢?
    • 这是为了防止异常信息的丢失
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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 异常");
    }
    }
    • 输出如下,两个异常信息都不会丢失
    1
    2
    3
    4
    5
    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. 子类返回值可以是父类返回值的子类(比较绕口,直接看下面的例子来理解)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      class A {
      public Number m() {
      return 1;
      }
      }
      class B extends A {
      @Override
      // 父类A方法的返回值是Number类型,子类B方法的返回值是Integer类型,Integer是Number的子类
      public Integer m() {
      return 2;
      }
      }
      • 那么对于子类,编译器会做如下处理
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      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代码
1
2
3
4
5
6
7
8
9
10
public class Candy10 {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("ok");
}
};
}
}
  • 转换后代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 额外生成的类
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代码
1
2
3
4
5
6
7
8
9
10
public class Candy11 {
public static void test(final int x){
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("ok" + x);
}
};
}
}
  • 转换后代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 额外生成的类
final class Candy11$1 implements Runnable {
int val$x;
Candy11$1(int x) {
this.val$x = x;
}
public void run() {
System.out.println("ok:" + this.val$x);
}
}
public class Candy11 {
public static void test(final int x) {
Runnable runnable = new Candy11$1(x);
}
}
  • 注意:这也解释了为什么匿名内部类引用局部变量时,局部变量必须为final的
    • 因为在创建Candy$11对象时,将x的值赋给了val$x属性,所以x不应该再发生变化了
    • 如果变化,那么$val$x属性没有机会再跟着一起变化

类加载阶段

加载

  • 将类的字节码载入方法区中,内部采用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:接口方法表
  • 如果这个类还有父类没有加载,先加载父类
  • 加载和链接可能是交替运行的
  • instanceKlass这样的元数据是存储在方法区(1.8后是在元空间内),但_java_mirror是存储在堆中
  • 可以通过HSDB工具查看

链接

  1. 验证
    • 验证类是否符合JVM规范,安全性检查
    • 使用支持二进制的编辑器修改HelloWorld.class的魔数ca fe ba be,在控制台运行后悔报错
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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代码
    1
    2
    3
    4
    5
    6
    7
    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();
    }
    • 编译后的字节码文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    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. 解析
    • 将常量池中的符号引用解析为直接引用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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方法所在的类,总会被首先初始化
      1
      2
      3
      4
      5
      6
      7
      8
      9
      public class Load03 {
      static {
      System.out.println("main init");
      }

      public static void main(String[] args) throws ClassNotFoundException {

      }
      }
      • 控制台会输出
      1
      main init
    2. 首次访问这个类的静态变量或静态方法时,会进行初始化
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      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");
      }
      }
      • 控制台输出
      1
      2
      3
      main init
      a init
      0
    3. 子类初始化,如果父类还没未初始化,则父类也会进行初始化
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      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");
      }
      }
      • 控制台输出
      1
      2
      3
      4
      main init
      a init
      b init
      false
    4. 默认的Class.forName会导致初始化
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      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");
      }
      }
      • 控制台输出
      1
      2
      main init
      a init
    5. new对象会导致初始化
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      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");
      }
      }
      • 控制台输出
      1
      2
      main init
      a init
  • 不会导致类初始化的情况
    1. 访问类的 static final 静态常量(基本类型和字符串) 不会触发初始化
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      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");
      }
      }
      • 控制台输出
      1
      2
      main init
      5.0
    2. 调用类对象.class不会触发初始化
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      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");
      }
      }
      • 控制台输出
      1
      2
      main init
      class com.demo.B
    3. 类加载器的loadClass方法不会触发初始化
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      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");
      }
      }
      • 控制台输出
      1
      main init
    4. Class.forName的参数2为false时(initalize = false),不会触发初始化
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      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");
      }
      }
      • 控制台输出
      1
      main init

练习

  • 从字节码分析,使用a、b、c这三个常量,是否会导致E初始化
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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()方法来装箱,只能推迟到初始化阶段运行,字节码如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    {
    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
    }
  • 典型应用 -> 完成懒惰初始化的单例模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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等。
1
2
3
4
5
6
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类
1
2
3
4
5
public class Tmp {
static {
System.out.println("classpath Tmp init");
}
}
  • 加载Tmp类,并获取classLoader
1
2
3
4
5
6
public class Load06 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("com.demo.load.Tmp");
System.out.println(aClass.getClassLoader());
}
}
  • 输出如下,可以看到此时是由应用类加载器加载的
1
2
classpath Tmp init
sun.misc.Launcher$AppClassLoader@18b4aac2
  • 那我们现在写一个同名的Tmp类,将输出内容改为ext Tmp init
1
2
3
4
5
public class Tmp {
static {
System.out.println("ext Tmp init");
}
}
  • 将其打成一个jar包,放到JAVA_HOME/jre/ext目录下
1
2
3
$ jar -cvf tmp.jar com/demo/load/Tmp.class
已添加清单
正在添加: Tmp.class(输入 = 479) (输出 = 321)(压缩了 32%)
  • 重新执行Load06,输出结果如下
1
2
ext Tmp init
sun.misc.Launcher$ExtClassLoader@29453f44
  • 此时就是从扩展类加载器加载的Tmp类了,因为当JVM需要加载一个类时,它会首先委托父类加载器去加载这个类

双亲委派机制

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

感觉这个双亲翻译成上级更合适,因为它们之间并没有继承关系

  • 我们来看看ClassLoader中的loadClass()方法的源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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;
}
}
  • 精简一下逻辑,双亲委派的核心思路如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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类
1
Class.forName("com.mysql.jdbc.Driver");
  • 那么实际上是如何加载这个驱动的呢?让我们来追踪一下源码,这里只看最核心的部分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class DriverManager {

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

···

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

···

}
  • 我们试着输出一下DirverManager的类加载器是谁
1
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()方法的源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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包下,以接口全限定名名为文件,文件内容是实现类名称
    • 这样就可以使用如下代码遍历来得到实现类
    1
    2
    3
    4
    5
    ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class);
    Iterator<接口类型> iter = allImpls.iterator();
    while(iter.hasNext()){
    iter.next();
    }
    • 体现的是面向接口编程 + 解耦的思想,在下面的一些框架中都运用了此思想
      • JDBC
      • Servlet初始化器
      • Spring容器
      • Dubbo(对SPI进行了扩展)
    • 接着看ServiceLoader.load方法
    1
    2
    3
    4
    public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
    }
    • 线程上下文类加载器是当前线程使用的类加载器,默认就是应用类加载器,它内部又是由Class.forName调用了线程上下文类加载器完成类加载,具体代码在ServiceLoader的内部类LazyIterator中
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    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盘根目录下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    package myclasspath;

    public class Tmp {
    static {
    System.out.println("init myclasspath.Tmp");
    }

    public static void main(String[] args) {
    System.out.println();
    }
    }
    1. 自定义MyClassLoader类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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类
    1
    2
    3
    4
    5
    6
    7
    public class Load07 {
    public static void main(String[] args) throws Exception {
    MyClassLoader classLoader = new MyClassLoader();
    Class<?> aClass = classLoader.loadClass("myclasspath.Tmp");
    aClass.newInstance();
    }
    }
    • 控制台输出如下,成功加载Tmp类
    1
    init myclasspath.Tmp

运行期优化

即时编译

分层编译(TieredComlilation)

  • 先来举个例子
1
2
3
4
5
6
7
8
9
10
11
12
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));
}
}
}
  • 输出结果如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
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场,但编译出来的代码执行速度更快。

    profilling是指在运行过程中手机一些程序执行的状态数据,例如方法的调用次数循环的回边次数

  • 即时编译器(JIT)和解释器的区别
    • 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
    • JIT是将一些字节码编译为机器码,并存入Code Cache,下次遇到相同的代码,直接执行,无需再次编译
    • 解释器是将字节码解释为针对所有平台都通用的机器码
    • JIT会根据平台类型,生成平台特定的机器码
  • 对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采用解释器执行的方法运行;
  • 另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度
  • 执行效率上简单比较一下:Interceptor < C1 < C2
  • 上面代码中最后的耗时都在300附近,这是C2即时编译器做了逃逸分析,因为上面的代码中,我们仅仅是创建了Object对象,而并没有使用它,也就是没有逃逸出当前作用域
    • 在进行逃逸分析时,JVM会分析对象是否可能被线程外的代码引用,如果对象不会逃逸出当前方法的作用域,那么JVM会将对象的分配优化为栈上分配,从而避免了堆内存的分配和垃圾回收的压力。
  • 将对象分配在栈上的优点是:
    1. 快速分配和回收:栈内存的分配和回收都非常快,比堆内存要快得多。如果对象可以在栈上分配,那么它的分配和回收都可以更快,从而提高程序的性能。
    2. 减少垃圾回收:在Java中,对象的分配和回收是由垃圾回收器来完成的。如果对象可以在栈上分配,那么它就不会对堆内存的使用和垃圾回收产生影响,从而可以减少垃圾回收的频率和时间,提高程序的性能。
  • 我们可以添加VM参数-XX:-DoEscapeAnalysis关闭逃逸分析,然后再次执行代码,观察耗时情况
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
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)

  • 方法内联
1
2
3
4
5
private static int square(int i) {
return i * i;
}

System.out.println(square(9));
  • 如果发现square是热点方法,且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝,并粘贴到调用者的位置
1
System.out.println(9 * 9);
  • 还能够进行常量折叠(constant folding)的优化
1
System.out.println(81);
  • 下面来验证一下,还是输出耗时
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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
1
2
3
4
5
6
7
8

···

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

字段优化

1
2
3
4
5
6
7
8
9
10
11
<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>
  • 编写基准测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@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),测试结果如下
1
2
3
4
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),测试结果如下

1
2
3
4
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方法会被优化成下面的样子(伪代码)
    1
    2
    3
    4
    5
    6
    7
    @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数组的访问都需要通过方法调用来完成,这会导致每次循环中都需要进行一次对数组引用的读取操作,因此不能进行上述优化。

反射优化

  • 示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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()方法暂停程序的运行,以便我们可以观察程序的输出结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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次的反射调用的性能相对较差。
1
private static int inflationThreshold = 15;
  • 而第16次调用则采用了GeneratedMethodAccessor1实现的MethodAccessor,这个实现类通常是使用Java字节码动态生成的,因此方法调用的性能比NativeMethodAccessorImpl更好。这是因为在第15次调用时,生成了一个新的MethodAccessorImpl实现类(MethodAccessorGenerator),并在下一次方法调用时使用该实现类,即第16次调用。