类文件结构

  • 一个简单的HelloWorld.java

    1
    2
    3
    4
    5
    6
    JAVA
    public class HelloWorld {
    public static void main(String[] args) {
    System.out.println("Hello, World!");
    }
    }
  • 编译后查看其二进制字节码文件的内容

    1
    2
    BASH
    od -t xC target/classes/com/demo/HelloWorld.class

    img

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    LESS
    ClassFile {
    u4 magic; // 魔数,用于标识文件类型
    u2 minor_version; // Java虚拟机的次版本号
    u2 major_version; // Java虚拟机的主版本号
    u2 constant_pool_count; // 常量池大小
    cp_info constant_pool[constant_pool_count-1]; // 常量池数组
    u2 access_flags; // 访问标识符,用于表示类或接口的访问控制
    u2 this_class; // 当前类或接口的索引
    u2 super_class; // 当前类的超类(父类)索引
    u2 interfaces_count; // 接口数量
    u2 interfaces[interfaces_count]; // 接口索引列表
    u2 fields_count; // 字段数量
    field_info fields[fields_count]; // 字段信息数组
    u2 methods_count; // 方法数量
    method_info methods[methods_count]; // 方法信息数组
    u2 attributes_count; // 类或接口的附加属性数量
    attribute_info attributes[attributes_count]; // 类或接口的附加属性信息数组
    }

魔数

  • 以下面的字节码文件,按顺序逐个进行分析

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    PLAINTEXT
    0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
    0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07
    0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29
    0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e
    0000100 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63
    0000120 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01
    0000140 00 04 74 68 69 73 01 00 1d 4c 63 6e 2f 69 74 63
    0000160 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c 6f
    0000200 57 6f 72 6c 64 3b 01 00 04 6d 61 69 6e 01 00 16
    0000220 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72
    0000240 69 6e 67 3b 29 56 01 00 04 61 72 67 73 01 00 13
    0000260 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69
    0000300 6e 67 3b 01 00 10 4d 65 74 68 6f 64 50 61 72 61
    0000320 6d 65 74 65 72 73 01 00 0a 53 6f 75 72 63 65 46
    0000340 69 6c 65 01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64
    0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e
    0000400 00 1f 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64
    0000420 07 00 20 0c 00 21 00 22 01 00 1b 63 6e 2f 69 74
    0000440 63 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c
    0000460 6f 57 6f 72 6c 64 01 00 10 6a 61 76 61 2f 6c 61
    0000500 6e 67 2f 4f 62 6a 65 63 74 01 00 10 6a 61 76 61
    0000520 2f 6c 61 6e 67 2f 53 79 73 74 65 6d 01 00 03 6f
    0000540 75 74 01 00 15 4c 6a 61 76 61 2f 69 6f 2f 50 72
    0000560 69 6e 74 53 74 72 65 61 6d 3b 01 00 13 6a 61 76
    0000600 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d
    0000620 01 00 07 70 72 69 6e 74 6c 6e 01 00 15 28 4c 6a
    0000640 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b
    0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
    0000700 00 07 00 08 00 01 00 09 00 00 00 2f 00 01 00 01
    0000720 00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a 00
    0000740 00 00 06 00 01 00 00 00 04 00 0b 00 00 00 0c 00
    0000760 01 00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e 00
    0001000 0f 00 02 00 09 00 00 00 37 00 02 00 01 00 00 00
    0001020 09 b2 00 02 12 03 b6 00 04 b1 00 00 00 02 00 0a
    0001040 00 00 00 0a 00 02 00 00 00 06 00 08 00 07 00 0b
    0001060 00 00 00 0c 00 01 00 00 00 09 00 10 00 11 00 00
    0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00
    0001120 00 00 02 00 14
  • 0-3字节,表示它是否是class类型的文件

    • 0000000 ca fe ba be 00 00 00 34 00 22 0a 00 06 00 14 09
  • 在Java中,所有的.class文件都以魔数

    1
    ca fe ba be

    开头,这个魔数的前4个字节用于识别该文件是否为Java类文件,如果这个魔数不匹配,那么Java虚拟机将无法加载该文件。

    • 关于

      1
      cafebabe

      这个魔数的由来并没有具体的官方解释,但有一些有趣的猜测和传说。

      • 一种说法是,这个魔数是由Java的创造者之一、现任谷歌高管James Gosling取的。据说Gosling是个爱好咖啡的人,他认为Java这个名字也与咖啡有关,所以他将cafebabe取作魔数来向咖啡致敬。另外,有一种传说认为这个魔数是来自于一个好莱坞电影中的经典台词,cafe babe(咖啡宝贝?)。
      • 然而,无论是什么样的由来,cafebabe这个魔数现在已经成为Java世界中的一个标志,每一个Java程序员都能够轻松地辨认出这个魔数,这也是Java文件格式稳定性的一个体现。

版本

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

