黑马JVM课程——类加载(三)

类文件结构

参考文档:Chapter 4. The class File Format (oracle.com)

以一个简单的 HelloWorld..java 为例:

1
2
3
4
5
public class HelloWorld {  
public static void main(String[] args) {
System.out.println("hello world");
}
}
  • 执行 javac -parameters -d . HelloWorld.java 编译
    • 使用 -parameters 参数,可以在编译只保留方法参数的名称信息
  • 编译之后的class文件以16进制打开如下所示:

    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
    [root@localhost ~]# od -t xC HelloWorld.class
    0000000 ca fe ba be 00 00 00 34 00 1f 0a 00 06 00 11 09
    0000020 00 12 00 13 08 00 14 0a 00 15 00 16 07 00 17 07
    0000040 00 18 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 04 6d 61 69
    0000120 6e 01 00 16 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67
    0000140 2f 53 74 72 69 6e 67 3b 29 56 01 00 10 4d 65 74
    0000160 68 6f 64 50 61 72 61 6d 65 74 65 72 73 01 00 04
    0000200 61 72 67 73 01 00 0a 53 6f 75 72 63 65 46 69 6c
    0000220 65 01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64 2e 6a
    0000240 61 76 61 0c 00 07 00 08 07 00 19 0c 00 1a 00 1b
    0000260 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64 07 00
    0000300 1c 0c 00 1d 00 1e 01 00 21 63 6f 6d 2f 65 78 61
    0000320 6d 70 6c 65 2f 64 65 6d 6f 31 2f 74 65 73 74 2f
    0000340 48 65 6c 6c 6f 57 6f 72 6c 64 01 00 10 6a 61 76
    0000360 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74 01 00 10
    0000400 6a 61 76 61 2f 6c 61 6e 67 2f 53 79 73 74 65 6d
    0000420 01 00 03 6f 75 74 01 00 15 4c 6a 61 76 61 2f 69
    0000440 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d 3b 01 00
    0000460 13 6a 61 76 61 2f 69 6f 2f 50 72 69 6e 74 53 74
    0000500 72 65 61 6d 01 00 07 70 72 69 6e 74 6c 6e 01 00
    0000520 15 28 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72
    0000540 69 6e 67 3b 29 56 00 21 00 05 00 06 00 00 00 00
    0000560 00 02 00 01 00 07 00 08 00 01 00 09 00 00 00 1d
    0000600 00 01 00 01 00 00 00 05 2a b7 00 01 b1 00 00 00
    0000620 01 00 0a 00 00 00 06 00 01 00 00 00 07 00 09 00
    0000640 0b 00 0c 00 02 00 09 00 00 00 25 00 02 00 01 00
    0000660 00 00 09 b2 00 02 12 03 b6 00 04 b1 00 00 00 01
    0000700 00 0a 00 00 00 0a 00 02 00 00 00 09 00 08 00 0a
    0000720 00 0d 00 00 00 05 01 00 0e 00 00 00 01 00 0f 00
    0000740 00 00 02 00 10
    0000745
  • 使用javap -v HelloWorld.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
    66
    67
    68
    Classfile /E:/project/java/demo2/src/main/java/com/example/demo1/test/com/example/demo1/test/HelloWorld.class
    Last modified 2022-8-20; size 485 bytes
    MD5 checksum fceb30d0e6ac220c6aad48757e3affde
    Compiled from "HelloWorld.java"
    public class com.example.demo1.test.HelloWorld
    minor version: 0
    major version: 52
    flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
    #1 = Methodref #6.#17 // java/lang/Object."<init>":()V
    #2 = Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream;
    #3 = String #20 // hello world
    #4 = Methodref #21.#22 // java/io/PrintStream.println:(Ljava/lang/String;)V
    #5 = Class #23 // com/example/demo1/test/HelloWorld
    #6 = Class #24 // java/lang/Object
    #7 = Utf8 <init>
    #8 = Utf8 ()V
    #9 = Utf8 Code
    #10 = Utf8 LineNumberTable
    #11 = Utf8 main
    #12 = Utf8 ([Ljava/lang/String;)V
    #13 = Utf8 MethodParameters
    #14 = Utf8 args
    #15 = Utf8 SourceFile
    #16 = Utf8 HelloWorld.java
    #17 = NameAndType #7:#8 // "<init>":()V
    #18 = Class #25 // java/lang/System
    #19 = NameAndType #26:#27 // out:Ljava/io/PrintStream;
    #20 = Utf8 hello world
    #21 = Class #28 // java/io/PrintStream
    #22 = NameAndType #29:#30 // println:(Ljava/lang/String;)V
    #23 = Utf8 com/example/demo1/test/HelloWorld
    #24 = Utf8 java/lang/Object
    #25 = Utf8 java/lang/System
    #26 = Utf8 out
    #27 = Utf8 Ljava/io/PrintStream;
    #28 = Utf8 java/io/PrintStream
    #29 = Utf8 println
    #30 = Utf8 (Ljava/lang/String;)V
    {
    public com.example.demo1.test.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
    4: return
    LineNumberTable:
    line 7: 0

    public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
    stack=2, locals=1, args_size=1
    0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
    3: ldc #3 // String hello world
    5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    8: return
    LineNumberTable:
    line 9: 0
    line 10: 8
    MethodParameters:
    Name Flags
    args
    }
    SourceFile: "HelloWorld.java"
  • 根据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;//小版本号
    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];//类中附加信息
    }

魔数

0-3字节,表示他是否是【class】类型的文件
0000000 ca fe ba be 00 00 00 34 00 1f 0a 00 06 00 11 09

版本

4-7字节,表示类的版本,00 34(16进制,10进制为52,jdk8)表示jdk8
0-3字节,表示他是否是【class】类型的文件
0000000 ca fe ba be 00 00 00 34 00 1f 0a 00 06 00 11 09

常量池

常量池信息对照表

Content Type Value
CONSTANT_Class 7
CONSTANT_Fielaref 9
CONSTANT_Methidref 10
CONSTANT_InterfaceMetodref 11
CONSTANT_String 8
CONSTANT_Integer 3
CONSTANT_Float 4
CONSTANT_Long 5
CONSTANT_Double 6
CONSTANT_NameAndType 12
CONSTANT_Utf8 1
CONSTANT_MethodHandle 15
CONSTANT_MethodType 16
CONSTANT_InvokeDynamic 18

常量池长度

8~9字节,表示常量池的长度,00 1f(31)表示常量池有#1-#30项,注意,#0项不计入,也没有值
0000000 ca fe ba be 00 00 00 34 00 1f 0a 00 06 00 11 09

#1

第#1项,0a(10)表示一个Method信息,00 06和 00 11(17) 表示它引用了常量池中#6和#17项来获得这个方法的【所属类】和【方法名】
0000000 ca fe ba be 00 00 00 34 00 1f 0a 00 06 00 11 09

#2

第#2项,09 表示一个Field信息,向后数4个字节,00 12(18)和 00 13(19) 表示它引用了常量池中#18和#19项来获得这个方法的【所属类】和【成员变量名】
0000000 ca fe ba be 00 00 00 34 00 1f 0a 00 06 00 11 09
0000020 00 12 00 13 08 00 14 0a 00 15 00 16 07 00 17 07

#3

第#3项,08 表示一个字符串常量名称,向后数2个字节,00 14(20), 表示它引用了常量池中#20项
0000020 00 12 00 13 08 00 14 0a 00 15 00 16 07 00 17 07

#4

第#4项,0a(10)表示一个Method信息,00 15(21)和 00 16(22) 表示它引用了常量池中#21和#22项来获得这个方法的【所属类】和【方法名】
0000020 00 12 00 13 08 00 14 0a 00 15 00 16 07 00 17 07

#5

第#5项,07 表示一个Class信息,00 17(23) 表示它引用了常量池中#23项
0000020 00 12 00 13 08 00 14 0a 00 15 00 16 07 00 17 07

#6

第#6项,07 表示一个Class信息,00 18(24) 表示它引用了常量池中#24项
0000020 00 12 00 13 08 00 14 0a 00 15 00 16 07 00 17 07
0000040 00 18 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 18 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 18 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 04 6d 61 69

#11

第#11项,01 表示一个UTF8串,00 04表示字符串的长度,【6d 61 69 6e】=【main】
0000100 75 6d 62 65 72 54 61 62 6c 65 01 00 04 6d 61 69
0000120 6e 01 00 16 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67

#12

第#12项,01 表示一个UTF8串,00 16(22)表示字符串的长度,【28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 29 56】=【([Ljava/lang/String;)V】
0000120 6e 01 00 16 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67
0000140 2f 53 74 72 69 6e 67 3b 29 56 01 00 10 4d 65 74

#13

