类文件结构
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ClassFile { u4 magic ; u2 minor_version ; u2 major_version ; u2 constant_pool_count ; cp_info constant_pool [constant_pool_count-1] ; u2 access_flags ; u2 this_class ; u2 super_class ; u2 interfaces_count ; u2 interfaces [interfaces_count] ; u2 fields_count ; field_info fields [fields_count] ; u2 methods_count ; method_info methods [methods_count] ; u2 attributes_count ; attribute_info attributes [attributes_count] ; }
魔数
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 0 a 00 06 00 15 09 0000020 00 16 00 17 08 00 18 0 a 00 19 00 1 a 07 00 1 b 07 0000040 00 1 c 01 00 06 3 c 69 6 e 69 74 3 e 01 00 03 28 29 0000060 56 01 00 04 43 6 f 64 65 01 00 0 f 4 c 69 6 e 65 4 e0000100 75 6 d 62 65 72 54 61 62 6 c 65 01 00 12 4 c 6 f 63 0000120 61 6 c 56 61 72 69 61 62 6 c 65 54 61 62 6 c 65 01 0000140 00 04 74 68 69 73 01 00 1 d 4 c 63 6 e 2 f 69 74 63 0000160 61 73 74 2 f 6 a 76 6 d 2 f 74 35 2 f 48 65 6 c 6 c 6 f0000200 57 6 f 72 6 c 64 3 b 01 00 04 6 d 61 69 6 e 01 00 16 0000220 28 5 b 4 c 6 a 61 76 61 2 f 6 c 61 6 e 67 2 f 53 74 72 0000240 69 6 e 67 3 b 29 56 01 00 04 61 72 67 73 01 00 13 0000260 5 b 4 c 6 a 61 76 61 2 f 6 c 61 6 e 67 2 f 53 74 72 69 0000300 6 e 67 3 b 01 00 10 4 d 65 74 68 6 f 64 50 61 72 61 0000320 6 d 65 74 65 72 73 01 00 0 a 53 6 f 75 72 63 65 46 0000340 69 6 c 65 01 00 0 f 48 65 6 c 6 c 6 f 57 6 f 72 6 c 64 0000360 2 e 6 a 61 76 61 0 c 00 07 00 08 07 00 1 d 0 c 00 1 e0000400 00 1 f 01 00 0 b 68 65 6 c 6 c 6 f 20 77 6 f 72 6 c 64 0000420 07 00 20 0 c 00 21 00 22 01 00 1 b 63 6 e 2 f 69 74 0000440 63 61 73 74 2 f 6 a 76 6 d 2 f 74 35 2 f 48 65 6 c 6 c0000460 6 f 57 6 f 72 6 c 64 01 00 10 6 a 61 76 61 2 f 6 c 61 0000500 6 e 67 2 f 4 f 62 6 a 65 63 74 01 00 10 6 a 61 76 61 0000520 2 f 6 c 61 6 e 67 2 f 53 79 73 74 65 6 d 01 00 03 6 f0000540 75 74 01 00 15 4 c 6 a 61 76 61 2 f 69 6 f 2 f 50 72 0000560 69 6 e 74 53 74 72 65 61 6 d 3 b 01 00 13 6 a 61 76 0000600 61 2 f 69 6 f 2 f 50 72 69 6 e 74 53 74 72 65 61 6 d0000620 01 00 07 70 72 69 6 e 74 6 c 6 e 01 00 15 28 4 c 6 a0000640 61 76 61 2 f 6 c 61 6 e 67 2 f 53 74 72 69 6 e 67 3 b0000660 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 2 f 00 01 00 01 0000720 00 00 00 05 2 a b7 00 01 b1 00 00 00 02 00 0 a 00 0000740 00 00 06 00 01 00 00 00 04 00 0 b 00 00 00 0 c 00 0000760 01 00 00 00 05 00 0 c 00 0 d 00 00 00 09 00 0 e 00 0001000 0 f 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 0 a0001040 00 00 00 0 a 00 02 00 00 00 06 00 08 00 07 00 0 b0001060 00 00 00 0 c 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项
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项
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项
08 表示一个字符串常量名称,00 18(24)表示它引用了常量池中 #24 项
0000020 00 16 00 17 08 00 18
0a 00 19 00 1a 07 00 1b 07
第#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项
07 表示一个 Class 信息,00 1b(27) 表示它引用了常量池中 #27 项
0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b
07
第#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项
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项
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项
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项
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项
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项
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项
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项
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项
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项
01 表示一个 utf8 串,00 04 表示长度,是args
0000240 69 6e 67 3b 29 56 01 00 04 61 72 67 73
01 00 13
第#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项
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项
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项
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项
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项
07 表示一个 Class 信息,00 1d(29) 引用了常量池中 #29 项
0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d
0c 00 1e
第#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项
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项
07 表示一个 Class 信息,00 20(32) 引用了常量池中 #32 项
0000420 07 00 20
0c 00 21 00 22 01 00 1b 63 6e 2f 69 74
第#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项
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项
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项
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项
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项
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项
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项
01 表示一个 utf8 串,00 07 表示长度,是println
0000620 01 00 07 70 72 69 6e 74 6c 6e
01 00 15 28 4c 6a
第#34项
01 表示一个 utf8 串,00 15(21) 表示长度,是(Ljava/lang/String;)V
0000620 01 00 07 70 72 69 6e 74 6c 6e 01 00 15 28 4c 6a
0000640 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b
0000660 29 56
00 21 00 05 00 06 00 00 00 00 00 02 00 01
访问标识与继承信息
访问标识符:21表示class是一个类,公共的
0000660 29 56 00 21
00 05 00 06 00 00 00 00 00 02 00 01
当前类或接口的索引:05表示根据常量池中的#5找到本类的全限定名
0000660 29 56 00 21 00 05
00 06 00 00 00 00 00 02 00 01
当前类的超类(父类)索引:06表示根据常量池中的#6找到父类全限定名
0000660 29 56 00 21 00 05 00 06
00 00 00 00 00 02 00 01
接口数量:本类为0
0000660 29 56 00 21 00 05 00 06 00 00
00 00 00 02 00 01
Flag Name
Value
Interpretation
ACC_PUBLIC
0x0001
Declared public; may be accessed from outside its package.
ACC_FINAL
0x0010
Declared final; no subclasses allowed.
ACC_SUPER
0x0020
Treat superclass methods specially when invoked by the invokespecial instruction.
ACC_INTERFACE
0x0200
Is an interface, not a class.
ACC_ABSTRACT
0x0400
Declared abstract; must not be instantiated.
ACC_SYNTHETIC
0x1000
Declared synthetic; not present in the source code.
ACC_ANNOTATION
0x2000
Declared as an annotation type.
ACC_ENUM
0x4000
Declared as an enum type.
Field信息
字段数量(成员变量数量),本类为0
0000660 29 56 00 21 00 05 00 06 00 00 00 00
00 02 00 01
Method信息
方法数量:本类为2,构造方法和main方法
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02
00 01
一个方法由访问修饰符、名称、参数描述、方法苏属性数量、方法属性组成
00 01表示访问修饰符(本类中为public)
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
00 07表示引用了常量池中的#07项作为方法名称
0000700 00 07
00 08 00 01 00 09 00 00 00 2f 00 01 00 01
00 08表示引用了常量池中的#08项作为方法参数描述
0000700 00 07 00 08
00 01 00 09 00 00 00 2f 00 01 00 01
01表示引方法属性数量,本方法是1
0000700 00 07 00 08 00 01
00 09 00 00 00 2f 00 01 00 01
00 09表示引用常量池#09项,发现是code
属性
0000700 00 07 00 08 00 01 00 09
00 00 00 2f 00 01 00 01
00 00 00 2f表示此属性的长度是47
0000700 00 07 00 08 00 01 00 09 00 00 00 2f
00 01 00 01
00 01表示操作数栈最大深度
0000700 00 07 00 08 00 01 00 09 00 00 00 2f 00 01
00 01
00 01表示局部变量最大槽(slot)数
0000700 00 07 00 08 00 01 00 09 00 00 00 2f 00 01 00 01
00 00 00 05表示字节码长度,本例为5
0000720 00 00 00 05
2a b7 00 01 b1 00 00 00 02 00 0a 00
2a b7 00 01 b1 是字节码指令
0000720 00 00 00 05 2a b7 00 01 b1
00 00 00 02 00 0a 00
00 00 00 02 表示方法细节属性数量,本例为2
0000720 00 00 00 05 2a b7 00 01 b1 00 00 00 02
00 0a 00
00 0a表示引用了常量池#10项,发现是LineNumberTable属性
0000720 00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a
00
00 00 00 06表示此属性的总长度,本例是6
0000720 00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a 00
0000740 00 00 06
00 01 00 00 00 04 00 0b 00 00 00 0c 00
00 01表示LineNumberTable长度
0000740 00 00 06 00 01
00 00 00 04 00 0b 00 00 00 0c 00
00 00 表示字节码行号
0000740 00 00 06 00 01 00 00
00 04 00 0b 00 00 00 0c 00
00 04表示Java源码行号
0000740 00 00 06 00 01 00 00 00 04
00 0b 00 00 00 0c 00
00 0b表示引用了常量池#11项,发现是LocalVariableTable属性
0000740 00 00 06 00 01 00 00 00 04 00 0b
00 00 00 0c 00
00 00 00 0c 表示此属性总长度,本例为12
0000740 00 00 06 00 01 00 00 00 04 00 0b 00 00 00 0c
00
00 01 表示LocalVariableTable
长度
0000740 00 00 06 00 01 00 00 00 04 00 0b 00 00 00 0c 00
0000760 01
00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e 00
00 00 表示局部变量生命周期开始,相对于字节码的偏移量
0000760 01 00 00
00 05 00 0c 00 0d 00 00 00 09 00 0e 00
00 05 表示局部变量覆盖的范围长度
0000760 01 00 00 00 05
00 0c 00 0d 00 00 00 09 00 0e 00
00 0c 表示局部变量的名称,引用常量池#12项
0000760 01 00 00 00 05 00 0c
00 0d 00 00 00 09 00 0e 00
00 0d 表示局部变量的类型,本例引用了常量池 #13 项,是Lcn/itcast/jvm/t5/HelloWorld;
0000760 01 00 00 00 05 00 0c 00 0d
00 00 00 09 00 0e 00
00 00 表述局部变量占有的槽位(slot)编号,本例是0
0000760 01 00 00 00 05 00 0c 00 0d 00 00
00 09 00 0e 00
00 09代表访问修饰符(本类中是 public static)
00 0e 代表引用了常量池 #14 项作为方法名称
00 0f 代表引用了常量池 #15 项作为方法参数描述
00 02 代表方法属性数量,本方法是 2
其余代表方法属性(属性1)
00 09 表示引用了常量池 #09 项,发现是Code
属性
00 00 00 37 表示此属性的长度是 55
00 02 表示操作数栈
最大深度
00 01 表示局部变量表
最大槽(slot)数
00 00 00 05 表示字节码长度,本例是 9
b2 00 02 12 03 b6 00 04 b1 是字节码指令
00 00 00 02 表示方法细节属性数量,本例是 2
00 0a 表示引用了常量池 #10 项,发现是LineNumberTable
属性
00 00 00 0a 表示此属性的总长度,本例是 10
00 02 表示LineNumberTable
长度
00 00 表示字节码
行号 00 06 表示java 源码
行号
00 08 表示字节码
行号 00 07 表示java 源码
行号
00 0b 表示引用了常量池 #11 项,发现是LocalVariableTable
属性
00 00 00 0c 表示此属性的总长度,本例是 12
00 01 表示LocalVariableTable
长度
00 00 表示局部变量生命周期开始,相对于字节码的偏移量
00 09 表示局部变量覆盖的范围长度
00 10 表示局部变量名称,本例引用了常量池 #16 项,是args
00 11 表示局部变量的类型,本例引用了常量池 #17 项,是[Ljava/lang/String;
00 00 表示局部变量占有的槽位(slot)编号,本例是 0
0000760 01 00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e 00
0001000 0f 00 02 00 09 00 00 00 37 00 02 00 01 00 00 00
0001020 09 b2 00 02 12 03 b6 00 04 b1 00 00 00 02 00 0a
0001040 00 00 00 0a 00 02 00 00 00 06 00 08 00 07 00 0b
0001060 00 00 00 0c 00 01 00 00 00 09 00 10 00 11 00 00
红色代表方法属性(属性2)
00 12 表示引用了常量池 #18 项,发现是MethodParameters
属性
00 00 00 05 表示此属性的总长度,本例是 5
01 参数数量
00 10 表示引用了常量池 #16 项,是args
00 00 访问修饰符
0001100 00 12 00 00 00 05 01 00 10 00 00
00 01 00 13 00
附加属性
00 01 表示附加属性数量
00 13 表示引用了常量池 #19 项,即SourceFile
00 00 00 02 表示此属性的长度
00 14 表示引用了常量池 #20 项,即HelloWorld.java
0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00
0001120 00 00 02 00 14
分
字节码指令
入门
在上一小节,有两个字节码指令,我们没有细说,那现在就来具体看看
一个是public cn.itcast.jvm.t5.HelloWorld();
构造方法的字节码指令
2a => aload_0 加载 slot 0 的局部变量,即 this,做为下面的 invokespecial 构造方法调用的参数
b7 => invokespecial 预备调用构造方法,哪个方法呢?
00 01 引用常量池中 #1 项,即Method java/lang/Object."<init>":()V
b1 表示返回
另一个是 public static void main(java.lang.String[]);
主方法的字节码指令 1 b2 00 02 12 03 b6 00 04 b1
b2 => getstatic 用来加载静态变量,哪个静态变量呢?
00 02 引用常量池中 #2 项,即Field java/lang/System.out:Ljava/io/PrintStream;
12 => ldc 加载参数,哪个参数呢?
03 引用常量池中 #3 项,即 String hello world
b6 => invokevirtual 预备调用成员方法,哪个方法呢?
00 04 引用常量池中 #4 项,即Method java/io/PrintStream.println:(Ljava/lang/String;)V
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 1389 d939c65ba536eb81d1a5c61d99be 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 #2 = Fieldref #21.#22 #3 = String #23 #4 = Methodref #24.#25 #5 = Class #26 #6 = Class #27 #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 #21 = Class #28 #22 = NameAndType #29:#30 #23 = Utf8 hello world #24 = Class #31 #25 = NameAndType #32:#33 #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 Code: stack =2 , locals=1 , args_size=1 0 : getstatic #2 3 : ldc #3 5 : invokevirtual #4 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"
图解方法执行流程
原始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 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 0 f9e41fb2a7334a69c89d2661540f4f1 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 #2 = Class #26 #3 = Integer 32768 #4 = Fieldref #27.#28 #5 = Methodref #29.#30 #6 = Class #31 #7 = Class #32 #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 #26 = Utf8 java/lang/Short #27 = Class #33 #28 = NameAndType #34:#35 #29 = Class #36 #30 = NameAndType #37:#38 #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 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 5 : istore_2 6 : iload_1 7 : iload_2 8 : iadd 9 : istore_3 10 : getstatic #4 13 : iload_3 14 : invokevirtual #5 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"
常量池载入运行时常量池
方法字节码载入方法区
main线程开始运行、分配栈帧内存
stack=2, locals=4
操作数栈的深度为2,也就是说,在执行该方法时,最多可以将两个值压入栈中进行操作。
包含四个局部变量
执行引擎开始执行字节码
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
iload_2
将局部变量表slot 2的值加载到操作数栈中
iadd
从操作数栈顶部弹出两个int类型的数值,将这两个数值相加,并将其结果压入操作数栈顶部;
istore_3
将操作数栈顶部数据弹出,存入局部变量表slot 3
getstatic #4
从常量池加载#4静态字段到操作数栈
iload_3
将局部变量表slot 3的值加载到操作数栈中
invokevirtual #5
找到常量池#5项
定位到方法区
生成新的栈帧(分配locals、stack等)
传递参数、执行新栈帧中的字节码
执行完毕,弹出栈帧
清除main操作数栈内容
return
分析 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 5 bc962752b10ca4b57350ca9814ec5b0 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 #2 = Fieldref #23.#24 #3 = Methodref #25.#26 #4 = Class #27 #5 = Class #28 #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 #23 = Class #29 #24 = NameAndType #30:#31 #25 = Class #32 #26 = NameAndType #33:#34 #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 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 21 : iload_1 22 : invokevirtual #3 25 : getstatic #2 28 : iload_2 29 : invokevirtual #3 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_122 : invokevirtual #3 25 : getstatic #2 28 : iload_229 : invokevirtual #3 32 : return
那么最终的结果a = 11
,b = 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
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
循环控制指令
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
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
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_211 : iinc 2, 114 : istore_2
构造方法
<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 5: bipush 20 7: putstatic 10: bipush 30 12: putstatic 15: return
编译器会按照从上至下的顺序,收集所有的static静态代码块和静态成员赋值的代码,合并成一个特殊的方法<cinit>()V
<cinit>()V
方法会在类加载的初始化阶段被调用
<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 4: aload_0 5: ldc 7: putfield 10: aload_0 11: bipush 20 // <- 20 13: putfield 16: aload_0 17: bipush 10 // <- 10 19: putfield 22: aload_0 23: ldc 25: putfield 28: aload_0 // ------------------------------ 29: aload_1 // <- slot 1 (a) "s3" | 30: putfield 33: aload_0 | 34: iload_2 // <- slot 2 (b) 30 | 35: putfield 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 3: dup 4: invokespecial 7: astore_1 8: aload_1 9: invokespecial 12: aload_1 13: invokespecial 16: aload_1 17: invokevirtual 20: aload_1 21: pop 22: invokestatic 25: invokestatic 28: return
new #2是创建Demo_29对象,给对象分配内存,执行成功会将对象引用
压入操作数栈
dup是赋值操作数栈顶的内容,本例为对象引用
。那为什么需要两份引用呢?
一个是要配合invokespecial
调用该对象的构造方法"<init>:()V"
,会消耗掉栈顶一个引用
另一个要配合astore_1
赋值给局部变量
final方法、私有方法、构造方法,都是由invokespecial指令来调用,属于静态绑定
普通成员方法是由invokevirtual调用,属于动态绑定,即支持多态
成员方法与静态方法调用的另一个区别是,执行方法前是否需要对象引用
比较有意思的是,执行demo.test4()时,是通过对象引用
调用的静态方法,可以看到在调用前执行了pop指令,把对象引用从操作数栈弹掉了,因为静态方法不需要对象引用来掉,通过这种方式,反而会增加两步无用的字节码指令
1 2 3 20 : aload_121 : pop22 : invokestatic
多态的原理
原始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;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("想吃小鱼干" ); } }
运行代码
会停在System.in.read()方法上(当然你也可以直接打断点),运行jps命令获取进程id
运行HSDB工具
1 java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB
查找某个对象
打开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. 执行方法的字节码
异常处理
try-catch
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
的位置
多个catch块的情况
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块
multi-catch的情况
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 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
的行号也相同
finally
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 2 : bipush 10 4 : istore_1 5 : bipush 30 7 : istore_1 8 : goto 27 11 : astore_2 12 : bipush 20 14 : istore_1 15 : bipush 30 17 : istore_1 18 : goto 27 21 : astore_3 22 : bipush 30 24 : istore_1 25 : aload_3 26 : athrow 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的面试题
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 ; } } }
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 2 : istore_0 3 : bipush 20 5 : ireturn 6 : astore_1 7 : bipush 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 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
那个异常被吞掉了
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 2 : istore_0 3 : iload_0 4 : istore_1 5 : bipush 20 7 : istore_0 8 : iload_1 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 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代码块中出现了异常,还能正确的执行解锁操作呢?下面就从字节码的角度来分析一下底层原理
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 3 : dup 4 : invokespecial #1 7 : astore_1 8 : aload_1 9 : dup 10 : astore_2 11 : monitorenter 12 : getstatic #3 15 : ldc #4 17 : invokevirtual #5 20 : aload_2 21 : monitorexit 22 : goto 30 25 : astore_3 26 : aload_2 27 : monitorexit 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 {}
1 2 3 4 5 6 7 public class Candy1 { public Candy1 () { super (); } }
自动拆装箱
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 ); Integer x = list.get(0 ); } }
所以在取值时,编译器真正生成的字节码中,还需要额外做一个类型转换的操作
1 2 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 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 3 : dup 4 : invokespecial #3 7 : astore_1 8 : aload_1 9 : bipush 10 11 : invokestatic #4 14 : invokeinterface #5 , 2 19 : pop 20 : aload_1 21 : iconst_0 22 : invokeinterface #6 , 2 27 : checkcast #7 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循环
1 2 3 4 5 6 7 8 public class Candy05 { public static void main (String[] args) { int [] array = {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 : if (str.equals("hello" )) { x = 0 ; } break ; case 113318802 : 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是为了防止哈希冲突,例如BM
和C.
这两个字符串的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 : 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枚举
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 { static class $MAP { 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 ; } } }
枚举类
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}; } 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类的两个枚举值,它们被定义为静态常量。
除此之外,还有一个私有的、final
的Sex
类型数组$VALUES
,它用于存储Sex类的所有枚举值。在类的静态块中,$VALUES
数组被初始化为一个包含MALE
和FEMALE
的数组。
构造函数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 = e1; throw e1; } finally { if (is != null ) { if (t != null ) { try { is.close(); } catch (Throwable e2) { t.addSuppressed(e2); } } else { 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 3 4 5 6 7 8 9 10 11 12 class A { public Number m () { return 1 ; } } class B extends A { @Override 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 synthetic bridge Number m () { return m(); } }
其中的桥接方法比较特殊,仅对Java虚拟机课件,并且与原来的public Integer m()没有命名冲突
匿名内部类
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有
_java_mirror:Java的类镜像,例如对String来说,就是String.class,作用是把klass暴露给Java使用
_super:父类
_fields:成员变量
_methods:方法
_constants:常量池
_class_loader:类加载器
_vtable:需方发表
_itable:接口方法表
如果这个类还有父类没有加载,先加载父类
加载和链接可能是交替运行的
instanceKlass这样的元数据
是存储在方法区(1.8后是在元空间内),但_java_mirror是存储在堆中
可以通过HSDB工具查看
链接
验证
验证类是否符合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)
准备
为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 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 5 : new #3 8 : dup 9 : invokespecial #1 12 : putstatic #4 15 : return LineNumberTable: line 7 : 0 line 10 : 5
变量a和b都是静态变量,但是只有变量b被赋予了初始值10,赋值操作在初始化阶段体现,也就是在 static 块中实现
变量c和d都被声明为静态final变量,它们的值在编译时就已经确定了,分别是20和"Hello",并且在字节码中使用了 ConstantValue 指令来指定这些常量的值。
变量e也是静态final变量,但它是一个引用类型变量,因此在初始化阶段才会被赋值,也就是在 static 块中实现。
解析
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(); Class<?> c = classloader.loadClass("com.demo.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保证这个类的构造方法
的线程安全
发生的时机:总的来说,类的初始化是懒惰的
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 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 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
默认的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" ); } }
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" ); } }
不会导致类初始化的情况
访问类的 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" ); } }
调用类对象.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
类加载器的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" ); } }
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" ); } }
练习
从字节码分析,使用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 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 5 : putstatic #3 8 : getstatic #4 11 : ldc #5 13 : invokevirtual #6 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 (); } public static Singleton getInstance () { return LazyHolder.INSTANCE; } }
以上的实现特点是:
懒惰实例化
初始化时的线程安全是有保障的
类加载器
名称
加载哪的类
说明
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。
扩展类加载器
1 2 3 4 5 public class Tmp { static { System.out.println("classpath Tmp init" ); } }
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%)
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)) { 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) { } if (c == null ) { long t1 = System.nanoTime(); c = findClass(name); 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 { if (parent != null ) { c = parent.loadClass(name, false ); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { 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 ; } AccessController.doPrivileged(new PrivilegedAction <Void>() { public Void run () { ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); Iterator<Driver> driversIterator = loadedDrivers.iterator(); try { while (driversIterator.hasNext()) { driversIterator.next(); } } catch (Throwable t) { } return null ; } }); println("DriverManager.initialize: jdbc.drivers = " + 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 (); } 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 (); } }
自定义类加载器
先来思考一下:什么时候需要自定义类加载器
自定义类加载器可用于加载非 Classpath 路径中的类文件,例如外部配置文件夹、网络资源或其他自定义路径。这种需求在一些动态扩展或插件化的场景中比较常见。
在应用程序中使用的类可以通过接口来使用,而不是直接引用类。这种做法可以减少应用程序之间的依赖,从而提高代码的灵活性和可维护性。同时,这种做法也使得框架的设计更加清晰和可扩展。
在Tomcat容器中,每个Web应用程序都使用自己的类加载器,从而避免了不同Web应用程序之间的类冲突问题。
步骤
继承ClassLoader类
遵从双亲委派机制,重写findClass方法
注意不要重写loadClass方法,否则不会走双亲委派机制
读取类文件的字节码
调用父类的defineClass方法来加载类
使用者调用类加载器的loadClass方法
示例
准备一个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(); } }
自定义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); } } }
调用自定义的类加载器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(); } }
运行期优化
即时编译
分层编译(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个层次
0层
:解释执行(Interpreter)
在0层,JVM使用解释器来直接解释Java字节码,并执行程序。这种方式简单但效率较低,因为解释器需要逐条解释字节码指令,并执行它们,每次执行时都需要对字节码进行解析
1层
:使用C1即时编译器编译执行(不带profilling)
在1层,JVM会使用即时编译器(JIT)将Java字节码编译成本地机器码,然后直接执行机器码。这种方式相比于解释器,可以提供更高的执行速度。C1即时编译器适合编译执行热点代码,即被频繁执行的代码
2层
:使用C1即时编译器编译执行(带基本的profilling)
在2层,JVM会收集一些基本的执行状态数据,即profilling。例如方法的调用次数、循环的回边次数等,然后根据这些数据来决定哪些代码块需要被编译执行。这种方式可以更加精确地编译热点代码,从而提高程序的执行速度
3层
:使用C1即时编译器编译执行(带完全的profilling)
在3层,JVM会收集更加详细的执行状态数据,例如内联调用的次数、方法的参数类型等,以便更好地优化代码。这种方式可以进一步提高程序的执行速度,但同时也会增加编译的开销
4层
:使用C2即时编译器编译执行
在4层,JVM会使用更高级别的即时编译器(C2)来对代码进行优化,包括对循环、分支和递归等结构的优化。C2编译器的编译时间比C1场,但编译出来的代码执行速度更快。
profilling是指在运行过程中手机一些程序执行的状态数据,例如方法的调用次数
、循环的回边次数
即时编译器(JIT)和解释器的区别
解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
JIT是将一些字节码编译为机器码,并存入Code Cache
,下次遇到相同的代码,直接执行
,无需再次编译
解释器是将字节码解释为针对所有平台都通用的机器码
JIT会根据平台类型,生成平台特定的机器码
对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码
,而是采用解释器执行
的方法运行;
另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码
,以达到理想的运行速度
。
执行效率上简单比较一下:Interceptor < C1 < C2
上面代码中最后的耗时都在300
附近,这是C2即时编译器做了逃逸分析,因为上面的代码中,我们仅仅是创建
了Object对象,而并没有使用
它,也就是没有逃逸
出当前作用域
在进行逃逸分析时,JVM会分析对象是否可能被线程外的代码引用,如果对象不会逃逸出当前方法的作用域,那么JVM会将对象的分配优化为栈上分配
,从而避免了堆内存的分配和垃圾回收的压力。
将对象分配在栈上的优点
是:
快速分配和回收
:栈内存的分配和回收都非常快,比堆内存要快得多。如果对象可以在栈上分配,那么它的分配和回收都可以更快,从而提高程序的性能。
减少垃圾回收
:在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 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 UnitsBenchmark1 .test1 thrpt 5 2830851 .513 ± 68534 .850 ops/sBenchmark1 .test2 thrpt 5 2844317 .417 ± 8097 .137 ops/sBenchmark1 .test3 thrpt 5 2849940 .840 ± 7190 .091 ops/s
1 2 3 4 Benchmark Mode Cnt Score Error UnitsBenchmark1 .test1 thrpt 5 313751 .710 ± 34874 .348 ops/sBenchmark1 .test2 thrpt 5 388759 .125 ± 90456 .387 ops/sBenchmark1 .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 () { for (int i = 0 ; i < elements.length; i++) { sum += elements[i]; } }
如果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
方法获取名为foo
的Method
对象,以便之后进行反射调用。
接着使用循环调用反射方法,循环次数从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次调用。