常量池

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

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

    • 0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
    • 0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07
  3. 第#3项 08 表示一个字符串常量名称,00 18(24)表示它引用了常量池中 #24 项

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

    • 0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07
  5. 第#5项
    
    1
    2
    3
    4
    5
    6
    7
    8
    9



    07 表示一个 Class 信息,00 1b(27) 表示它引用了常量池中 #27 项

    - 0000020 00 16 00 17 08 00 18 0a 00 19 00 1a `07 00 1b` 07

    6. ```
    #6项
    07 表示一个 Class 信息,00 1c(28) 表示它引用了常量池中 #28 项 - 0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b `07` - 0000040 `00 1c` 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29
  6. 第#7项
    
    1
    2
    3
    4
    5



    01 表示一个 utf8 串,00 06 表示长度,3c 69 6e 69 74 3e 是

    <init>
    1
    2
    3
    4
    5

    - 0000040 00 1c 01 `00 06 3c 69 6e 69 74 3e` 01 00 03 28 29

    8. ```
    #8项
    01 表示一个 utf8 串,00 03 表示长度,28 29 56 是
    1
    ()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
  7. 第#9项
    
    1
    2
    3
    4
    5



    01 表示一个 utf8 串,00 04 表示长度,43 6f 64 65

    Code
    1
    2
    3
    4
    5

    - 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 是
    1
    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
  8. 第#11项
    
    1
    2
    3
    4
    5



    01 表示一个 utf8 串,00 1218) 表示长度,4c 6f 63 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65

    LocalVariableTable
    1
    2
    3
    4
    5
    6

    - 0000100 75 6d 62 65 72 54 61 62 6c 65 `01 00 12 4c 6f 63`
    - 0000120 `61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65` 01

    12. ```
    #12项
    01 表示一个 utf8 串,00 04 表示长度,74 68 69 73 是
    1
    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
  9. 第#13项
    
    1
    2
    3
    4
    5



    01 表示一个 utf8 串,00 1d(29) 表示长度,是

    Lcn/itcast/jvm/t5/HelloWorld;
    1
    2
    3
    4
    5
    6
    7

    - 0000140 00 04 74 68 69 73 `01 00 1d 4c 63 6e 2f 69 74 63`
    - 0000160 `61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c 6f`
    - 0000200 `57 6f 72 6c 64 3b` 01 00 04 6d 61 69 6e 01 00 16

    14. ```
    #14项
    01 表示一个 utf8 串,00 04 表示长度,74 68 69 73 是
    1
    main
    - 0000200 57 6f 72 6c 64 3b `01 00 04 6d 61 69 6e` 01 00 16
  10. 第#15项
    
    1
    2
    3
    4
    5



    01 表示一个 utf8 串,00 1622) 表示长度,是

    ([Ljava/lang/String;)V
    1
    2
    3
    4
    5
    6
    7
    8
    9

    其实就是参数为字符串数组,无返回值

    - 0000200 57 6f 72 6c 64 3b 01 00 04 6d 61 69 6e `01 00 16`
    - 0000220 `28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72`
    - 0000240 `69 6e 67 3b 29 56` 01 00 04 61 72 67 73 01 00 13

    16. ```
    #16项
    01 表示一个 utf8 串,00 04 表示长度,是
    1
    args
    - 0000240 69 6e 67 3b 29 56 `01 00 04 61 72 67 73` 01 00 13
  11. 第#17项
    
    1
    2
    3
    4
    5



    01 表示一个 utf8 串,00 1319) 表示长度,是

    [Ljava/lang/String;
    1
    2
    3
    4
    5
    6
    7

    - 0000240 69 6e 67 3b 29 56 01 00 04 61 72 67 73 `01 00 13`
    - 0000260 `5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69`
    - 0000300 `6e 67 3b` 01 00 10 4d 65 74 68 6f 64 50 61 72 61

    18. ```
    #18项
    01 表示一个 utf8 串,00 10(16) 表示长度,是
    1
    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
  12. 第#19项
    
    1
    2
    3
    4
    5



    01 表示一个 utf8 串,00 0a(10) 表示长度,是

    SourceFile
    1
    2
    3
    4
    5
    6

    - 0000320 6d 65 74 65 72 73 `01 00 0a 53 6f 75 72 63 65 46`
    - 0000340 `69 6c 65` 01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64

    20. ```
    #20项
    01 表示一个 utf8 串,00 0f(15) 表示长度,是
    1
    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
  13. 第#21项
    
    1
    2
    3
    4
    5
    6
    7



    0c 表示一个



    名+类型
    1
    2
    3
    4
    5
    6
    7

    ,00 07 00 08 引用了常量池中 #7 #8 两项

    - 0000360 2e 6a 61 76 61 `0c 00 07 00 08` 07 00 1d 0c 00 1e

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



    0c 表示一个



    名+类型
    1
    2
    3
    4
    5
    6
    7
    8

    ,00 1e(30) 00 1f (31)引用了常量池中 #30 #31 两项

    - 0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d `0c 00 1e`
    - 0000400 `00 1f` 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64

    24. ```
    #24项
    01 表示一个 utf8 串,00 0f(15) 表示长度,是
    1
    hello world
    - 0000400 00 1f `01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64`
  15. 第#25项
    
    1
    2
    3
    4
    5
    6
    7
    8
    9



    07 表示一个 Class 信息,00 20(32) 引用了常量池中 #32 项

    - 0000420 `07 00 20` 0c 00 21 00 22 01 00 1b 63 6e 2f 69 74

    26. ```
    #26项
    0c 表示一个
    1
    名+类型
    ,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
  16. 第#27项
    
    1
    2
    3
    4
    5



    01 表示一个 utf8 串,00 1b(27) 表示长度,是

    cn/itcast/jvm/t5/HelloWorld
    1
    2
    3
    4
    5
    6
    7

    - 0000420 07 00 20 0c 00 21 00 22 01 `00 1b 63 6e 2f 69 74`
    - 0000440 `63 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c`
    - 0000460 `6f 57 6f 72 6c 64` 01 00 10 6a 61 76 61 2f 6c 61

    28. ```
    #28项
    01 表示一个 utf8 串,00 10(16) 表示长度,是
    1
    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
  17. 第#29项
    
    1
    2
    3
    4
    5



    01 表示一个 utf8 串,00 1016) 表示长度,是

    java/lang/System
    1
    2
    3
    4
    5
    6

    - 0000500 6e 67 2f 4f 62 6a 65 63 74 `01 00 10 6a 61 76 61`
    - 0000520 `2f 6c 61 6e 67 2f 53 79 73 74 65 6d` 01 00 03 6f

    30. ```
    #30项
    01 表示一个 utf8 串,00 03 表示长度,是
    1
    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
  18. 第#31项
    
    1
    2
    3
    4
    5



    01 表示一个 utf8 串,00 1521) 表示长度,是

    Ljava/io/PrintStream;
    1
    2
    3
    4
    5
    6

    - 0000540 75 74 `01 00 15 4c 6a 61 76 61 2f 69 6f 2f 50 72`
    - 0000560 `69 6e 74 53 74 72 65 61 6d 3b` 01 00 13 6a 61 76

    32. ```
    #32项
    01 表示一个 utf8 串,00 13(19) 表示长度,是
    1
    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`
  19. 第#33项
    
    1
    2
    3
    4
    5



    01 表示一个 utf8 串,00 07 表示长度,是

    println
    1
    2
    3
    4
    5

    - 0000620 `01 00 07 70 72 69 6e 74 6c 6e` 01 00 15 28 4c 6a

    34. ```
    #34项
    01 表示一个 utf8 串,00 15(21) 表示长度,是
    1
    (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项,发现是

    1
    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 表示

      1
      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 项,是

      1
      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 项,发现是

      1
      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 项,即

    1
    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

字节码指令

入门

  • 在上一小节,有两个字节码指令,我们没有细说,那现在就来具体看看

  • 一个是

    1
    public cn.itcast.jvm.t5.HelloWorld();

    构造方法的字节码指令

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

    1
    public static void main(java.lang.String[]);

    主方法的字节码指令

    1
    2
    PLAINTEXT
    b2 00 02 12 03 b6 00 04 b1
    1. b2 => getstatic 用来加载静态变量,哪个静态变量呢?
    2. 00 02 引用常量池中 #2 项,即Field java/lang/System.out:Ljava/io/PrintStream;
    3. 12 => ldc 加载参数,哪个参数呢?
    4. 03 引用常量池中 #3 项,即 String hello world
    5. b6 => invokevirtual 预备调用成员方法,哪个方法呢?
    6. 00 04 引用常量池中 #4 项,即Method java/io/PrintStream.println:(Ljava/lang/String;)V
    7. b1 表示返回
  • 详情请参考官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5

javap工具

  • 自己分析类文件结构太麻烦了,Oracle提供了javap工具来反编译class文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    PLAINTEXT
    $ javap -v HelloWorld.class
    Classfile /D:/Workspace/JVM/demo/target/classes/com/demo/HelloWorld.class
    Last modified 2023-4-5; size 551 bytes
    MD5 checksum 1389d939c65ba536eb81d1a5c61d99be
    Compiled from "HelloWorld.java"
    public class com.demo.HelloWorld
    minor version: 0
    major version: 52
    flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
    #1 = Methodref #6.#20 // java/lang/Object."<init>":()V
    #2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
    #3 = String #23 // hello world
    #4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
    #5 = Class #26 // com/demo/HelloWorld
    #6 = Class #27 // java/lang/Object
    #7 = Utf8 <init>
    #8 = Utf8 ()V
    #9 = Utf8 Code
    #10 = Utf8 LineNumberTable
    #11 = Utf8 LocalVariableTable
    #12 = Utf8 this
    #13 = Utf8 Lcom/demo/HelloWorld;
    #14 = Utf8 main
    #15 = Utf8 ([Ljava/lang/String;)V
    #16 = Utf8 args
    #17 = Utf8 [Ljava/lang/String;
    #18 = Utf8 SourceFile
    #19 = Utf8 HelloWorld.java
    #20 = NameAndType #7:#8 // "<init>":()V
    #21 = Class #28 // java/lang/System
    #22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
    #23 = Utf8 hello world
    #24 = Class #31 // java/io/PrintStream
    #25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
    #26 = Utf8 com/demo/HelloWorld
    #27 = Utf8 java/lang/Object
    #28 = Utf8 java/lang/System
    #29 = Utf8 out
    #30 = Utf8 Ljava/io/PrintStream;
    #31 = Utf8 java/io/PrintStream
    #32 = Utf8 println
    #33 = Utf8 (Ljava/lang/String;)V
    {
    public com.demo.HelloWorld();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
    stack=1, locals=1, args_size=1
    0: aload_0
    1: invokespecial #1 // Method java/lang/Object."<init>":()V
    Code:
    stack=2, locals=1, args_size=1
    0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
    3: ldc #3 // String hello world
    5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    8: return
    LineNumberTable:
    line 5: 0
    line 6: 8
    LocalVariableTable:
    Start Length Slot Name Signature
    0 9 0 args [Ljava/lang/String;
    }
    SourceFile: "HelloWorld.java"

图解方法执行流程

  1. 原始Java代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    JAVA
    /**
    * 演示 字节码指令 和 操作数栈、常量池的关系
    */
    public class Demo_20 {
    public static void main(String[] args) {
    int a = 10;
    int b = Short.MAX_VALUE + 1;
    int c = a + b;
    System.out.println(c);
    }
    }
  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
    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
    PLAINTEXT
    $ javap -v Demo_20.class
    Classfile /D:/Workspace/JVM/demo/target/classes/com/demo/Demo_20.class
    Last modified 2023-4-7; size 601 bytes
    MD5 checksum 0f9e41fb2a7334a69c89d2661540f4f1
    Compiled from "Demo_20.java"
    public class com.demo.Demo_20
    minor version: 0
    major version: 52
    flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
    #1 = Methodref #7.#25 // java/lang/Object."<init>":()V
    #2 = Class #26 // java/lang/Short
    #3 = Integer 32768
    #4 = Fieldref #27.#28 // java/lang/System.out:Ljava/io/PrintStream;
    #5 = Methodref #29.#30 // java/io/PrintStream.println:(I)V
    #6 = Class #31 // com/demo/Demo_20
    #7 = Class #32 // java/lang/Object
    #8 = Utf8 <init>
    #9 = Utf8 ()V
    #10 = Utf8 Code
    #11 = Utf8 LineNumberTable
    #12 = Utf8 LocalVariableTable
    #13 = Utf8 this
    #14 = Utf8 Lcom/demo/Demo_20;
    #15 = Utf8 main
    #16 = Utf8 ([Ljava/lang/String;)V
    #17 = Utf8 args
    #18 = Utf8 [Ljava/lang/String;
    #19 = Utf8 a
    #20 = Utf8 I
    #21 = Utf8 b
    #22 = Utf8 c
    #23 = Utf8 SourceFile
    #24 = Utf8 Demo_20.java
    #25 = NameAndType #8:#9 // "<init>":()V
    #26 = Utf8 java/lang/Short
    #27 = Class #33 // java/lang/System
    #28 = NameAndType #34:#35 // out:Ljava/io/PrintStream;
    #29 = Class #36 // java/io/PrintStream
    #30 = NameAndType #37:#38 // println:(I)V
    #31 = Utf8 com/demo/Demo_20
    #32 = Utf8 java/lang/Object
    #33 = Utf8 java/lang/System
    #34 = Utf8 out
    #35 = Utf8 Ljava/io/PrintStream;
    #36 = Utf8 java/io/PrintStream
    #37 = Utf8 println
    #38 = Utf8 (I)V
    {
    public com.demo.Demo_20();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
    stack=1, locals=1, args_size=1
    0: aload_0
    1: invokespecial #1 // Method java/lang/Object."<init>":()V
    4: return
    LineNumberTable:
    line 6: 0
    LocalVariableTable:
    Start Length Slot Name Signature
    0 5 0 this Lcom/demo/Demo_20;

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

  4. 方法字节码载入方法区
    img

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

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

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

分析 i++

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

  • 原始Java代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    JAVA
    public class Demo_21 {
    public static void main(String[] args) {
    int a = 10;
    int b = a++ + ++a + a--;
    System.out.println(a);
    System.out.println(b);
    }
    }
  • 编译后的字节码文件

    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
    PLAINTEXT
    $ javap -v Demo_21.class
    Classfile /D:/Workspace/JVM/demo/target/classes/com/demo/Demo_21.class
    Last modified 2023-4-7; size 576 bytes
    MD5 checksum 5bc962752b10ca4b57350ca9814ec5b0
    Compiled from "Demo_21.java"
    public class com.demo.Demo_21
    minor version: 0
    major version: 52
    flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
    #1 = Methodref #5.#22 // java/lang/Object."<init>":()V
    #2 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream;
    #3 = Methodref #25.#26 // java/io/PrintStream.println:(I)V
    #4 = Class #27 // com/demo/Demo_21
    #5 = Class #28 // java/lang/Object
    #6 = Utf8 <init>
    #7 = Utf8 ()V
    #8 = Utf8 Code
    #9 = Utf8 LineNumberTable
    #10 = Utf8 LocalVariableTable
    #11 = Utf8 this
    #12 = Utf8 Lcom/demo/Demo_21;
    #13 = Utf8 main
    #14 = Utf8 ([Ljava/lang/String;)V
    #15 = Utf8 args
    #16 = Utf8 [Ljava/lang/String;
    #17 = Utf8 a
    #18 = Utf8 I
    #19 = Utf8 b
    #20 = Utf8 SourceFile
    #21 = Utf8 Demo_21.java
    #22 = NameAndType #6:#7 // "<init>":()V
    #23 = Class #29 // java/lang/System
    #24 = NameAndType #30:#31 // out:Ljava/io/PrintStream;
    #25 = Class #32 // java/io/PrintStream
    #26 = NameAndType #33:#34 // println:(I)V
    #27 = Utf8 com/demo/Demo_21
    #28 = Utf8 java/lang/Object
    #29 = Utf8 java/lang/System
    #30 = Utf8 out
    #31 = Utf8 Ljava/io/PrintStream;
    #32 = Utf8 java/io/PrintStream
    #33 = Utf8 println
    #34 = Utf8 (I)V
    {
    public com.demo.Demo_21();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
    stack=1, locals=1, args_size=1
    0: aload_0
    1: invokespecial #1 // Method java/lang/Object."<init>":()V
    4: return
    LineNumberTable:
    line 3: 0
    LocalVariableTable:
    Start Length Slot Name Signature
    0 5 0 this Lcom/demo/Demo_21;

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

    1
    iinc

    指令是直接在局部变量slot上进行运算,下面逐行分析字节码指令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    PLAINTEXT
    0: bipush 10 // 将一个byte压入操作数栈,此时就是将10压入操作数栈
    2: istore_1 // 将操作数栈顶部数据弹出,存入局部变量表 slot 1
    3: iload_1 // 将局部变量表slot 1的值加载到操作数栈中,也就是将10加载到栈中
    4: iinc 1, 1 // 在当前局部变量上进行运算,自增1,此时局部变量 a = 11 至此 a++ 执行完毕
    7: iinc 1, 1 // 在当前局部变量上进行运算,自增1,此时局部变量 a = 12
    10: iload_1 // 将局部变量表slot 1的值加载到操作数栈中,也就是将12加载到栈中
    11: iadd // 将栈内两个元素相加,10 + 12 = 22,将结果22加载到栈中
    12: iload_1 // 将局部变量表slot 1的值加载到操作数栈中,也就是将12加载到栈中
    13: iinc 1, -1 // 在当前局部变量上进行运算,自减1,此时局部变量 a = 11
    16: iadd // 将栈内两个元素相加,22 + 12 = 34,结果为34
    17: istore_2 // 将操作数栈顶部数据弹出,存入局部变量表slot 2
    18: getstatic #2 // 下面就不分析了,就是输出a和b的值
    21: iload_1
    22: invokevirtual #3
    25: getstatic #2
    28: iload_2
    29: invokevirtual #3
    32: return
  • 那么最终的结果a = 11b = 34

  • 从字节码指令中,我们可以看出,

    1
    a++

    1
    ++a

    的区别为

    • a++是先执行iload,再执行iinc
    • ++a是先执行iinc,再执行iload

条件判断指令

指令 助记符 含义
0x99 ifeq 判断是否 == 0
0x9a ifne 判断是否 != 0
0x9b iflt 判断是否 < 0
0x9c ifge 判断是否 >= 0
0x9d ifgt 判断是否 > 0
0x9e ifle 判断是否 <= 0
0x9f if_icmpeq 两个int是否 ==
0xa0 if_icmpne 两个int是否 !=
0xa1 if_icmplt 两个int是否 <
0xa2 if_icmpge 两个int是否 >=
0xa3 if_icmpgt 两个int是否 >
0xa4 if_icmple 两个int是否 <=
0xa5 if_acmpeq 两个引用是否 ==
0xa6 if_acmpne 两个引用是否 !=
0xc6 ifnull 判断是否 == null
0xc7 ifnonnull 判断是否 != null
  • 原始Java代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    JAVA
    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
    11
    PLAINTEXT
    0: iconst_0 // 将整数常量值0(int类型)压入操作数栈中。
    1: istore_1 // 将栈顶数据存入局部变量表 slot 1
    2: iload_1 // 将局部变量表slot 1的值压入操作数栈
    3: ifne 12 // 判断不等于0,成立跳转至12行,不成立则执行下一行
    6: bipush 10 //10压入操作数栈
    8: istore_1 // 将栈顶数据存入局部变量表 slot 1
    9: goto 15 // 跳转至第15
    12: bipush 20 //20压入操作数栈,对应 a = 20
    14: istore_1 // 将栈顶数据存入局部变量表 slot 1
    15: return

循环控制指令

  • 原始Java代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    JAVA
    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
    9
    PLAINTEXT
    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
    do while

    循环

    1
    2
    3
    4
    5
    6
    7
    8
    9
    JAVA
    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
    8
    PLAINTEXT
    0: iconst_0 // 将整数常量值0(int类型)压入操作数栈中。
    1: istore_1 // 将栈顶数据存入局部变量表 slot 1
    2: iinc 1, 1 // i自增
    5: iload_1 // 将局部变量表slot 1加载到操作数栈
    6: bipush 10 //10加载到操作数栈
    8: if_icmplt 2 // 判断 i < 10,成立则跳转到第2行,不成立执行下一行
    11: return
  • 最后再来看看for循环

    1
    2
    3
    4
    5
    6
    7
    8
    JAVA
    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
    9
    PLAINTEXT
    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
    12
    JAVA
    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
    4
    PLAINTEXT
    10: iload_2
    11: iinc 2, 1
    14: istore_2

构造方法

  1. <cinit>()V
    
    1

    JAVA public class Demo_27 { static int i = 10; static { i = 20; } static { i = 30; } public static void main(String[] args) { } }
    1
    2
    3

    - 编译后的字节码文件

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

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

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

    2. ```
    <init>()V
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    JAVA
    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
    22
    PLAINTEXT
    0: aload_0
    1: invokespecial #1 // super.<init>()V
    4: aload_0
    5: ldc #2 // <- "s1"
    7: putfield #3 // -> this.a
    10: aload_0
    11: bipush 20 // <- 20
    13: putfield #4 // -> this.b
    16: aload_0
    17: bipush 10 // <- 10
    19: putfield #4 // -> this.b
    22: aload_0
    23: ldc #5 // <- "s2"
    25: putfield #3 // -> this.a
    28: aload_0 // ------------------------------
    29: aload_1 // <- slot 1(a) "s3" |
    30: putfield #3 // -> this.a |
    33: aload_0 |
    34: iload_2 // <- slot 2(b) 30 |
    35: putfield #4 // -> this.b --------------------
    38: return
    - 编译器会按照从上至下的顺序,收集所有代码块和所有成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是会在最后

方法调用

  • 看一下几种不同方法调用对应的字节码指令,私有方法,final方法,公共方法,静态方法

    • 其中静态方法包括对象调静态方法和类直接调静态方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      JAVA
      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
      16
      PLAINTEXT
      0: new #2 // class com/demo/Demo_29
      3: dup
      4: invokespecial #3 // Method "<init>":()V
      7: astore_1
      8: aload_1
      9: invokespecial #4 // Method test1:()V
      12: aload_1
      13: invokespecial #5 // Method test2:()V
      16: aload_1
      17: invokevirtual #6 // Method test3:()V
      20: aload_1
      21: pop
      22: invokestatic #7 // Method test4:()V
      25: invokestatic #7 // Method test4:()V
      28: return
    • new #2是创建Demo_29对象,给对象分配内存,执行成功会将对象引用压入操作数栈

    • dup是赋值操作数栈顶的内容,本例为

      1
      对象引用

      。那为什么需要两份引用呢?

      • 一个是要配合invokespecial调用该对象的构造方法"<init>:()V",会消耗掉栈顶一个引用
      • 另一个要配合astore_1赋值给局部变量
    • final方法、私有方法、构造方法,都是由invokespecial指令来调用,属于静态绑定

    • 普通成员方法是由invokevirtual调用,属于动态绑定,即支持多态

    • 成员方法与静态方法调用的另一个区别是,执行方法前是否需要对象引用

    • 比较有意思的是,执行demo.test4()时,是通过

      1
      对象引用

      调用的静态方法,可以看到在调用前执行了pop指令,把对象引用从操作数栈弹掉了,因为静态方法不需要对象引用来掉,通过这种方式,反而会增加两步无用的字节码指令

      1
      2
      3
      4
      PLAINTEXT
      20: aload_1
      21: pop
      22: invokestatic #7 // Method test4:()V

多态的原理

  • 原始Java代码

    • 定义了一个抽象类Animal,还有其两个子类Cat和Dog

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      JAVA
      import java.io.IOException;

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

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

      abstract class Animal {
      public abstract void eat();

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

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

      class Cat extends Animal {
      @Override
      public void eat() {
      System.out.println("想吃小鱼干");
      }
      }
  1. 运行代码

    • 会停在System.in.read()方法上(当然你也可以直接打断点),运行jps命令获取进程id
  2. 运行HSDB工具

    • 进入JDK安装目录,执行

      1
      2
      BASH
      java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB
    • 进入图形界面attach进程id

  3. 查找某个对象

    • 打开Tools -> Find Object By Query,输入命令,点击Execute执行

      1
      2
      BASH
      select d from com.demo.Dog d

      img

  4. 查看对象内存结构

    • 点击超链接可以看到对象的内存结构,此对象没有任何属性,因此只有对象头的16字节,前8字节是MarkWord,后8字节就是对象的Class指针,但现在看不到它的实际地址
  5. 查看对象Class的内存地址

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

      1
      2
      BASH
      mem 0x000001f1676e77c8 2
    • mem指令有两个参数,参数1是对象地址,参数2是查看2行(即16字节)

    • 结果中第二行0x000001f1f48841a0即为Class的内存地址
      img

  6. 查看类的vtable

    • 方法1:ALT + R进入Inspector工具输入刚才的Class内存地址

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

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

    • 那么这6个虚方法都是谁呢?从Class的起始地址开始算,偏移

      1
      0x1b8

      就是vtable的其实地址,进行计算得到

      1
      2
      3
      4
      5
      PLAINTEXT
      0x000001f1f48841a0
      1b8 +
      --------------------
      0x000001f1f4884358
    • 通过Windows -> Console进入命令行模式,执行如下命令,就得到了6个虚方法的入口地址

      1
      2
      3
      4
      5
      6
      7
      8
      BASH
      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
      7
      PLAINTEXT
      0x000001f1f4481b10 -> Object -- protected void finalize() @0x000001f1f4481b10;
      0x000001f1f44815e8 -> Object -- public boolean equals(java.lang.Object) @0x000001f1f44815e8;
      0x000001f1f4883750 -> Animal -- public java.lang.String toString() @0x000001f1f4883750;
      0x000001f1f4481540 -> Object -- public native int hashCode() @0x000001f1f4481540;
      0x000001f1f4481678 -> Object -- protected native java.lang.Object clone() @0x000001f1f4481678;
      0x000001f1f4884148 -> Dog -- public void eat() @0x000001f1f4884148;
    • 对号入座,发现

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

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

异常处理

  1. try-catch

    • 原始Java代码

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      JAVA
      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
      18
      JAVA
      public static void main(java.lang.String[]);
      descriptor: ([Ljava/lang/String;)V
      flags: ACC_PUBLIC, ACC_STATIC
      Code:
      stack=1, locals=3, args_size=1
      0: iconst_0
      1: istore_1
      2: bipush 10
      4: istore_1
      5: goto 12
      8: astore_2
      9: bipush 20
      11: istore_1
      12: return
      Exception table:
      from to target type
      2 5 8 Class java/lang/Exception
    • 可以看到多出来一个Exception table的结构,[from to)是左闭右开的检测范围,一旦这个范围内的字节码执行出现异常,则通过type匹配异常类型,如果一致,进入target所指示的行号,该例中是第8行,也就是执行catch代码块

    • 第8行的字节码指令astore_2是将异常对象存入局部变量表的slot 2的位置

  2. 多个catch块的情况

    • 原始Java代码

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      JAVA
      public class Demo_32 {
      public static void main(String[] args) {
      int i = 0;
      try {
      i = 10;
      } catch (ArithmeticException e) {
      i = 20;
      } catch (NullPointerException e) {
      i = 30;
      } catch (Exception e) {
      i = 40;
      }
      }
      }
    • 编译后的字节码文件

      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
      JAVA
      public static void main(java.lang.String[]);
      descriptor: ([Ljava/lang/String;)V
      flags: ACC_PUBLIC, ACC_STATIC
      Code:
      stack=1, locals=3, args_size=1
      0: iconst_0
      1: istore_1
      2: bipush 10
      4: istore_1
      5: goto 26
      8: astore_2
      9: bipush 20
      11: istore_1
      12: goto 26
      15: astore_2
      16: bipush 30
      18: istore_1
      19: goto 26
      22: astore_2
      23: bipush 40
      25: istore_1
      26: return
      Exception table:
      from to target type
      2 5 8 Class java/lang/ArithmeticException
      2 5 15 Class java/lang/NullPointerException
      2 5 22 Class java/lang/Exception
      LineNumberTable:
      line 5: 0
      line 7: 2
      line 14: 5
      line 8: 8
      line 9: 9
      line 14: 12
      line 10: 15
      line 11: 16
      line 14: 19
      line 12: 22
      line 13: 23
      line 15: 26
      LocalVariableTable:
      Start Length Slot Name Signature
      9 3 2 e Ljava/lang/ArithmeticException;
      16 3 2 e Ljava/lang/NullPointerException;
      23 3 2 e Ljava/lang/Exception;
      0 27 0 args [Ljava/lang/String;
      2 25 1 i I
    • 因为异常出现时,只能进入一个Exception table的分支,所以局部变量表slot 2位置被共用

    • [from, to)的检测范围都相同,只不过target的行号不同,对应三个catch块

  3. multi-catch的情况

    • 原始Java代码

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      JAVA
      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
      JAVA
      public static void main(java.lang.String[]);
      descriptor: ([Ljava/lang/String;)V
      flags: ACC_PUBLIC, ACC_STATIC
      Code:
      stack=1, locals=3, args_size=1
      0: iconst_0
      1: istore_1
      2: bipush 10
      4: istore_1
      5: goto 13
      8: astore_2
      9: aload_2
      10: invokevirtual #5 // Method java/lang/Throwable.printStackTrace:()V
      13: return
      Exception table:
      from to target type
      2 5 8 Class java/lang/NoSuchMethodError
      2 5 8 Class java/lang/IllegalAccessError
      2 5 8 Class java/lang/Exception
      LineNumberTable:
      line 5: 0
      line 7: 2
      line 10: 5
      line 11: 13
      LocalVariableTable:
      Start Length Slot Name Signature
      9 4 2 e Ljava/lang/Throwable;
      0 14 0 args [Ljava/lang/String;
      2 12 1 i I
    • [from, to)的检测范围都相同,target的行号也相同

  4. finally

    • 原始Java代码

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      JAVA
      public class Demo_34 {
      public static void main(String[] args) {
      int i = 0;
      try {
      i = 10;
      } catch (Exception e) {
      i = 20;
      } finally {
      i = 30;
      }
      }
      }
    • 编译后的字节码文件

      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
      JAVA
      public static void main(java.lang.String[]);
      descriptor: ([Ljava/lang/String;)V
      flags: ACC_PUBLIC, ACC_STATIC
      Code:
      stack=1, locals=4, args_size=1
      0: iconst_0
      1: istore_1 // 0 -> i
      2: bipush 10 // try ---------------------------
      4: istore_1 // 10 -> i |
      5: bipush 30 // |
      7: istore_1 // 30 -> i |
      8: goto 27 // return ------------------------
      11: astore_2 // catch Exception -> e ----------
      12: bipush 20 // |
      14: istore_1 // 20 -> i |
      15: bipush 30 // |
      17: istore_1 // 30 -> i |
      18: goto 27 // return ------------------------
      21: astore_3 // catch any -> slot 3 -----------
      22: bipush 30 // |
      24: istore_1 // 30 -> i |
      25: aload_3 // <- slot 3 |
      26: athrow // throw -------------------------
      27: return
      Exception table:
      from to target type
      2 5 11 java/lang/Exception
      2 5 21 any
      11 15 21 any
      LineNumberTable: ...
      LocalVariableTable:
      Start Length Slot Name Signature
      0 28 0 args [Ljava/lang/String;
      2 26 1 i I
      12 3 2 e Ljava/lang/Exception;
    • 可以看到有3个

      1
      [from, to)
      • 第一个[2, 5)是检测try块中是否有Exception异常,如果有则跳转至11行执行catch块
      • 第二个[2, 5)是检测try块中是否有其他异常(非Exception异常),如果有则跳转至21行执行finally块
      • 第三个[11, 15)是检测catch快中是否有其他异常,如果有则跳转至21行执行finally块
    • 结论:finally中的代码被复制了三分,分别放进try流程、catch流程以及catch剩余的异常类型流程

关于finally的面试题

  1. finally中出现了return

    • 原始Java代码,先自己试着想一下最终的结果是啥:

      20

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      JAVA
      public class Demo_35 {
      public static void main(String[] args) {
      int result = test();
      System.out.println(result);
      }

      private static int test() {
      try {
      return 10;
      } finally {
      return 20;
      }
      }
      }
    • 编译后的字节码文件

      • 注意这里要加上-p参数,才能显示私有方法的信息

        1
        2
        BASH
        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
      22
      JAVA
      private static int test();
      descriptor: ()I
      flags: ACC_PRIVATE, ACC_STATIC
      Code:
      stack=1, locals=2, args_size=0
      0: bipush 10 // 将 int 10 压入栈顶
      2: istore_0 // 将栈顶的 int 10 存入到局部变量 slot 0 中,并从栈顶弹出
      3: bipush 20 // 将 int 20 压入栈顶
      5: ireturn // 返回栈顶的 int 20
      6: astore_1 // 捕获任何异常
      7: bipush 20 // 将 int 20 压入栈顶
      9: ireturn
      Exception table:
      from to target type
      0 3 6 any
      LineNumberTable:
      line 11: 0
      line 13: 3
      StackMapTable: number_of_entries = 1
      frame_type = 70 /* same_locals_1_stack_item */
      stack = [ class java/lang/Throwable ]
    • 由于finally中的ireturn被插入了所有可能的流程,因此返回结果肯定以finally为准

    • 至于字节码中的第二行,目前看似没啥用,先留个伏笔,等下个例子来讲解

    • 之前的finally例子中,最后都会有一个athrow,这告诉我们,如果在finally中出现了return,那么就会吞掉异常,具体来看下面这个例子

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      JAVA
      public class Demo_36 {
      public static void main(String[] args) {
      int result = test();
      System.out.println(result);
      }

      private static int test() {
      try {
      int i = 1 / 0;
      return 10;
      } finally {
      return 20;
      }
      }
      }
    • 运行上面的代码,不会出现任何异常,输出20,i = 1 / 0那个异常被吞掉了

  2. finally对返回值的影响

    • 原始Java代码,还是先试着想想结果会输出什么:

      10

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      JAVA
      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
      37
      JAVA
      private static int test();
      descriptor: ()I
      flags: ACC_PRIVATE, ACC_STATIC
      Code:
      stack=1, locals=3, args_size=0
      0: bipush 10 // 将 10 放入栈顶
      2: istore_0 // 10 -> i
      3: iload_0 // <- i(10)
      4: istore_1 // 将 i(10) 暂存至 slot 1,目的是为了固定返回值
      5: bipush 20 // 将 20 放入栈顶
      7: istore_0 // 20 -> i
      8: iload_1 // 载入 slot 1 暂存的值 (10)
      9: ireturn // 返回栈顶的值
      10: astore_2
      11: bipush 20
      13: istore_0
      14: aload_2
      15: athrow
      Exception table:
      from to target type
      3 5 10 any
      LineNumberTable:
      line 10: 0
      line 12: 3
      line 14: 5
      line 12: 8
      line 14: 10
      line 15: 14
      LocalVariableTable:
      Start Length Slot Name Signature
      3 13 0 i I
      StackMapTable: number_of_entries = 1
      frame_type = 255 /* full_frame */
      offset_delta = 10
      locals = [ int ]
      stack = [ class java/lang/Throwable ]
    • 虽然在 finally 块中将 i 的值修改为 20,但是这不会影响 return 语句的返回值,因为在返回之前,i 的值已经被暂存到了 slot 1 中。在 finally 块中对 i 进行的修改不会影响 slot 1 中的值,因此 ireturn 指令返回的是 slot 1 中的值,即 10。

synchronized

  • synchronized代码块是对一个对象进行加锁操作,那么它是如何保障当synchronized代码块中出现了异常,还能正确的执行解锁操作呢?下面就从字节码的角度来分析一下底层原理

    • 原始Java代码

      1
      2
      3
      4
      5
      6
      7
      8
      9
      JAVA
      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
      40
      JAVA
      public static void main(java.lang.String[]);
      descriptor: ([Ljava/lang/String;)V
      flags: ACC_PUBLIC, ACC_STATIC
      Code:
      stack=2, locals=4, args_size=1
      0: new #2 // class java/lang/Object
      3: dup
      4: invokespecial #1 // Method java/lang/Object."<init>":()V
      7: astore_1 // lock引用 -> lock
      8: aload_1 // <- lock (synchronized开始)
      9: dup
      10: astore_2 // lock引用 -> slot 2
      11: monitorenter // monitorenter(lock引用)
      12: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
      15: ldc #4 // String ok
      17: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      20: aload_2 // <- slot 2(lock引用)
      21: monitorexit // monitorexit(lock引用)
      22: goto 30
      25: astore_3 // any -> slot 3
      26: aload_2 // <- slot 2(lock引用)
      27: monitorexit // monitorexit(lock引用)
      28: aload_3
      29: athrow
      30: return
      Exception table:
      from to target type
      12 22 25 any
      25 28 25 any
      LineNumberTable:
      line 5: 0
      line 6: 8
      line 7: 12
      line 8: 20
      line 9: 30
      LocalVariableTable:
      Start Length Slot Name Signature
      0 31 0 args [Ljava/lang/String;
      8 23 1 lock Ljava/lang/Object;
    • [12, 22)是监测的释放锁的流程,如果出现了异常,则跳转到25行,将异常信息存储到slot 3,同时再次尝试释放锁

    • [25, 28)也是监测异常,如果有异常,

编译期处理

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

默认构造器

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

    1
    2
    3
    4
    JAVA
    public class Candy01 {

    }
  • 编译成class后的代码

    1
    2
    3
    4
    5
    6
    7
    8
    JAVA
    public class Candy1 {

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

自动拆装箱

  • 这个特性是

    1
    JDK 5

    开始加入的,

    1
    代码片段1:
    1
    2
    3
    4
    5
    6
    7
    JAVA
    public class Candy02 {
    public static void main(String[] args) {
    Integer x = 1;
    int y = x;
    }
    }
  • 但是这段代码在

    1
    JDK 5

    之前是无法编译通过的,比如改写为如下形式,

    1
    代码片段2
    1
    2
    3
    4
    5
    6
    7
    JAVA
    public class Candy02 {
    public static void main(String[] args) {
    Integer x = Integer.valueOf(1);
    int y = x.intValue();
    }
    }
  • 显然之前版本的代码太麻烦了,需要在基本类型和包装类型之间相互转换(尤其是集合类中的操作都是包装类型),因此这些转换的事情在JDK 5以后都由编译器在编译阶段完成。即代码片段1都会在编译阶段转换成代码片段2

泛型集合取值

  • 泛型也是

    1
    JDK 5

    开始加入的特性,但Java在编译泛型后会执行

    1
    泛型擦除

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

    1
    2
    3
    4
    5
    6
    7
    8
    JAVA
    public class Candy03 {
    public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
    list.add(10); // 实际调用的是 List.add(Object e)
    Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index);
    }
    }
  • 所以在取值时,编译器真正生成的字节码中,还需要额外做一个类型转换的操作

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

    1
    2
    JAVA
    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
      47
      JAVA
      public com.demo.Candy03();
      descriptor: ()V
      flags: ACC_PUBLIC
      Code:
      stack=1, locals=1, args_size=1
      0: aload_0
      1: invokespecial #1 // Method java/lang/Object."<init>":()V
      4: return
      LineNumberTable:
      line 6: 0
      LocalVariableTable:
      Start Length Slot Name Signature
      0 5 0 this Lcom/demo/Candy03;
      public static void main(java.lang.String[]);
      descriptor: ([Ljava/lang/String;)V
      flags: ACC_PUBLIC, ACC_STATIC
      Code:
      stack=2, locals=3, args_size=1
      0: new #2 // class java/util/ArrayList
      3: dup
      4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
      7: astore_1
      8: aload_1
      9: bipush 10
      11: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      14: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      19: pop
      20: aload_1
      21: iconst_0
      22: invokeinterface #6, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
      27: checkcast #7 // class java/lang/Integer
      30: astore_2
      31: return
      LineNumberTable:
      line 8: 0
      line 9: 8
      line 10: 20
      line 11: 31
      LocalVariableTable:
      Start Length Slot Name Signature
      0 32 0 args [Ljava/lang/String;
      8 24 1 list Ljava/util/List;
      31 1 2 x Ljava/lang/Integer;
      LocalVariableTypeTable:
      Start Length Slot Name Signature
      8 24 1 list Ljava/util/List<Ljava/lang/Integer;>;
  • 使用反射,能够获取到方法类型参数的泛型和方法返回值泛型的信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    JAVA
    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
    7
    PLAINTEXT
    原始类型 - 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>

可变参数

  • 可变参数也是

    1
    JDK 5

    开始加入的新特性,示例代码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    JAVA
    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
    13
    JAVA
    public class Candy04 {

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

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

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

foreach循环

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    JAVA
    public class Candy05 {
    public static void main(String[] args) {
    int[] array = {1, 2, 3, 4, 5}; // 数组的赋初值的简化,也是语法糖 new int[]{1, 2, 3, 4, 5}
    for (int a : array) {
    System.out.println(a);
    }
    }
    }
  • 会被编译器转换为

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    JAVA
    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
    9
    JAVA
    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
    14
    JAVA
    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字符串

  • 1
    JDK 7

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    JAVA
    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
    28
    JAVA
    public class Candy07 {
    public Candy07() {

    }

    public static void choose(String str) {
    byte x = -1;
    switch (str.hashCode()) {
    case 99162322: // hello 的 hashCode
    if (str.equals("hello")) {
    x = 0;
    }
    break;
    case 113318802: // world 的 hashCode
    if (str.equals("world")) {
    x = 1;
    }
    }
    switch (x) {
    case 0:
    System.out.println("h");
    break;
    case 1:
    System.out.println("w");
    }
    }
    }
  • 可以看到,执行了两边switch,第一遍是根据字符串的hashCode和queals将字符串转换为相应byte类型,第二遍才是利用byte进行比较

  • 那为什么第一遍既要比较hashCode又利用equals比较呢?

    • hashCode是为了提高效率,减少可能的比较

    • 而equals是为了防止哈希冲突,例如

      1
      BM

      1
      C.

      这两个字符串的hashCode值都是2123,例如下面的代码

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      JAVA
      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
      20
      JAVA
      public static void choose(String str) {
      byte x = -1;
      switch (str.hashCode()) {
      case 2123: // hashCode 值可能相同,需要进一步用 equals 比较
      if (str.equals("C.")) {
      x = 1;
      } else if (str.equals("BM")) {
      x = 0;
      }
      default:
      switch (x) {
      case 0:
      System.out.println("h");
      break;
      case 1:
      System.out.println("w");
      }
      }
      }

switch枚举

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

    1
    2
    3
    4
    JAVA
    enum Sex {
    MALE, FEMALE
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    JAVA
    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
    30
    JAVA
    public class Candy08 {
    /**
    * 定义一个合成类(仅 jvm 使用,对我们不可见)
    * 用来映射枚举的 ordinal 与数组元素的关系
    * 枚举的 ordinal 表示枚举对象的序号,从 0 开始
    * 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1
    */
    static class $MAP {
    // 数组大小即为枚举元素个数,里面存储case用来对比的数字
    static int[] map = new int[2];

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

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

枚举类

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

    1
    2
    3
    4
    JAVA
    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
    34
    JAVA
    public final class Sex extends Enum<Sex> {
    public static final Sex MALE;
    public static final Sex FEMALE;
    private static final Sex[] $VALUES;
    static {
    MALE = new Sex("MALE", 0);
    FEMALE = new Sex("FEMALE", 1);
    $VALUES = new Sex[]{MALE, FEMALE};
    }

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

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

    public static Sex valueOf(String name) {
    return Enum.valueOf(Sex.class, name);
    }
    }
  • Sex被声明为一个final类,它继承了Enum<Sex>类,Enum是Java中定义枚举的抽象类。MALE和FEMALE是Sex类的两个枚举值,它们被定义为静态常量。

  • 除此之外,还有一个私有的、finalSex类型数组$VALUES,它用于存储Sex类的所有枚举值。在类的静态块中,$VALUES数组被初始化为一个包含MALEFEMALE的数组。

  • 构造函数Sex(String name, int ordinal)是私有的,这意味着无法在类的外部使用这个构造函数来创建Sex的实例。只有Java编译器生成的代码才能调用这个构造函数来创建Sex的实例。

  • values()valueOf(String name)是从Enum类继承的两个静态方法。values()方法返回一个包含Sex类所有枚举值的数组,valueOf(String name)方法返回指定名称的枚举值。

  • 当我们使用MALE或者FEMALE时,其实底层调用的是Enum.valueOf(Sex.class, "MALE")Enum.valueOf(Sex.class, "FEMALE")

try-with-resources

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

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

    } catch() {

    }
  • 其中资源对象需要实现AutoCloseable接口,例如InputStream、OutputStream、Connection、Statement、ResultSet等接口都实现了AuthCloseable接口,使用try-with-resources 可以不用写finally语句块,编译器会帮助我们生成关闭资源代码,例如

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    JAVA
    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
    37
    JAVA
    public class Candy09 {
    public Candy09() {
    }

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

    • 这是为了防止异常信息的丢失

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      JAVA
      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
      6
      PLAINTEXT
      java.lang.ArithmeticException: / by zero
      at com.demo.Test.main(Test.java:6)
      Suppressed: java.lang.Exception: close 异常
      at com.demo.MyResource.close(Test.java:14)
      at com.demo.Test.main(Test.java:7)

方法重写时的桥接方法

  • 方法重写时,对返回值分两种情况

    1. 父类与子类的返回值完全一致

    2. 子类返回值可以是父类返回值的子类(比较绕口,直接看下面的例子来理解)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      JAVA
      class A {
      public Number m() {
      return 1;
      }
      }
      class B extends A {
      @Override
      // 父类A方法的返回值是Number类型,子类B方法的返回值是Integer类型,Integer是Number的子类
      public Integer m() {
      return 2;
      }
      }
      • 那么对于子类,编译器会做如下处理

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        JAVA
        class B extends A {
        public Integer m() {
        return 2;
        }

        // 此方法才是真正重写了父类 public Number m() 方法
        public synthetic bridge Number m() {
        // 调用 public Integer m()
        return m();
        }
        }
      • 其中的桥接方法比较特殊,仅对Java虚拟机课件,并且与原来的public Integer m()没有命名冲突

匿名内部类

  • 原始Java代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    JAVA
    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
    15
    JAVA
    // 额外生成的类
    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
    11
    JAVA
    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
    16
    JAVA
    // 额外生成的类
    final class Candy11$1 implements Runnable {
    int val$x;
    Candy11$1(int x) {
    this.val$x = x;
    }
    public void run() {
    System.out.println("ok:" + this.val$x);
    }
    }
    public class Candy11 {
    public static void test(final int x) {
    Runnable runnable = new Candy11$1(x);
    }
    }
    • 注意:这也解释了为什么匿名内部类引用局部变量时,局部变量必须为final的
      • 因为在创建Candy$11对象时,将x的值赋给了val$x属性,所以x不应该再发生变化了
      • 如果变化,那么$val$x属性没有机会再跟着一起变化

类加载阶段

加载

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

img

链接

  1. 验证

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

    • 使用支持二进制的编辑器修改HelloWorld.class的魔数

      1
      ca fe ba be

      ,在控制台运行后悔报错

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      BASH
      Error: A JNI error has occurred, please check your installation and try again
      Exception in thread "main" java.lang.ClassFormatError: Incompatible magic value 3405691578 in class file cn/itcast/jvm/t5/HelloWorld
      at java.lang.ClassLoader.defineClass1(Native Method)
      at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
      at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
      at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
      at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
      at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
      at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
      at java.security.AccessController.doPrivileged(Native Method)
      at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
      at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
      at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
      at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
      at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)
  2. 准备

    • 为static变量分配空间,设置默认值

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

      1
      2
      3
      4
      5
      6
      7
      8
      JAVA
      public class Load01 {
      static int a;
      static int b = 10;
      static final int c = 20;
      static final String d = "Hello";
      static final Object e = new Object();
      }
    • 编译后的字节码文件

      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
      JAVA
      static int a;
      descriptor: I
      flags: ACC_STATIC

      static int b;
      descriptor: I
      flags: ACC_STATIC

      static final int c;
      descriptor: I
      flags: ACC_STATIC, ACC_FINAL
      ConstantValue: int 20

      static final java.lang.String d;
      descriptor: Ljava/lang/String;
      flags: ACC_STATIC, ACC_FINAL
      ConstantValue: String Hello

      static final java.lang.Object e;
      descriptor: Ljava/lang/Object;
      flags: ACC_STATIC, ACC_FINAL

      public com.demo.Load01();
      descriptor: ()V
      flags: ACC_PUBLIC
      Code:
      stack=1, locals=1, args_size=1
      0: aload_0
      1: invokespecial #1 // Method java/lang/Object."<init>":()V
      4: return
      LineNumberTable:
      line 5: 0
      LocalVariableTable:
      Start Length Slot Name Signature
      0 5 0 this Lcom/demo/Load01;

      static {};
      descriptor: ()V
      flags: ACC_STATIC
      Code:
      stack=2, locals=0, args_size=0
      0: bipush 10
      2: putstatic #2 // Field b:I
      5: new #3 // class java/lang/Object
      8: dup
      9: invokespecial #1 // Method java/lang/Object."<init>":()V
      12: putstatic #4 // Field e:Ljava/lang/Object;
      15: return
      LineNumberTable:
      line 7: 0
      line 10: 5
    • 变量a和b都是静态变量,但是只有变量b被赋予了初始值10,赋值操作在初始化阶段体现,也就是在 static 块中实现

    • 变量c和d都被声明为静态final变量,它们的值在编译时就已经确定了,分别是20和”Hello”,并且在字节码中使用了 ConstantValue 指令来指定这些常量的值。

    • 变量e也是静态final变量,但它是一个引用类型变量,因此在初始化阶段才会被赋值,也就是在 static 块中实现。

  3. 解析

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

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      JAVA
      public class Load02 {
      public static void main(String[] args) throws ClassNotFoundException, IOException {
      ClassLoader classloader = Load02.class.getClassLoader();
      // loadClass 方法不会导致类的解析和初始化
      Class<?> c = classloader.loadClass("com.demo.C");
      // new C();
      System.in.read();
      }
      }

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

      class D {
      }
    • 默认情况下,类的加载都是懒惰式的,如果用到了类C,没有用到类D的话,那么类D是不会主动加载的

    • 使用loadClass方法不会导致类的解析和初始化

      img

      • 可以看到类D现在是UnresolvedClass,也就是未经解析的类,在常量池中仅仅是一个符号
    • 使用new C()的方式会导致类的解析和初始化

      img

      • 可以看到此时类D已经加载成功了,同时在类C的常量池中也可以解析类D的地址

初始化

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

  • 发生的时机:总的来说,类的初始化是懒惰的

    1. main方法所在的类,总会被首先初始化

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      JAVA
      public class Load03 {
      static {
      System.out.println("main init");
      }

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

      }
      }
      • 控制台会输出

        1
        2
        PLAINTEXT
        main init
    2. 首次访问这个类的静态变量或静态方法时,会进行初始化

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      JAVA
      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
        PLAINTEXT
        main init
        a init
        0
    3. 子类初始化,如果父类还没未初始化,则父类也会进行初始化

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      JAVA
      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
        5
        PLAINTEXT
        main init
        a init
        b init
        false
    4. 默认的Class.forName会导致初始化

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      JAVA
      public class Load03 {
      static {
      System.out.println("main init");
      }

      public static void main(String[] args) throws ClassNotFoundException {
      Class.forName("com.demo.A");
      }
      }
      class A {
      static int a = 0;

      static {
      System.out.println("a init");
      }
      }
      • 控制台输出

        1
        2
        3
        JAVA
        main init
        a init
    5. new对象会导致初始化

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      JAVA
      public class Load03 {
      static {
      System.out.println("main init");
      }

      public static void main(String[] args) throws ClassNotFoundException {
      new A();
      }
      }
      class A {
      static int a = 0;

      static {
      System.out.println("a init");
      }
      }
      • 控制台输出

        1
        2
        3
        JAVA
        main init
        a init
  • 不会导致类初始化的情况

    1. 访问类的 static final 静态常量(基本类型和字符串) 不会触发初始化

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      JAVA
      public class Load03 {
      static {
      System.out.println("main init");
      }

      public static void main(String[] args) throws ClassNotFoundException {
      System.out.println(B.b);
      }
      }
      class A {
      static int a = 0;

      static {
      System.out.println("a init");
      }
      }
      class B extends A {
      final static double b = 5.0;
      static boolean c = false;

      static {
      System.out.println("b init");
      }
      }
      • 控制台输出

        1
        2
        3
        PLAINTEXT
        main init
        5.0
    2. 调用类对象.class不会触发初始化

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      JAVA
      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
        3
        PLAINTEXT
        main init
        class com.demo.B
    3. 类加载器的loadClass方法不会触发初始化

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      JAVA
      public class Load03 {
      static {
      System.out.println("main init");
      }

      public static void main(String[] args) throws ClassNotFoundException {
      ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
      classLoader.loadClass("com.demo.B");
      }
      }
      class A {
      static int a = 0;

      static {
      System.out.println("a init");
      }
      }
      class B extends A {
      final static double b = 5.0;
      static boolean c = false;

      static {
      System.out.println("b init");
      }
      }
      • 控制台输出

        1
        2
        PLAINTEXT
        main init
    4. Class.forName的参数2为false时(initalize = false),不会触发初始化

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      JAVA
      public class Load03 {
      static {
      System.out.println("main init");
      }

      public static void main(String[] args) throws ClassNotFoundException {
      ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
      Class.forName("com.demo.A", false, classLoader);
      }
      }
      class A {
      static int a = 0;

      static {
      System.out.println("a init");
      }
      }
      • 控制台输出

        1
        2
        PLAINTEXT
        main init

练习

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    JAVA
    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
      47
      JAVA
      {
      public static final int a;
      descriptor: I
      flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
      ConstantValue: int 10

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

      public static final java.lang.Integer c;
      descriptor: Ljava/lang/Integer;
      flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL

      com.demo.E();
      descriptor: ()V
      flags:
      Code:
      stack=1, locals=1, args_size=1
      0: aload_0
      1: invokespecial #1 // Method java/lang/Object."<init>":()V
      4: return
      LineNumberTable:
      line 10: 0
      LocalVariableTable:
      Start Length Slot Name Signature
      0 5 0 this Lcom/demo/E;

      static {};
      descriptor: ()V
      flags: ACC_STATIC
      Code:
      stack=2, locals=0, args_size=0
      0: bipush 20
      2: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      5: putstatic #3 // Field c:Ljava/lang/Integer;
      8: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
      11: ldc #5 // String init E
      13: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      16: return
      LineNumberTable:
      line 13: 0
      line 15: 8
      line 16: 16
      }
  • 典型应用 -> 完成懒惰初始化的单例模式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    JAVA
    public final class Singleton {
    private Singleton() {
    }

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

    // 第一次调用 getInstance 方法,才会导致内部类加载和初始化其静态成员
    public static Singleton getInstance() {
    return LazyHolder.INSTANCE;
    }
    }
  • 以上的实现特点是:

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

类加载器

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

引导类加载器

  • Bootstrap ClassLoader
    
    1
    2
    3

    是所有类加载器中最早的一个,负责加载JRE/lib下的核心类库,如java.lang.Object、java.lang.String等。

    JAVA public class Load05 { public static void main(String[] args) throws ClassNotFoundException { Class<?> aClass = Class.forName("java.lang.Object"); System.out.println(aClass.getClassLoader()); } }
    1
    2
    3
    4
    5
    6
    7

    - 输出的结果是null,因为引导类加载器是由JVM的实现者用C/C++等语言编写的,而不是由Java编写的。在Java虚拟机的实现中,引导类加载器不是Java对象,也没有对应的Java类,因此它的ClassLoader属性为null

    ## 扩展类加载器

    - 编写一个Tmp类

    JAVA public class Tmp { static { System.out.println("classpath Tmp init"); } }
    1
    2
    3

    - 加载Tmp类,并获取classLoader

    JAVA 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
    3

    - 输出如下,可以看到此时是由应用类加载器加载的

    PLAINTEXT classpath Tmp init sun.misc.Launcher$AppClassLoader@18b4aac2
    1
    2
    3

    - 那我们现在写一个同名的Tmp类,将输出内容改为

    ext Tmp init
    1

    JAVA public class Tmp { static { System.out.println("ext Tmp init"); } }
    1
    2
    3

    - 将其打成一个jar包,放到JAVA_HOME/jre/ext目录下

    BASH $ jar -cvf tmp.jar com/demo/load/Tmp.class 已添加清单 正在添加: Tmp.class(输入 = 479) (输出 = 321)(压缩了 32%)
    1
    2
    3

    - 重新执行Load06,输出结果如下

    PLAINTEXT ext Tmp init sun.misc.Launcher$ExtClassLoader@29453f44
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    - 此时就是从扩展类加载器加载的Tmp类了,因为当JVM需要加载一个类时,它会首先委托父类加载器去加载这个类

    ## 双亲委派机制

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

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

    - 我们来看看ClassLoader中的loadClass()方法的源码

    JAVA protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
    1
    2
    3

    - 精简一下逻辑,双亲委派的核心思路如下

    JAVA protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 检查类是否已经被加载 Class<?> c = findLoadedClass(name); if (c == null) { try { // 如果类没有被加载,则委托给父ClassLoader加载 if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // 如果父ClassLoader加载失败,则在自身查找类 c = findClass(name); } } if (resolve) { resolveClass(c); } return c; } }
    1
    2
    3
    4
    5

    ## 线程上下文类加载器

    - 我们在使用JDBC时,都需要加载Driver驱动,但是我们好像并没有显示的调用Class.forName来加载Driver类

    JAVA Class.forName("com.mysql.jdbc.Driver");
    1
    2
    3

    - 那么实际上是如何加载这个驱动的呢?让我们来追踪一下源码,这里只看最核心的部分

    JAVA public class DriverManager { private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>(); ··· static { loadInitialDrivers(); println("JDBC DriverManager initialized"); } ··· }
    1
    2
    3

    - 我们试着输出一下DirverManager的类加载器是谁

    JAVA System.out.println(DriverManager.class.getClassLoader());
    1
    2
    3
    4
    5
    6
    7

    - 输出的结果是`null`,那么说明它是由`Bootstrap ClassLoader`加载的,那么按理说应该是去`JAVA_HOMT/jre/lib`下搜索驱动类。

    - 但`JAVA_HOMT/jre/lib`显然没有mysql-connector-java-5.7.31.jar包,在DriverManager的静态代码块中,是如何正确加载`com.mysql.jdbc.Driver`的呢?

    - 继续来看看loadInitialDrivers()方法的源码

    JAVA private static void loadInitialDrivers() { String drivers; try { drivers = AccessController.doPrivileged(new PrivilegedAction<String>() { public String run() { return System.getProperty("jdbc.drivers"); } }); } catch (Exception ex) { drivers = null; } // If the driver is packaged as a Service Provider, load it. // Get all the drivers through the classloader // exposed as a java.sql.Driver.class service. // ServiceLoader.load() replaces the sun.misc.Providers() // 1. 使用 ServiceLoader 机制加载驱动,即 SPI AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); Iterator<Driver> driversIterator = loadedDrivers.iterator(); /* Load these drivers, so that they can be instantiated. * It may be the case that the driver class may not be there * i.e. there may be a packaged driver with the service class * as implementation of java.sql.Driver but the actual class * may be missing. In that case a java.util.ServiceConfigurationError * will be thrown at runtime by the VM trying to locate * and load the service. * * Adding a try catch block to catch those runtime errors * if driver not available in classpath but it's * packaged as service and that service is there in classpath. */ try{ while(driversIterator.hasNext()) { driversIterator.next(); } } catch(Throwable t) { // Do nothing } return null; } }); println("DriverManager.initialize: jdbc.drivers = " + drivers); // 2. 使用 jdbc.drivers 定义的驱动名加载驱动 if (drivers == null || drivers.equals("")) { return; } String[] driversList = drivers.split(":"); println("number of Drivers:" + driversList.length); for (String aDriver : driversList) { try { println("DriverManager.Initialize: loading " + aDriver); Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); } catch (Exception ex) { println("DriverManager.Initialize: load failed: " + ex); } } }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

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

    - 在看1,它就是大名鼎鼎的Service Provider Interface(SPI)

    - 约定如下,在jar包的META-INF/services包下,以接口全限定名名为文件,文件内容是实现类名称
    [![img](https://imgse.com/i/ppqEGM8)](https://imgse.com/i/ppqEGM8)

    - 这样就可以使用如下代码遍历来得到实现类

    JAVA ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class); Iterator<接口类型> iter = allImpls.iterator(); while(iter.hasNext()){ iter.next(); }
    1
    2
    3

    - 体现的是

    面向接口编程 + 解耦
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    的思想,在下面的一些框架中都运用了此思想

    - JDBC
    - Servlet初始化器
    - Spring容器
    - Dubbo(对SPI进行了扩展)

    - 接着看ServiceLoader.load方法

    JAVA public static <S> ServiceLoader<S> load(Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); }
    1
    2
    3

    - 线程上下文类加载器是当前线程使用的类加载器,默认就是应用类加载器,它内部又是由Class.forName调用了线程上下文类加载器完成类加载,具体代码在ServiceLoader的内部类LazyIterator中

    JAVA private class LazyIterator implements Iterator<S> { Class<S> service; ClassLoader loader; Enumeration<URL> configs = null; Iterator<String> pending = null; String nextName = null; private LazyIterator(Class<S> service, ClassLoader loader) { this.service = service; this.loader = loader; } private boolean hasNextService() { if (nextName != null) { return true; } if (configs == null) { try { String fullName = PREFIX + service.getName(); if (loader == null) configs = ClassLoader.getSystemResources(fullName); else configs = loader.getResources(fullName); } catch (IOException x) { fail(service, "Error locating configuration files", x); } } while ((pending == null) || !pending.hasNext()) { if (!configs.hasMoreElements()) { return false; } pending = parse(service, configs.nextElement()); } nextName = pending.next(); return true; } private S nextService() { if (!hasNextService()) throw new NoSuchElementException(); String cn = nextName; nextName = null; Class<?> c = null; try { c = Class.forName(cn, false, loader); } catch (ClassNotFoundException x) { fail(service, "Provider " + cn + " not found"); } if (!service.isAssignableFrom(c)) { fail(service, "Provider " + cn + " not a subtype"); } try { S p = service.cast(c.newInstance()); providers.put(cn, p); return p; } catch (Throwable x) { fail(service, "Provider " + cn + " could not be instantiated", x); } throw new Error(); // This cannot happen } public boolean hasNext() { if (acc == null) { return hasNextService(); } else { PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() { public Boolean run() { return hasNextService(); } }; return AccessController.doPrivileged(action, acc); } } public S next() { if (acc == null) { return nextService(); } else { PrivilegedAction<S> action = new PrivilegedAction<S>() { public S run() { return nextService(); } }; return AccessController.doPrivileged(action, acc); } } public void remove() { throw new UnsupportedOperationException(); } }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

    ## 自定义类加载器

    - 先来思考一下:什么时候需要自定义类加载器

    1. 自定义类加载器可用于加载非 Classpath 路径中的类文件,例如外部配置文件夹、网络资源或其他自定义路径。这种需求在一些动态扩展或插件化的场景中比较常见。
    2. 在应用程序中使用的类可以通过接口来使用,而不是直接引用类。这种做法可以减少应用程序之间的依赖,从而提高代码的灵活性和可维护性。同时,这种做法也使得框架的设计更加清晰和可扩展。
    3. 在Tomcat容器中,每个Web应用程序都使用自己的类加载器,从而避免了不同Web应用程序之间的类冲突问题。

    - 步骤

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

    - 示例

    1. 准备一个Tmp类,编译后将其.class文件放至D盘根目录下

    JAVA package myclasspath; public class Tmp { static { System.out.println("init myclasspath.Tmp"); } public static void main(String[] args) { System.out.println(); } }
    1
    2
    3

    2. 自定义MyClassLoader类

    JAVA class MyClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { String path = "D:\\myclasspath\\" + name + ".class"; try { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); Files.copy(Paths.get(path), outputStream); byte[] bytes = outputStream.toByteArray(); return defineClass(name, bytes, 0, bytes.length); } catch (IOException e) { e.printStackTrace(); throw new ClassNotFoundException("类文件未找到:" + e); } } }
    1
    2
    3

    3. 调用自定义的类加载器loadClass方法来加载Tmp类

    JAVA public class Load07 { public static void main(String[] args) throws Exception { MyClassLoader classLoader = new MyClassLoader(); Class<?> aClass = classLoader.loadClass("myclasspath.Tmp"); aClass.newInstance(); } }
    1
    2
    3

    - 控制台输出如下,成功加载Tmp类

    PLAINTEXT init myclasspath.Tmp
    1
    2
    3
    4
    5
    6
    7
    8
    9

    # 运行期优化

    ## 即时编译

    ### 分层编译(TieredComlilation)

    - 先来举个例子

    JAVA 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

    - 输出结果如下

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

    - 可以看到循环到73次附近时,速度明显加快了,循环到178次时,速度又明显加快了,这是为什么呢?

    - JVM将执行状态分为5个层次

    1. ```
    0层
    :解释执行(Interpreter) - 在0层,JVM使用解释器来直接解释Java字节码,并执行程序。这种方式简单但效率较低,因为解释器需要逐条解释字节码指令,并执行它们,每次执行时都需要对字节码进行解析 2. ``` 1层
    1
    2
    3
    4
    5
    6
    7

    :使用C1即时编译器编译执行(不带profilling)

    - 在1层,JVM会使用即时编译器(JIT)将Java字节码编译成本地机器码,然后直接执行机器码。这种方式相比于解释器,可以提供更高的执行速度。C1即时编译器适合编译执行热点代码,即被频繁执行的代码

    3. ```
    2
    :使用C1即时编译器编译执行(带基本的profilling) - 在2层,JVM会收集一些基本的执行状态数据,即profilling。例如方法的调用次数、循环的回边次数等,然后根据这些数据来决定哪些代码块需要被编译执行。这种方式可以更加精确地编译热点代码,从而提高程序的执行速度 4. ``` 3层
    1
    2
    3
    4
    5
    6
    7

    :使用C1即时编译器编译执行(带完全的profilling)

    - 在3层,JVM会收集更加详细的执行状态数据,例如内联调用的次数、方法的参数类型等,以便更好地优化代码。这种方式可以进一步提高程序的执行速度,但同时也会增加编译的开销

    5. ```
    4
    :使用C2即时编译器编译执行 - 在4层,JVM会使用更高级别的即时编译器(C2)来对代码进行优化,包括对循环、分支和递归等结构的优化。C2编译器的编译时间比C1场,但编译出来的代码执行速度更快。 profilling是指在运行过程中手机一些程序执行的状态数据,例如`方法的调用次数`、`循环的回边次数`
  • 即时编译器(JIT)和解释器的区别

    • 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
    • JIT是将一些字节码编译为机器码,并存入Code Cache,下次遇到相同的代码,直接执行,无需再次编译
    • 解释器是将字节码解释为针对所有平台都通用的机器码
    • JIT会根据平台类型,生成平台特定的机器码
  • 对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采用解释器执行的方法运行;

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

  • 执行效率上简单比较一下:Interceptor < C1 < C2

  • 上面代码中最后的耗时都在

    1
    300

    附近,这是C2即时编译器做了逃逸分析,因为上面的代码中,我们仅仅是

    1
    创建

    了Object对象,而并没有

    1
    使用

    它,也就是没有

    1
    逃逸

    出当前作用域

    • 在进行逃逸分析时,JVM会分析对象是否可能被线程外的代码引用,如果对象不会逃逸出当前方法的作用域,那么JVM会将对象的分配优化为栈上分配,从而避免了堆内存的分配和垃圾回收的压力。
  • 将对象分配在栈上的

    1
    优点

    是:

    1. 快速分配和回收:栈内存的分配和回收都非常快,比堆内存要快得多。如果对象可以在栈上分配,那么它的分配和回收都可以更快,从而提高程序的性能。
    2. 减少垃圾回收:在Java中,对象的分配和回收是由垃圾回收器来完成的。如果对象可以在栈上分配,那么它就不会对堆内存的使用和垃圾回收产生影响,从而可以减少垃圾回收的频率和时间,提高程序的性能。
  • 我们可以添加VM参数

    1
    -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
    201
    PLAINTEXT
    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
    6
    JAVA
    private static int square(int i) {
    return i * i;
    }

    System.out.println(square(9));
  • 如果发现square是热点方法,且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝,病粘贴到调用者的位置

    1
    2
    JAVA
    System.out.println(9 * 9);
  • 还能够进行常量折叠(constant folding)的优化

    1
    2
    JAVA
    System.out.println(81);
  • 下面来验证一下,还是输出耗时

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    JAVA
    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方法被标记为了热点代码
    img

  • 同时也可以禁止某个方法的内联

    1
    -XX:CompileCommand=dontinline,*JIT2.square

    ,不能进行常量折叠优化了,速度不会到达0

    1
    2
    3
    4
    5
    6
    7
    8
    9
    JAVA

    ···

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

字段优化

  • JMH基准测试参考:https://openjdk.org/projects/code-tools/jmh/

  • 添加如下依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    XML
    <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
    53
    JAVA
    @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的方法内联

    1
    @CompilerControl(CompilerControl.Mode.INLINE)

    ,测试结果如下

    1
    2
    3
    4
    5
    PLAINTEXT
    Benchmark Mode Cnt Score Error Units
    Benchmark1.test1 thrpt 5 2830851.513 ± 68534.850 ops/s
    Benchmark1.test2 thrpt 5 2844317.417 ± 8097.137 ops/s
    Benchmark1.test3 thrpt 5 2849940.840 ± 7190.091 ops/s
  • 我们这里重点关注的是Score,现在开启了doSum的内联,这三种遍历方式的性能没有显著差异

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

    1
    2
    3
    4
    5
    PLAINTEXT
    Benchmark Mode Cnt Score Error Units
    Benchmark1.test1 thrpt 5 313751.710 ± 34874.348 ops/s
    Benchmark1.test2 thrpt 5 388759.125 ± 90456.387 ops/s
    Benchmark1.test3 thrpt 5 394614.041 ± 50721.161 ops/s
  • 这三种遍历方式的性能与之前相比,都下降了一个数量级,test2和test3的性能差异不大,test1的性能明显要差一点,这是为什么呢?

  • 因为doSum方法是否内联,会影响elements成员变量的读取的优化

    • 如果doSum方法内联了,那么刚刚的test1方法会被优化成下面的样子(伪代码)

      1
      2
      3
      4
      5
      6
      7
      8
      JAVA
      @Benchmark
      public void test1() {
      // elements.length 首次读取会缓存起来 -> int[] local
      for (int i = 0; i < elements.length; i++) { // 后续 999 次 求长度 <- local
      sum += elements[i]; // 1000 次取下标 i 的元素 <- local
      }
      }
    • 如果doSum方法被内联,则循环中的每次对elements数组的访问都可以被优化,编译器可以将数组长度的读取操作提到循环外部,将elements数组的引用保存在本地变量中,从而避免了循环中每次访问数组引用的开销。这样,循环中只需要进行一次数组长度的读取,以及1000次对数组元素的访问操作,可以节省1999次对数组引用的访问。

    • 如果doSum方法没有被内联,则循环中的每次对elements数组的访问都需要通过方法调用来完成,这会导致每次循环中都需要进行一次对数组引用的读取操作,因此不能进行上述优化。

反射优化

  • 示例代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    JAVA
    import java.lang.reflect.Method;

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

    public static void main(String[] args) throws Exception {
    Method foo = Reflect1.class.getMethod("foo");
    for (int i = 0; i <= 16; i++) {
    System.out.printf("%d\t", i);
    foo.invoke(null);
    }
    System.in.read();
    }
    }
  • 首先定义了一个名为foo的静态方法,该方法只是简单地输出一条字符串。

  • 然后在main方法中,使用Reflect1类的getMethod方法获取名为fooMethod对象,以便之后进行反射调用。

  • 接着使用循环调用反射方法,循环次数从0到16,每次循环都调用反射获取的Method对象的invoke方法,传入null作为静态方法的调用者。因为foo方法是静态方法,所以调用者可以为null。

  • 最后使用System.in.read()方法暂停程序的运行,以便我们可以观察程序的输出结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
JAVA
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
    2
    JAVA
    private static int inflationThreshold = 15;
  • 而第16次调用则采用了GeneratedMethodAccessor1实现的MethodAccessor,这个实现类通常是使用Java字节码动态生成的,因此方法调用的性能比NativeMethodAccessorImpl更好。这是因为在第15次调用时,生成了一个新的MethodAccessorImpl实现类(MethodAccessorGenerator),并在下一次方法调用时使用该实现类,即第16次调用。