第#13项,01 表示一个UTF8串,00 10(16)表示字符串的长度,【4d 65 74 68 6f 64 50 61 72 61 6d 65 74 65 72 73】=【MethodParameters】
0000140 2f 53 74 72 69 6e 67 3b 29 56 01 00 10 4d 65 74
0000160 68 6f 64 50 61 72 61 6d 65 74 65 72 73 01 00 04

#14

第#14项,01 表示一个UTF8串,00 04(16)表示字符串的长度,【61 72 67 73】=【args】
0000160 68 6f 64 50 61 72 61 6d 65 74 65 72 73 01 00 04
0000200 61 72 67 73 01 00 0a 53 6f 75 72 63 65 46 69 6c

#15

第#15项,01 表示一个UTF8串,00 0a(10)表示字符串的长度,【53 6f 75 72 63 65 46 69 6c 65】=【SourceFile】
0000200 61 72 67 73 01 00 0a 53 6f 75 72 63 65 46 69 6c
0000220 65 01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64 2e 6a

#16

第#16项,01 表示一个UTF8串,00 0f(15)表示字符串的长度,【48 65 6c 6c 6f 57 6f 72 6c 64 2e 6a 61 76 61】=【HelloWorld.java】
0000220 65 01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64 2e 6a
0000240 61 76 61 0c 00 07 00 08 07 00 19 0c 00 1a 00 1b

#17

第#17项,0c(12) 表示一个NameAndType【名+类型】,00 07和00 08表示引用了常量池中的#7和#8项【<init>:()V】
0000240 61 76 61 0c 00 07 00 08 07 00 19 0c 00 1a 00 1b

#18

第#18项,07 表示一个Class信息,00 19(25) 表示它引用了常量池中#25项
0000240 61 76 61 0c 00 07 00 08 07 00 19 0c 00 1a 00 1b

#19

第#19项,0c(12) 表示一个NameAndType【名+类型】,00 1a(26)和00 1b(27)表示引用了常量池中的#26和#27项
0000240 61 76 61 0c 00 07 00 08 07 00 19 0c 00 1a 00 1b

#20

第#20项,01 表示一个UTF8串,00 0b(11)表示字符串的长度,【68 65 6c 6c 6f 20 77 6f 72 6c 64】=【hello world】
0000260 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64 07 00

#21

第#21项,07 表示一个Class信息,00 1c(28) 表示它引用了常量池中#28项
0000260 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64 07 00
0000300 1c 0c 00 1d 00 1e 01 00 21 63 6f 6d 2f 65 78 61

#22

第#22项,0c(12) 表示一个NameAndType【名+类型】,00 1d(29)和00 1e(30)表示引用了常量池中的#29和#30项
0000300 1c 0c 00 1d 00 1e 01 00 21 63 6f 6d 2f 65 78 61

#23

第#23项,01 表示一个UTF8串,00 21(33)表示字符串的长度,【63 6f 6d 2f 65 78 61 6d 70 6c 65 2f 64 65 6d 6f 31 2f 74 65 73 74 2f 48 65 6c 6c 6f 57 6f 72 6c 64】=【com/example/demo1/test/HelloWorld】
0000300 1c 0c 00 1d 00 1e 01 00 21 63 6f 6d 2f 65 78 61
0000320 6d 70 6c 65 2f 64 65 6d 6f 31 2f 74 65 73 74 2f
0000340 48 65 6c 6c 6f 57 6f 72 6c 64 01 00 10 6a 61 76

#24

第#24项,01 表示一个UTF8串,00 10(16)表示字符串的长度,【6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74】=【java/lang/Object】
0000340 48 65 6c 6c 6f 57 6f 72 6c 64 01 00 10 6a 61 76
0000360 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74 01 00 10

#25

第#25项,01 表示一个UTF8串,00 10(16)表示字符串的长度,【6a 61 76 61 2f 6c 61 6e 67 2f 53 79 73 74 65 6d】=【java/lang/System】
0000360 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74 01 00 10
0000400 6a 61 76 61 2f 6c 61 6e 67 2f 53 79 73 74 65 6d

#26

第#26项,01 表示一个UTF8串,00 03(16)表示字符串的长度,【6f 75 74】=【out】
0000420 01 00 03 6f 75 74 01 00 15 4c 6a 61 76 61 2f 69

#27

第#27项,01 表示一个UTF8串,00 15(21)表示字符串的长度,【4c 6a 61 76 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d 3b】=【Ljava/io/PrintStream;】
0000420 01 00 03 6f 75 74 01 00 15 4c 6a 61 76 61 2f 69
0000440 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d 3b 01 00

#28

第#28项,01 表示一个UTF8串,00 13(19)表示字符串的长度,【6a 61 76 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d】=【java/io/PrintStream】
0000440 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d 3b 01 00
0000460 13 6a 61 76 61 2f 69 6f 2f 50 72 69 6e 74 53 74
0000500 72 65 61 6d 01 00 07 70 72 69 6e 74 6c 6e 01 00

#29

第#29项,01 表示一个UTF8串,00 07表示字符串的长度,【70 72 69 6e 74 6c 6e】=【println】
0000500 72 65 61 6d 01 00 07 70 72 69 6e 74 6c 6e 01 00

#30

第#30项,01 表示一个UTF8串,00 15(21)表示字符串的长度,【28 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 29 56】=【(Ljava/lang/String;)V】
0000500 72 65 61 6d 01 00 07 70 72 69 6e 74 6c 6e 01 00
0000520 15 28 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72
0000540 69 6e 67 3b 29 56 00 21 00 05 00 06 00 00 00 00

访问标识与继承信息

Flag Name Value Interpretation
ACC_PUBLIC 0x0001 表示是公开的,public
ACC_FINAL 0x0010 表示是final的
ACC_SUPER 0x0020 表示是一个类
ACC_INTERFACE 0x0200 表示是一个接口
ACC_ABSTRACT 0x0400 表示是一个抽象的
ACC_SYNTHETIC 0x1000 表示是人工合成的,不是原生的
ACC_ANNOTATION 0x2000 表示是一个注解
ACC_ENUM 0X4000 表示是一个枚举

修饰符

00 21 表示(0x0001)+(0x0020),表示该class是一个公共的类
0000540 69 6e 67 3b 29 56 00 21 00 05 00 06 00 00 00 00

全限定名

00 05 表示全限定名的位置,常量池中#5项的位置【com/example/demo1/test/HelloWorld】
0000540 69 6e 67 3b 29 56 00 21 00 05 00 06 00 00 00 00

父类全限定名

00 06 表示父类全限定名的位置,常量池中#6项的位置【java/lang/Object】
0000540 69 6e 67 3b 29 56 00 21 00 05 00 06 00 00 00 00

接口信息

00 00 表示该类继承的接口数量,现在表示没有继承接口,如果继承了接口,后面就会跟随接口的描述信息
0000540 69 6e 67 3b 29 56 00 21 00 05 00 06 00 00 00 00

成员变量信息(Field)

00 00 表示该类的成员变量的数量,现在表示没有成员变量,如果有成员变量,后面会跟随成员变量的信息
0000540 69 6e 67 3b 29 56 00 21 00 05 00 06 00 00 00 00

FieldType Type Interpretation
B byte 字节类型
C char 字符类型
D double 双精度浮点类型
F float 单精度浮点类型
I int 整型
J long 长整型
L ClassName; reference 引用类型,例如【Ljava/io/PrintStream;】
S short 短整数
Z boolean 布尔类型
[ reference 数组类型

方法信息(Method)

方法数量

00 02 表示该类中的方法数量,当前类里面是有2个方法,【无参构造,main方法】
0000560 00 02 00 01 00 07 00 08 00 01 00 09 00 00 00 1d

方法组成

一个方法由访问修饰符,名称,参数描述,方法属性数量,方法属性组成

第一个方法的详细解析

  • 蓝色部分的 00 01 表示方法的访问修饰,当前表示是一个公共方法
  • 绿色部分的 00 07 表示要查询常量池中的第#7项得到方法的名字【<init>】
  • 黄色部分的 00 08 表示要查询常量池中的#8项获得方法的参数和返回值信息【()V】(无参,返回值是void)
  • 紫色部分的 00 01 表示方法的属性数量,当前是1个
  • 剩下的红色部分代表方法属性
    • 00 09表示方法的属性名称,需要查询常量池中的#9 【Code】
    • 00 00 00 1d 表示该属性占据的字节码长度是29
    • 00 01 表示【操作数栈】最大深度
    • 00 01 表示【局部变量表】最大槽(slot)数
    • 00 00 00 05 表示【Code】属性内的(代码)字节码长度
    • 2a b7 00 01 b1 方法内的具体代码
    • 00 00 00 01 表示方法细节属性数量(code属性的有些子属性)
    • 00 0a 表示引用了常量池中的#10项,发现是【LineNumberTable】属性(将字节码行号和java代码行号对应)
      • 00 00 00 06 【LineNumberTable】属性的字节码总长度,6
      • 00 01 【LineNumberTable】属性的长度(有几对)
      • 00 00 00 07,00 00 表示【字节码】行号,00 04 表示【java源码】行号

0000560 00 02 00 01 00 07 00 08 00 01 00 09 00 00 00 1d
0000600 00 01 00 01 00 00 00 05 2a b7 00 01 b1 00 00 00
0000620 01 00 0a 00 00 00 06 00 01 00 00 00 07 00 09 00

第二个方法的详细解析

  • 蓝色部分的 00 09 表示方法的访问修饰,相当于是(08+01)08是静态,当前表示是一个静态公共方法
  • 绿色部分的 00 0b (11)表示要查询常量池中的第#11项得到方法的名字【main】
  • 黄色部分的 00 0c (12) 表示要查询常量池中的#12项获得方法的参数和返回值信息【([Ljava/lang/String;)V】(如此是引用数组类型,返回值是void)
  • 紫色部分的 00 02 表示方法的属性数量,当前是2个
  • 剩下的红色部分代表方法属性
    • 00 09表示方法的第一个属性名称,需要查询常量池中的#9 【Code】
      • 00 00 00 25 表示该属性的占据的字节码长度是37
      • 00 02 表示【操作数栈】最大深度2
      • 00 01 表示【局部变量表】最大槽(slot)数
      • 00 00 00 09 表示【Code】属性内的(代码)字节码长度
      • b2 00 02 12 03 b6 00 04 b1 方法内的具体代码
      • 00 00 00 01 表示方法细节属性数量(code属性的有些子属性)
      • 00 0a 表示引用了常量池中的#10项,发现是【LineNumberTable】属性(将字节码行号和java代码行号对应)
        • 00 00 00 0a 【LineNumberTable】属性的字节码总长度,10
        • 00 02 【LineNumberTable】属性的长度(有几对)
        • 00 00 00 09,00 00 表示【字节码】行号,00 09 表示【java源码】行号
        • 00 08 00 0a,00 08 表示【字节码】行号,00 0a (10) 表示【java源码】行号
    • 00 0d(13)表示方法的第一个属性名称,需要查询常量池中的#13 【MethodParameters】
      • 00 00 00 05 表示该属性的占据的字节码长度是5
      • 01 参数的数量
      • 00 0e (14) 参数的名字,引用了常量池中的#14项,【args】
      • 00 00 访问修饰符

0000620 01 00 0a 00 00 00 06 00 01 00 00 00 07 00 09 00
0000640 0b 00 0c 00 02 00 09 00 00 00 25 00 02 00 01 00
0000660 00 00 09 b2 00 02 12 03 b6 00 04 b1 00 00 00 01
0000700 00 0a 00 00 00 0a 00 02 00 00 00 09 00 08 00 0a
0000720 00 0d 00 00 00 05 01 00 0e 00 00 00 01 00 0f 00

附加属性

  • 00 01 表示附加属性的数量
  • 00 0f(15) 表示引用了常量池中的#15项【SourceFile】
  • 00 00 00 02 表示此属性的长度
  • 00 10(16) 表示引用了常量池#16项,【HelloWorld.java】

0000720 00 0d 00 00 00 05 01 00 0e 00 00 00 01 00 0f 00
0000740 00 00 02 00 10
0000745

字节码指令

入门

可参考文章:

上面的方法编译后,留下了两组字节码指令没有讲解

构造方法中的5个字节码指令

1
2a b7 00 01 b1
  • 2a => aload_0,从局部变量0中装载引用类型值入栈,及this,做为下面的 invokespecial 构造方法调用的参数
  • b7 => invokespecial ,编译时方法绑定调用方法(预备调用构造方法),调用那个方法?
  • 00 01 引用常量池中 #1 项,即【Method java/lang/Object.<init>:()V】
  • b1 =>return, viod函数返回

main方法中的9个字节码指令

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 => invokvirtual,运行时方法绑定调用方法(预备调用成员方法),调用那个方法?
  • 00 04 引用常量池中#4项,即【Method java/io/PrintStream.println:(Ljava/lang/String;)V】
  • b1 =>return, viod函数返回

javap 工具

  • jdk提供了javap工具来进行反编译,具体操作方式,见上面内容

图解方法执行流程

原始java代码

1
2
3
4
5
6
7
8
public class Test1 {  
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
Classfile /E:/project/java/demo2/src/main/java/com/example/demo1/test/com/example/demo1/test/Test1.class
Last modified 2022-8-21; size 488 bytes
MD5 checksum b8a87685b4f2ed97f03db1e25823271e
Compiled from "Test1.java"
public class com.example.demo1.test.Test1
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#18 // java/lang/Object."<init>":()V
#2 = Class #19 // java/lang/Short
#3 = Integer 32768
#4 = Fieldref #20.#21 // java/lang/System.out:Ljava/io/PrintStream;
#5 = Methodref #22.#23 // java/io/PrintStream.println:(I)V
#6 = Class #24 // com/example/demo1/test/Test1
#7 = Class #25 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 main
#13 = Utf8 ([Ljava/lang/String;)V
#14 = Utf8 MethodParameters
#15 = Utf8 args
#16 = Utf8 SourceFile
#17 = Utf8 Test1.java
#18 = NameAndType #8:#9 // "<init>":()V
#19 = Utf8 java/lang/Short
#20 = Class #26 // java/lang/System
#21 = NameAndType #27:#28 // out:Ljava/io/PrintStream;
#22 = Class #29 // java/io/PrintStream
#23 = NameAndType #30:#31 // println:(I)V
#24 = Utf8 com/example/demo1/test/Test1
#25 = Utf8 java/lang/Object
#26 = Utf8 java/lang/System
#27 = Utf8 out
#28 = Utf8 Ljava/io/PrintStream;
#29 = Utf8 java/io/PrintStream
#30 = Utf8 println
#31 = Utf8 (I)V
{
public com.example.demo1.test.Test1();
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 7: 0

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 9: 0
line 10: 3
line 11: 6
line 12: 10
line 13: 17
MethodParameters:
Name Flags
args
}
SourceFile: "Test1.java"

常量池载入运行时常量池

一些比较小的数字,不会存储在常量池中,而是跟方法的字节码存在一起,比如int a = 10; 中10就是存在字节码中的
b的值32768则是存在常量池中的

运行时常量池图示

方法字节码载入方法区

方法区域中的字节码,会载入到方法区

方法区字节码载入图示

main 线程开始运行,分配栈帧内存

(stack=2, locals=4)
主线程分配栈帧内存图示

执行引擎开始执行字节码

bipush 10

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

java代码运行图示1

istore_1

  • 将操作数栈顶数据弹出,存入局部变量表的slot_1

java代码运行图示2

  • 实现给a变量赋值
    java代码运行图示3

ldc #32

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

java代码运行图示4

istore_2

  • 将操作数栈顶数据弹出,放入局部变量表slot_2
    java代码运行图示5
  • 数据读取之后入图所示:
    java代码运行图示6

    iload_1

  • 将局部变量表中1位置的变量读取到操作数栈
    java代码运行图示7

iload_2

  • 将局部变量表中2位置的变量读取到操作数栈
    java代码运行图示8

iadd

  • 将栈顶两int类型整数相加,结果入栈
    java代码运行图示9

java代码运行图示10

istore_3

  • 将操作数栈中的数据取出来存到局部变量表3的位置
    java代码运行图示11
  • 执行之后:
    java代码运行图示12

getstatic #4

  • 从常量池中找到对应类型,然后从堆中找到该类型的对象(或者创建对象),然后将引用放入操作数栈

java代码运行图示13

  • 注意:放入操作数栈的并不是对象,而是对象的引用
    java代码运行图示14

iload_3

  • 将局部变量表3号位置的数据放入操作数栈
    java代码运行图示15

java代码运行图示16

invokevirtual #5

  • 找到常量池中#5项
  • 定位到方法区 【java/io/PrintStream.println:(I)V】 方法
  • 生成新的栈帧,(分配localsstack等)
  • 传递参数,执行新栈帧中的字节码
    java代码运行图示17
  • 执行完毕,弹出栈帧
  • 清除main操作数栈内容
    java代码运行图示18

return

  • 完成main方法调用,弹出main栈帧
  • 程序结束

练习-分析 i++

目的:从字节码角度分析 a++ 相关题目
源码:

1
2
3
4
5
6
7
8
9
10
11
/**  
* 从字节码角度分析 a++ 相关题目
*/
public class Test2 {
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
Classfile /E:/project/java/demo2/src/main/java/com/example/demo1/test/com/example/demo1/test/com/example/demo1/test/com/example/demo1/test/Test2.class
Last modified 2022-8-21; size 500 bytes
MD5 checksum 540a55d259f5e8971e2576618da6c5b7
Compiled from "Test2.java"
public class com.example.demo1.test.com.example.demo1.test.Test2
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#16 // java/lang/Object."<init>":()V
#2 = Fieldref #17.#18 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #19.#20 // java/io/PrintStream.println:(I)V
#4 = Class #21 // com/example/demo1/test/com/example/demo1/test/Test2
#5 = Class #22 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 main
#11 = Utf8 ([Ljava/lang/String;)V
#12 = Utf8 MethodParameters
#13 = Utf8 args
#14 = Utf8 SourceFile
#15 = Utf8 Test2.java
#16 = NameAndType #6:#7 // "<init>":()V
#17 = Class #23 // java/lang/System
#18 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(I)V
#21 = Utf8 com/example/demo1/test/com/example/demo1/test/Test2
#22 = Utf8 java/lang/Object
#23 = Utf8 java/lang/System
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Utf8 java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (I)V
{
public com.example.demo1.test.com.example.demo1.test.Test2();
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 11: 0

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 13: 0
line 14: 3
line 15: 18
line 16: 25
line 17: 32
MethodParameters:
Name Flags
args
}
SourceFile: "Test2.java"

分析:

  • 注意 iinc 指令是直接在局部变量slot上进行运算
  • a++++a 的区别是先执行iload还是先执行iinc
    • a++ 先执行 iload,然后再iinc自增
    • ++a 先执行iinc自增,再执行iload

bipush 10

  • 把10先放到操作数栈

istore_1

  • 把操作数栈中栈顶的数据弹出,存到局部变量表一号桶位

iload_1

  • 把局部变量表一号槽位中的数据读取到操作数栈

iinc 1,1

  • 执行 a++
  • 局部变量表一号槽位数据自增1
  • 前面的1表示局部变量表的槽位,后面的1表示自增1
  • 该操作直接在局部变量表中完成,并不会影响操作数栈中的数据

iinc 1,1

  • 执行++a
  • 局部变量表一号槽位自增1(不会影响到操作数栈)

iload_1

  • 把局部变量表一号槽位的数据加载到操作数栈(12加载到操作数栈)

iadd

  • 将栈顶两int类型整数相加,结果入栈

iload_1

  • a--iloadiinc
  • 先将12放入操作数栈

iinc 1,-1

  • 执行 a--
  • 局部变量表一号槽位增加-1,也就是减1

    iadd

  • 将栈顶两int类型整数相加,结果入栈

istore_2

  • 把操作数栈顶的数据弹出到局部变了表2号槽位

条件判断指令

详情见《Java字节码指令大全》<1.11 控制流指令——条件跳转指令>
Java字节码指令大全 | YS (gitee.io)

说明:

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

循环控制指令

  • 其实循环也是依靠上面的判断指令完成的
    源码:
    1
    2
    3
    4
    5
    6
    7
    8
    public class Test {  
    public static void main(String[] args) {
    int a = 0;
    while (a < 10){
    a++;
    }
    }
    }

    编译后的字节码如下:
    iconst_0 准备一个0值入栈
    istore_1 将操作数栈顶的数据,也就是刚刚的0值,存放到局部变量表1号位
    iload_1 将局部变量表1号位,也就是刚刚的0值存入操作数栈
    bipush 准备一个数字10入栈
    if_icmpge 若栈顶两int类型值前大于等于后则跳转,也就说0所代表的变量大于等于10的时候,可以跳转到第14行字节码
    iinc 局部变量表1号位自增1,也就是0值的位置,现在为1
    goto 跳转到第2行字节码执行
    然后会继续循环判断
    以此实现循环,当满足条件的时候就会跳转到14行return

字节码:

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

构造方法

<cinit>()V

1
2
3
4
5
6
7
8
9
public class Test2 {  
static int a = 10;
static {
a = 20;
}
static {
a = 30;
}
}

静态变量的赋值操作和静态代码块的内容,最终会被合并成一个操作
编译器会按从上向下的顺序,收集所有static几台代码块和静态成员赋值的代码,合并为一个特殊的方法
<cinit>()V:

1
2
3
4
5
6
7
0: bipush        10
2: putstatic #2 // Field a:I
5: bipush 20
7: putstatic #2 // Field a:I
10: bipush 30
12: putstatic #2 // Field a:I
15: return

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

<init>()V

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test3 {  
private String a = "S1";
{
b = 20;
}
private int b = 10;
{
a = "S2";
}
public Test3(String a, int b) {
this.a = a;
this.b = b;
}
public static void main(String[] args) {
Test3 test3 = new Test3("S3",30);
System.out.println(test3.a);
System.out.println(test3.b);
}
}

编译器会按从上到下的顺序,收集所有{}代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在最后面
先调用父类Object类的<init>方法
然后给this.a赋值S1
然后给this.b赋值为20(并不是先赋值初始化的10,而是处于上面的代码块的20
然后给this.b赋值为10
this.a赋值为S2
然后下面的是原本构造方法里面的代码
将局部变量表1号位的数据赋值给this.a
将局部变量表2号位的数据赋值给this.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
public com.example.demo1.test1.Test3(java.lang.String, int);
descriptor: (Ljava/lang/String;I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2 // String S1
7: putfield #3 // Field a:Ljava/lang/String;
10: aload_0
11: bipush 20
13: putfield #4 // Field b:I
16: aload_0
17: bipush 10
19: putfield #4 // Field b:I
22: aload_0
23: ldc #5 // String S2
25: putfield #3 // Field a:Ljava/lang/String;
28: aload_0 //---------------------------------------------------
29: aload_1
30: putfield #3 // Field a:Ljava/lang/String;
33: aload_0
34: iload_2
35: putfield #4 // Field b:I
38: return
LineNumberTable:
line 17: 0
line 8: 4
line 10: 10
line 12: 16
line 14: 22
line 18: 28
line 19: 33
line 20: 38
MethodParameters:
Name Flags
a
b

方法调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Test4 {  
public Test4() {}
private void test1(){}
private final void test2(){}
public void test3(){}
private static void test4(){}
public static void main(String[] args) {
Test4 t = new Test4();//调用构造方法
t.test1();//调用私有普通方法
t.test2();//调用final方法
t.test3();//调用公有普通方法
t.test4();//通过对象调用静态方法
Test4.test4();//通过类名调用静态方法
}
}

通过字节码可以看出,调用构造方法私有普通方法以及final方法,都是使用的invokespecial指令
调用普通公共方法使用的是invokevirtual指令
调用静态方法使用的是invokestatic指令

invokespecial:编译时方法绑定调用方法
invokestatic:调用静态方法
invokevirtual:运行时方法绑定调用方法
invokespecialinvokestatic属于静态绑定,在字节码生成的时候,就已经知道需要找到那个类的那个方法了,性能相比之下会更高一点
invokevirtual并不能在生成字节码的时候就知道调用的是那个类的,因为java多态的原因,方法可能会被重写,有可能调用的是父类的方法,也可能调用的是子类额方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0: new           #2                  // class com/example/demo1/test1/Test4
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

多态的原理

示例代码如下,内容很简单
创建一个Animal的抽象类,他有两个子类实现了他的抽象方法eat()
之后在test()方法中以父类去调用eat()main()方法中传入不同的实现

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

public static void main(String[] args) throws IOException {
test(new Dog());
test(new Cat());
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("吃鱼");
}
}

分析

  • 在方法启动的时候需要添加以下参数,禁止虚拟机自动压缩指针
    • -XX:-UseCompressedOops -XX:-UseCompressedClassPointers
  • 使用System.in.read();暂停程序,然后使用jps命令获取进程id
  • 运行HSDB工具
    • 进入JDK安装目录,执行下面的命令:
    • java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB
    • 进入图形界面输入程序的进程id
  • 打开图形界面,点击file再点击Attach to HotSpot...,之后输入进程id,就会连上当前进程
  • 然后点击Tools,再点击Find Object by Query,之后输入查询语句,类似于sql语句
    • select + 别名 + from + 需要查询的类的全限定名 + 别名
    • 如:select d from com.example.demo1.test1.Dog d
  • 如下图所示,可以在Inspector框里面查看对象的相关信息,不过这些并不是对象的真正信息
  • 点击标记1处,会弹出标记2处的框,然后使用mem指令查看对象头信息的地址信息
  • mem命令后面跟随的是标记3处的地址信息,后面的2表示要查看几个mark word
  • 在标记1处敲击回车,就会显示出类型的指针
  • 然后再在Tools处打开一个Inspector框,使用刚刚查出来的类型指针查询
  • 如标记2处,就是对象在虚拟机中完整的类型表示
  • 类的多态方法都存在一个vtable表中,也就是最后一行箭头所指的位置
  • vtable的和当前类型指针的偏移量是1b8,所以只需要以16进制将当前指针地址加上1b8就可以得到vtable的地址
    • 0x000000001c7c3ca0 + 1b8 = 0x000000001c7c3e58
  • 下面就是执行mem指令,从上面第二张图可以看到vtable_len=6,所以查6个mark
    • eme 0x000000001c7c3e58 6
  • 如图所示,查询出对应的work,一共有六个,Dog类对应的多态方法
  • 可以通过Class Browser查询Dog类,对应一下这六个方法
  • 从图中可以看出,0x000000001c453c48 对应的是cat()方法
  • 如图所示,0x000000001c453768 对应的是Dog类的父类Animal类的toString()方法
  • toString()方法也是一个多态方法,只不过Dog并没有重写这个方法,所以使用的是父类的方法
  • 剩下的四个方法均来自Animal的父类Object,对应关系如下图所示
  • finalize():该方法并没有在AnimalDog类中重写,所以使用的是所有类的超类Object类的
  • equals():该方法同样没有重写,使用的也是Object中的
  • hashCode():同样使用的是Object中的
  • clone():同样使用的是Object中的
  • 虚方法表是在类加载链接阶段就会诞生的,所以就是说在类链接阶段,就已经配置好了每个方法需要调用的具体实现

小结

执行invokevirtual指令时

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

异常梳理

try-catch

1
2
3
4
5
6
7
8
9
10
 public class Test6 {  
public static void main(String[] args) {
int a = 0;
try {
a = 10;
}catch (Exception e){
a = 20;
}
}
}

从下面的字节码中可以看出:

0值入栈,然后放入局部变量表中,然后数值10入栈,然后赋值给局部变量表1号位,然后goto到12行
如果没有异常,那么执行到5行后就会跳转到12行结束

不过可以看出,多出来了一个Exception table的结构,[from,to)是前闭后开的检测范围,一旦这个范围内的字节码执行出现了异常,则通过type匹配异常类型,如果一致则进入target所指示的行号
8行的字节码指令astore_2是将异常对象引用存入局部变量表的2号位

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
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
LineNumberTable:
line 9: 0
line 11: 2
line 14: 5
line 12: 8
line 13: 9
line 15: 12
LocalVariableTable:
Start Length Slot Name Signature
9 3 2 e Ljava/lang/Exception;
0 13 0 args [Ljava/lang/String;
2 11 1 a I
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 8
locals = [ class "[Ljava/lang/String;", int ]
stack = [ class java/lang/Exception ]
frame_type = 3 /* same */
MethodParameters:
Name Flags
args

多个catch块的情况(single-catch)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Test7 {  
public static void main(String[] args) {
int a = 0;
try {
a = 10;
}catch (ArithmeticException e){
a = 30;
}catch (NullPointerException e){
a = 40;
}catch (Exception e){
a = 50;
}
}
}

从编译出的字节码中可以看出,多个catch一样都是监控的第2到5行字节码(不包含5)
区别在于后面的type,会根据type匹配到不同的异常之后,进行不同的跳转行(根据target判断跳转到哪一行)

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
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=1
0: iconst_0
1: istore_1
2: bipush 10
4: istore_1
5: goto 26
8: astore_2
9: bipush 30
11: istore_1
12: goto 26
15: astore_2
16: bipush 40
18: istore_1
19: goto 26
22: astore_2
23: bipush 50
25: istore_1
26: return
Exception table:
from to target type
2 5 8 Class java/lang/ArithmeticException
2 5 15 Class java/lang/NullPointerException
2 5 22 Class java/lang/Exception
LineNumberTable:
line 9: 0
line 11: 2
line 18: 5
line 12: 8
line 13: 9
line 18: 12
line 14: 15
line 15: 16
line 18: 19
line 16: 22
line 17: 23
line 19: 26
LocalVariableTable:
Start Length Slot Name Signature
9 3 2 e Ljava/lang/ArithmeticException;
16 3 2 e Ljava/lang/NullPointerException;
frame_type = 70 /* same_locals_1_stack_item */
stack = [ class java/lang/Exception ]
frame_type = 3 /* same */
MethodParameters:
Name Flags
args

multi-catch的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test8 {  
public static void main(String[] args) {
try {
Method test = Test8.class.getMethod("test");
test.invoke(null);
} catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public static void test(){
System.out.println("OK");
}
}

编译后的字节码如下所示:
三个异常监控的范围都是0-22,然后根据type去进行匹配,如果匹配到了就会跳转到25
和多个catch块的形式唯一的区别就是匹配到异常之后跳转的位置是相同的

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 void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: ldc #2 // class com/example/demo1/test1/Test8
2: ldc #3 // String test
4: iconst_0
5: anewarray #4 // class java/lang/Class
8: invokevirtual #5 // Method java/lang/Class.getMethod:(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;
11: astore_1
12: aload_1
13: aconst_null
14: iconst_0
15: anewarray #6 // class java/lang/Object
18: invokevirtual #7 // Method java/lang/reflect/Method.invoke:(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
21: pop
22: goto 35
25: astore_1
26: new #11 // class java/lang/RuntimeException
29: dup
30: aload_1
31: invokespecial #12 // Method java/lang/RuntimeException."<init>":(Ljava/lang/Throwable;)V
34: athrow
35: return
Exception table:
from to target type
0 22 25 Class java/lang/reflect/InvocationTargetException
0 22 25 Class java/lang/NoSuchMethodException
0 22 25 Class java/lang/IllegalAccessException
LineNumberTable:
line 13: 0
line 14: 12
line 17: 22
line 15: 25
line 16: 26
line 18: 35
LocalVariableTable:
Start Length Slot Name Signature
12 10 1 test Ljava/lang/reflect/Method;
3: ldc #14 // String OK
5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 20: 0
line 21: 8

finally

1
2
3
4
5
6
7
8
9
10
11
12
public class Test9 {  
public static void main(String[] args) {
int a = 0;
try {
a = 10;
}catch (Exception e){
a = 20;
}finally {
a = 30;
}
}
}

从下面的字节码中可以看出,finally的代码块其实就是在编译的时候复制到每一个代码块的最后位置
如,try,最后两行的字节码就是finally里面的内容
catch里面的也是一样
不过从异常表中可以看出,多了两个any的异常监控,any表示所有异常
any监控2-5行,则是为了防止有超出Exception的异常发生,比如Exception的父类或者兄弟类
监控11-15行,这时为了防止执行Exceptioncatch块的时候再发生异常
通过字节码来控制,保证每一块代码块跳转之前都会执行finally里面的异常,即便是发生了未知的异常,也能够跳转到finally的代码块中执行
整个字节码一共有三个分支:

1.try分支
2.catch分支
3.catch没有匹配到的异常的分支(最后会把异常再上抛)

如果在finallyreturn,将会吞掉代码块中的异常

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
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 -> a
2: bipush 10 // try ---------------------------
4: istore_1 // 10 -> a |
5: bipush 30 // finally |
7: istore_1 // 30 -> a |
8: goto 27 // return-------------------------
11: astore_2 // catch Exception -> e-----------
12: bipush 20 // |
14: istore_1 // 20 -> a |
15: bipush 30 // finally |
17: istore_1 // 30 -> a |
18: goto 27 // return-------------------------
21: astore_3 // catch any -> slot 3 -----------
22: bipush 30 // finally |
24: istore_1 // 30 -> a |
25: aload_3 // <- slot 3 |
26: athrow // throw--------------------------
27: return
Exception table:
from to target type
2 5 11 Class java/lang/Exception
2 5 21 any
11 15 21 any
LineNumberTable:
line 9: 0
line 11: 2
line 15: 5
line 16: 8
line 12: 11
frame_type = 73 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
frame_type = 5 /* same */
MethodParameters:
Name Flags
args

synchronized

1
2
3
4
5
6
7
8
public class Test {  
public static void main(String[] args) {
Object lock = new Object();
synchronized (lock){
System.out.println("OK");
}
}
}

字节码如下所示,其实和finally的字节码类似
会将一份普通代码分为两个分支,一个是正常结束的分支,一个是异常结束的分支,无论在哪个分支上,都会复制一份解锁的字节码,以保证加锁后肯定能够解锁

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
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
8: aload_1
9: dup
10: astore_2
11: monitorenter
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
21: monitorexit
22: goto 30
25: astore_3
26: aload_2
27: monitorexit
28: aload_3
29: athrow
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 25
locals = [ class "[Ljava/lang/String;", class java/lang/Object, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
MethodParameters:
Name Flags
args

编译期处理

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

默认构造器

1
2
public class Test1 {  
}
  • 编译成class之后:
    1
    2
    3
    4
    5
    6
    7
    public class Test1 {  
    //这个无参构造器就是编译器帮助我们加上的
    public Test1() {
    //即调用父类Object的无参构造,调用java/lang/Object.<init>:()V
    super();
    }
    }

自动拆装箱

  • 这个特性是JDk5开始加入的,代码片段1:
    1
    2
    3
    4
    5
    6
    public class Test2 {  
    public static void main(String[] args) {
    Integer x = 1;
    int y = x;
    }
    }
  • 在JDK5之前按照上面这样写是无法进行编译的,必须写成下面代码片段2的形式:
    1
    2
    3
    4
    5
    6
    public class Test2 {  
    public static void main(String[] args) {
    Integer x = Integer.valueOf(1);
    int y = x.intValue();
    }
    }

泛型集合取值

  • 泛型也是在JDK5开始加入的特性,但java在编译泛型代码后,会执行泛型擦除的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都是当做了Object类型来处理的
1
2
3
4
5
6
7
public class Test3 {  
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(10);//实际调用的List.add(Object o)
Integer x = list.get(0);//实际调用的是Object o = List.get(int index);
}
}
  • 所以在取值时,编译器真正生成的字节码中,还要额外做一个类型转换操作

    1
    2
    //需要将 Object 转换成 Integer
    Integer x = (Integer)list.get(0);
  • 如果前面的x变量类型修改为int基本类型,那么最终生成的字节码是:

    1
    2
    //需要将 Object 转换成 Integer ,并执行拆箱操作
    int x = (Integer)list.get(0).intValue();
  • 下面是编译之后的字节码

    • 第11行对应的就是list.add(10),因为list要放的是包装类型,10是基本类型,所以先调用了Integer.valueOf()方法把10转换成了Integer类型
    • 第14行可以看到,其实调用的就是list.add(Object o)方法,并没有泛型的类型
    • 第22行也可以看到,获取数据调用的List.get()的返回值也是一个Object类型
    • 第27行,checkcast表示强转,将getObject类型强转成Integer类型
  • 泛型擦除掉的是字节码上的泛型信息,可以看到LocalVariableTypeTable仍然保留了方法参数泛型的信息

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
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 12: 0
line 13: 8
line 14: 20
line 15: 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;>;
MethodParameters:
Name Flags
args

  • 方法上的泛型信息,可以通过反射
    1
    public Set<Integer> test(List<String> list, Map<Integer,Object> map){return null;}
1
2
3
4
5
6
7
8
9
10
11
12
13
    Method test = Test4.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]);
}
}
}
}

输出结果:

1
2
3
4
5
原始类型 - interface java.util.List
泛型参数【0】-class java.lang.String
原始类型 - interface java.util.Map
泛型参数【0】-class java.lang.Integer
泛型参数【1】-class java.lang.Object

可变参数

  • 可变参数也是JDk5开始加入的新特性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Test5 {  
    public static void foo(String... args){
    String[] array = args;//直接赋值
    System.out.println(array);
    }
    public static void main(String[] args) {
    foo("hello","world");
    }
    }
  • 可变参数String... args其实就是一个String[] args,从代码中的赋值语句就可以看出来

  • 同样java编译会在编译期间将上述代码转换为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Test5 {  
    public static void foo(String[] args){
    String[] array = args;//直接赋值
    System.out.println(array);
    }
    public static void main(String[] args) {
    foo(new String[]{"hello","world"});
    }
    }

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

foreach循环

  • 在JDK5开始引入的语法糖,数组循环:

    1
    2
    3
    4
    5
    6
    7
    8
    public class Test6 {  
    public static void main(String[] args) {
    int[] array = {1,2,3,4,5};//数组赋值的简化写法,也是语法糖
    for (int i : array) {
    System.out.println(i);
    }
    }
    }

    会被编译转换为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Test6 {  
    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 Test7 {  
    public static void main(String[] args) {
    List<Integer> list = Arrays.asList(1,2,3,4,5);
    for (Integer i : list) {
    System.out.println(i);
    }
    }
    }

实际会被编译器转换为迭代器调用:

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

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

switch 字符串

  • 从JDK7开始,switch可以用于字符串和枚举类,这个功能其实也是语法糖,例如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class Test8 {  
    public static void choose(String str) {
    switch (str) {
    case "hello":
    System.out.println("H");
    break; case "world":
    System.out.println("W");
    break;
    }
    }
    }

注意:
switch配合String和枚举使用时,变量不能为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
28
public class Test8 {  
public static void main(String[] args) {
System.out.println( "hello".hashCode());
System.out.println( "world".hashCode());
}
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;
}
break;
}
switch (x) {
case 0:
System.out.println("H");
break;
case 1:
System.out.println("W");
break; }
}
}

从上面的代码可以看到,字符串使用switch其实是被拆成了两个整数的switch来执行的,第一遍是根据字符串的hashCodeequals将字符串转换成对应byte类型,第二遍才是利用byte执行进行比较
为什么第一遍时必须比较hashCode又利用equals比较呢?

hashCode是为了提高效率,减少可能的比较,而equals是为了防止hashCode冲突,例如BMC.,这两个字符串的hashCode都是2123

switch 枚举

switch 枚举的例子,原始代码:

1
2
3
4
5
6
7
8
9
10
public class Test {  
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
public class Test1 {  
/**
* 定义一个合成类(仅JVM使用,对我们不可见)
* 用来映射枚举的 ordinal 与数组元素的关系
* 枚举的 ordinal 表示枚举对象的序号,从0开始
* 即 MALE 的 ordinal()=0,FEMALE 的 ordinal() = 1
*/ //com/example/demo1/test3/Test$1.$SwitchMap$com$example$demo1$test3$Sex
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; }
}
}

枚举类

JDK7新增了枚举类,以前面的性别枚举为例:

1
2
3
public enum Sex {  
MALE,FEMALE;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public final class Sex extends Enum<Sex> {  
public static final Sex MALE;
public static final Sex FEMALE;
public static final Sex[] $VALUES;
static {
MALE = new Sex("MALE",0);
FEMALE = new Sex("FEMALE",1);
$VALUES = new Sex[]{MALE,FEMALE};
}
private Sex1(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);
}
}

try-with-resources

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

1
2
3
4
5
try(资源变量 = 创建资源对象){

}catch(){

}

其中资源对象需要实现AutoCloseable接口,例如 InputStreamOutputStream等接口都实现了AutoCloseable接口,使用try-with-resources可以不写finally语句块,编译器会帮助生成关闭资源代码,例如:

1
2
3
4
5
6
7
8
9
public class Test2 {  
public static void main(String[] args) {
try (InputStream inputStream = new FileInputStream("")){
System.out.println(inputStream);
} 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
public class Test2 {  
public static void main(String[] args) {
try {
InputStream inputStream = new FileInputStream("");
Throwable var2 = null;
try {
System.out.println(inputStream);
} catch (Throwable var12) {
// var2就是我们业务代码块运行的时候可能出现的异常
var2 = var12;
throw var12;
} finally {
//判断资源不为空,否则不用关闭
if (inputStream != null) {
//如果我们的业务代码块有异常
if (var2 != null) {
try {
inputStream.close();
} catch (Throwable var11) {
//如果我们的业务代码块有异常,并且关闭资源的时候也出现了异常,会作为被压制异常添加(保证两个异常都不会丢失)
var2.addSuppressed(var11);
}
} else {
//如果我们的业务代码块没有异常,直接关闭资源,如果关闭资源出现了异常,那么该异常就是最后的异常,可能会被catch捕捉
inputStream.close();
}
}
}
} catch (IOException var14) {
var14.printStackTrace();
}
}
}

为什么要设计一个addSuppressed(Throwable e)(添加被压制异常)的方法呢?
是为了防止异常信息丢失,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test3 {  
public static void main(String[] args) {
try (MyResource myResource = new MyResource()){
int a = 1/0;
} catch (Exception e) {
e.printStackTrace();
}
}
}
class MyResource implements AutoCloseable{

@Override
public void close() throws Exception {
throw new Exception("close 异常");
}
}

输出:
除0异常和手动抛出来的close异常都未丢失

1
2
3
4
5
java.lang.ArithmeticException: / by zero
at com.example.demo1.test3.Test3.main(Test3.java:10)
Suppressed: java.lang.Exception: close 异常
at com.example.demo1.test3.MyResource.close(Test3.java:20)
at com.example.demo1.test3.Test3.main(Test3.java:11)

方法重写时的桥接方法

一般来说方法重写分两种情况:

  • 父类和子类返回值完全一致
  • 子类返回值可以是父类返回值的子类
1
2
3
4
5
6
7
8
9
10
11
class A extends B{  
@Override
public Integer m() {
return 1;
}
}
class B{
public Number m(){
return 2;
}
}

对于子类,java编译器会做如下处理:

1
2
3
4
5
6
7
8
9
class A extends B{  
public Integer m() {
return 1;
}
//此方法才是真正重写了父类,public Number m()方法
public synthetic bridge Number m(){
return m();
}
}

其中桥接方法比较特殊,仅对java虚拟机可见,并且与原来的public Number m()没有命名冲突,可以使用下面的反射代码来验证:

1
2
3
4
5
public static void main(String[] args) {  
for (Method method : A.class.getDeclaredMethods()) {
System.out.println(method);
}
}

输出:

1
2
public java.lang.Integer com.example.demo1.test3.A.m()
public java.lang.Number com.example.demo1.test3.A.m()

匿名内部类

源代码:

1
2
3
4
5
6
7
8
9
10
public class Test5 {  
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
//额外生成的一个类
final class Test5$1 implements Runnable {
Test5$1() {
}

public void run() {
System.out.println("OK");
}
}
1
2
3
4
5
public class Test5 {  
public static void main(String[] args) {
Runnable runnable = new Test5$1();
}
}

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

1
2
3
4
5
6
7
8
9
10
public class Test5 {  
public static void test(final int a) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("OK"+a);
}
};
}
}

转换后代码:

1
2
3
4
5
6
7
8
9
10
11
//额外生成的代码
final class Test5$1 implements Runnable {
int val$a;
Test5$1(final int val$a) {
this.val$a = val$a;
}

public void run() {
System.out.println("OK" + this.val$a);
}
}
1
2
3
4
5
public class Test5 {  
public static void test(final int a) {
Runnable runnable = new Test5$1(a);
}
}

注意:
这同时解释了为什么匿名内部类引用局部变量时,局部变量必须是final的原因
因为在创建Test5$1对象时,将a的值赋值给了Test5$1对象的val$a属性,所以a不应该再发生变化了,如果变化,那么val$a属性就没有机会再跟着一起变化

类加载阶段

加载

  • 将类的字节码载入方法区中,内部采用C++instanceKlass描述java类,他的重要field有:
    • _java_mirrorjava的类镜像,例如对String来说,String.class,作用就是把klass暴露给java使用
    • _super 即父类
    • _fields即成员变量
    • _methods 即方法
    • _constants 即常量池
    • _class_loader 即类加载器
    • _vtable 虚方法表
    • _itable 接口方法表
  • 如果这个类还有父类没有加载,先加载父类
  • 加载和链接可能是交替运行的

注意:

  • instanceKlass 这样的【元数据】是存储在方法区的(jdk1.8之后的元空间),但_java_mirror是存储在堆中的
  • 可以通过前面介绍的HSDB工具查看
  • 类加载之后,会在堆中保留一份信息,也就是类对象,类对象中会有地址指向元空间中的元数据,元空间中_java_mirror也会有指针指向堆中的元数据,可以通过类对象创建实例对象,创建出来的实例对象中,对象头里面会保留类对象信息
    本地内存&JVM内存结构

链接

验证

  • 验证类是否符合JVM规范,安全性检查
    用可以编辑16进制文件的工具修改编译后的class内容,然后在控制台运行:
    (报错内容为magic魔数部分值不正确)
    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 com/example/demo1/test3/Test6
    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:338)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)

准备

  • static变量分配空间,设置默认值
    • static 变量在JDK7之前存储于instanceKlass末尾,从JDK7开始,存储于_java_mirror末尾
    • static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
    • 如果static变量是final的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
    • 如果static变量是final的,但属于引用类型,那么赋值也会在初始化阶段完成

解析

  • 将常量池中的符号引用解析为直接引用
  • 符号引用替换为直接引用的理解:
    • 符号引用:比如一个类中引用了其他类,但是JVM不知道实际引用的其他类地址会在哪,所以就会用符号引用代表,等到解析的时候,再根据唯一符号引用去找到其他类的地址,不止是其他类的代表,符号引用也可以代表方法,字段等,主要注意的是符号引用与虚拟机布局无关,引用的目标不一定已经加载到内存
    • 直接引用:直接引用与虚拟机布局有关,如果使用直接引用,那么引用的目标一定已经加载到内存中
    • 符号引用要转换成直接引用才有效,这也说明直接引用的效率要比符号引用高,那么为什么要用符号引用呢?这时 因为类加载之前,javac会将源代码编译成class文件,这个时候javac是不知道被编译的类中所引用的类、方法或者变量他们的引用地址在哪,所以只能用符号引用来代表,当然,符号引用也是要准备JVM虚拟机规范的

初始化

<cinit>()V 方法

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

发生的时机

类的初始化是懒惰的,并不会在程序启动的时候全部初始化,而是等需要的时候初始化

会导致初始化的情况:

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

不会导致初始化的情况

  • 访问类的static final静态常量(基本类型和字符串类型)不会触初始化
  • 类对象.class不会触发初始化
  • 创建该类的数组不会触发初始化
  • 类加载器的 loadClass 方法
  • Class.forName()方法的参数2为false
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
public class Test {  
static {
//main方法所在的类,总是会被初始化
System.out.println("main init");
}
public static void main(String[] args) throws ClassNotFoundException {
//1.静态常量不会触发初始化
// System.out.println(B.b);
//2.类对象.class不会触发初始化
// System.out.println(B.class);
// 3.创建该类的数组,不会导致初始化
// System.out.println(new B[0]);
//4.不会初始化类B,但是会加载 B、A
// ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
// classLoader.loadClass("com.example.demo1.test5.B");
//5. 不会初始化类B,但是会加载 B、A
// ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
// Class.forName("com.example.demo1.test5.B",false,classLoader);


// 1.首次访问这个类的静态变量或者静态方法时,会初始化
// System.out.println(A.a);
//2. 子类初始化,如果父类还没有初始化,会引发
// System.out.println(B.c);
//3.子类访问父类静态变量,只会初始化父类
// System.out.println(B.a);
//4.会初始化类B,并且先初始化类A
// Class.forName("com.example.demo1.test5.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");
}
}

类加载器

以JDK8为例:

名称 描述 加载目录 说明
Bootstrap ClassLoader 启动类加载器 JAVA_HOME/jre/lib 无法直接访问
Extension ClassLoader 扩展类加载器 JAVA_HOME/jre/lib/ext 上级为Bootstrap,显示为null
Application ClassLoader 应用程序类加载器 classpath 上级为Extension
自定义类加载器 自定义 自定义类加载器 上级为Application

启动类加载器

使用Bootstrap类加载器加载类:

1
2
3
4
5
public class F {  
static {
System.out.println("F init");
}
}

执行:

1
2
3
4
5
6
public class Test1 {  
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("com.example.demo1.test5.F");
System.out.println(aClass.getClassLoader());
}
}

输出:

1
2
3
$ java -Xbootclasspath/a:. com.example.demo1.test5.Test1
F init
null
  • -Xbootclasspath 表示设置 bootclasspath
  • 其中/a:. 表示将当前目录追加至 bootclasspath 之后
  • 可以使用这个方法替换核心类
    • java -Xbootclasspath:<new bootclasspath> 完全替换路径
    • java -Xbootclasspath/a:<向后追加路径>
    • java -Xbootclasspath/p:<向前追加路径>

扩展类加载器

源代码:

1
2
3
4
5
public class G {  
static {
System.out.println("classpath G init");
}
}

执行

1
2
3
4
5
6
public class Test2 {  
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("com.example.demo1.test5.G");
System.out.println(aClass.getClassLoader());
}
}

输出:

1
2
classpath G init
sun.misc.Launcher$AppClassLoader@18b4aac2
  • 可以看到,默认情况下使用的是 AppClassLoader 类加载器
  • 那么怎么才能使用扩展类加载器加载呢?
    • 将对应的类,打成jar包,放到扩展目录下

源代码:

1
2
3
4
5
public class G {  
static {
System.out.println("ext G init");
}
}

更改输出内容,打包放入扩展目录,可以根据输出内容的不同,判断是加载的哪一个类
使用命令打包 jar -cvf my.jar com/example/demo1/test5/G.class
将jar包放入JAVA_HOME/jre/lib/ext目录下后,改回类中的输出内容,再次运行代码:

1
2
ext G init
sun.misc.Launcher$ExtClassLoader@69663380

从输出的内容可以看出,使用的是 ExtClassLoader 类加载器

双亲委派模式

所谓双亲委派,就是指调用类加载器的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
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) {
// 有上级的话,委派上级 loadClass
c = parent.loadClass(name, false);
} else {
// 如果上级为空(ExtClassLoader),则委派 BootstrapClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}

if (c == null) {
// 如果上一层找不到,调用 findClass方法(每个类加载器自己扩展)来加载
// 对于ExtClassLoader和AppClassLoader来说,如果上级找不到,会调用自己的findClass方法来寻找
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;
}
}

线程上下文类加载器(SPI)

1
2
String url = "jdbc:mysql://localhost:3306/test";
Connection conn = DriverManager.getConnection(url,username,password);

上面是获取数据库连接,并没有写去加载哪一个数据库驱动
不需要Class.forName("com.mysql.jdbc.Driver")来加载,也可以正确加载驱动

代码中并没有指定使用的是哪一个数据库驱动,是如何做到的呢?

源码:(代码中省略了部分代码,只保留了需要观察的部分)

1
2
3
4
5
6
7
8
9
public class DriverManager {
// 注册驱动集合
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
// 初始化驱动
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
}

从代码中可以看出,静态代码块中主要逻辑在 loadInitialDrivers();方法中,那我们来看一下 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
private static void loadInitialDrivers() {  
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// 1)使用 ServiceLoader 机制加载驱动, 即SPI
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});

println("DriverManager.initialize: jdbc.drivers = " + drivers);
//2) 使用 jdbc.drivers 定义的驱动名加载驱动
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
// 这里 ClassLoader.getSystemClassLoader() 就是应用程序类加载器
println("DriverManager.Initialize: loading " + aDriver);
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
  • 上面代码标记1处的ServiceLoader 就是大名鼎鼎的 Service Provider Interface (SPI)
  • 约定如下:
    • 在jar包的META-INF/service包下,以接口全限定名为文件名,文件内容是实现类名称

如图,mysql的驱动:

  • 这样就可以通过下面的方法使用:

    1
    2
    3
    4
    5
    ServiceLoader<接口类型> loader = ServiceLoader.load(接口类型.class);  
    Iterator<接口类型> iterator = loader.iterator();
    while (iterator.hasNext()){
    iterator.next();
    }
  • 接着看ServiceLoader.load()方法

    1
    2
    3
    4
    5
    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
    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
    }

自定义类加载器

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

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

实现步骤

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

如下所示,MyClassLoader 就是我们自定义的类加载器,可以到自定义的目录下进行类加载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Test {  
public static void main(String[] args) throws ClassNotFoundException {
MyClassLoader myClassLoader = new MyClassLoader();
Class<?> c1 = myClassLoader.loadClass("MapImpl1");
Class<?> c2 = myClassLoader.loadClass("MapImpl1");
System.out.println(c2 == c1);
}
}
class MyClassLoader extends ClassLoader{
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String path = "D:\\myclasspath\\"+name+".class";

try {
ByteArrayOutputStream os = new ByteArrayOutputStream();
Files.copy(Paths.get(path), os);
byte[] bytes = os.toByteArray();
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
throw new ClassNotFoundException("未找到对应的类文件",e);
}
}
}

运行期优化

即时编译

分层编译

运行下面的例子,可以看到,每次创建的一千个对象的时间,会从最开始的每次31000纳秒,到二百次之后降低到11900左右

1
2
3
4
5
6
7
8
9
10
11
12
public class Test {  
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.println(i+" "+(end-start));
}
}
}

原因是什么?

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

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

profiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的回放次数】等

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

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

对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;
另一方面,对于占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度;
执行效率上简单比较一下 Interpreter < C1 < c2
总的目标是发现热点代码(hotspot名称的由来),优化之

上面的一种优化手段称之为【逃逸分析】,检查新创建的对象是否逃逸。
可以使用-XX:-DoEscapeAnalysis关闭逃逸分析,再运行刚才的示例观察结果
如果开启逃逸分析,编译器发现新创建的对象在循环外面没有调用,那么就会选择不创建对象

方法内联

(Inlining)

1
2
3
public static int square(final int i) {  
return i * i;
}
1
System.out.println(square(9));

如果发现square()方法是热点方法,并且长度不太长,会进行内联,所谓的内联就是把方法内代码拷贝、黏贴到调用者的位置

1
System.out.println(9 * 9);

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

1
System.out.println(81);

可以使用参数打印内联信息-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining
从输出的结果可以看到,自定义的 square() 方法已经内联优化

也可以使用参数禁用内联-XX:CompileCommand=dontinline,*Test.square (*号表示所有包下的Test类中的square方法)

字段优化

JWH基准测试请参考 : OpenJDK: jmh

创建Maven工程,添加以下依赖:

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.35</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.35</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
//预热,预热2伦,每次间隔1毫秒  
@Warmup(iterations = 2,time = 1)
//进5伦测试,每次间隔1毫秒
@Measurement(iterations = 5,time = 1)
@State(Scope.Benchmark)
public class Test {
int[] elements = randomInts(1000);
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 options = new OptionsBuilder()
.include(Test.class.getSimpleName())
.forks(1).build();
new Runner(options).run();
}
}

上面代码运行之后,输出结果:
Score项为评分,可以看到,三种循环方式,评分并没有太大的差异

1
2
3
4
Benchmark    Mode  Cnt        Score        Error  Units
Test.test1 thrpt 5 4152019.656 ± 43675.816 ops/s
Test.test2 thrpt 5 4128473.418 ± 228612.603 ops/s
Test.test3 thrpt 5 4130624.821 ± 180536.674 ops/s

可以把doSum()方法上的注解参数修改为@CompilerControl(CompilerControl.Mode.DONT_INLINE),禁止方法内联
再次运行代码:
可以看出,得分比较与第一次,全都有了大幅度下降

1
2
3
4
Benchmark    Mode  Cnt       Score       Error  Units
Test.test1 thrpt 5 210418.245 ± 40126.738 ops/s
Test.test2 thrpt 5 627385.384 ± 27072.723 ops/s
Test.test3 thrpt 5 640138.597 ± 11598.502 ops/s

分析:
在刚才的示例中,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
doSum(elements[i]); // 1000 次取下标 i 的元素 <-- local
}
}

可以省去 1999Field读取操作
但如果doSum()方法没有内联,则不会有上面的优化

反射优化

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {  
public static void foo(){
System.out.println("foo...");
}
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {
Method foo = Test.class.getMethod("foo");
for (int i = 0; i <= 16; i++) {
System.out.printf("%d\t",i);
foo.invoke(null);
}
System.in.read();
}
}

foo.invoke()前面0-15次调用,使用的是 MethodAccessor 的 NativeMethodAccessorImpl 实现
之后会生成一个新的实现
下面的代码是 NativeMethodAccessorImpl 类删减之后的内容
可以看到,每次调用 this.numInvocations 都会加一,当大于ReflectionFactory.inflationThreshold()的结果时就会进入判断中的内容,而判断中会通过字节码技术,重新生成一个 MethodAccessorImpl 的实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
class NativeMethodAccessorImpl extends MethodAccessorImpl {  
......
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);
}
........
private static native Object invoke0(Method var0, Object var1, Object[] var2);
}

可以通过 阿里巴巴提供的工具包 arthas-boot 来查看通过字节码技术生成的类 下载路径: https://arthas.gitee.io/arthas-boot.jar
java -jar .\arthas-boot.jar
工具启动后会列出来对应的程序,主要选择自己想要查看的程序即可

然后使用 jad sun.reflect.GeneratedMethodAccessor1 命令,后面是需要查看的类的全限定名称
下面是自动生成的代码:

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
/*
* Decompiled with CFR.
*
* Could not load the following classes:
* com.example.demo1.test11.Test
*/
package sun.reflect;

import com.example.demo1.test11.Test;
import java.lang.reflect.InvocationTargetException;
import sun.reflect.MethodAccessorImpl;

public class GeneratedMethodAccessor1
extends MethodAccessorImpl {
/*
* Loose catch block
*/
public Object invoke(Object object, Object[] objectArray) throws InvocationTargetException {
//如果有参数,直接抛出非法参数异常
//会根据调用方法的实际情况来生成,如果方法有一个参数,则会限制为1个参数
//如果方法为普通方法,object为对象,会使用object.fangfa()调用
block4: {
if (objectArray == null || objectArray.length == 0) break block4;
throw new IllegalArgumentException();
}
try {
//可以看到,这里已经是使用直接调用了
Test.foo();
//没有返回值
return null;
}
catch (Throwable throwable) {
throw new InvocationTargetException(throwable);
}
catch (ClassCastException | NullPointerException runtimeException) {
throw new IllegalArgumentException(super.toString());
}
}
}

注意:
通过查看 ReflectionFactory 源码可以知道:

  • sun.reflect.noInflation 可以用来禁止膨胀(直接生成 sun.reflect.GeneratedMethodAccessor1,但是首次生成比较耗时,如果反射只使用一次,不划算)
  • sun.reflect.inflationThreshold 可以修改膨胀阀值