1. 类文件结构

  1. public class HelloWorld {
  2. public static void main(String[] args) {
  3. System.out.println("hello world");
  4. }
  5. }
  1. ruanrenzhao@MacBook-Pro constantpool % od -t xC HelloWorld.class
  2. 0000000 ca fe ba be 00 00 00 34 00 1f 0a 00 06 00 11 09
  3. 0000020 00 12 00 13 08 00 14 0a 00 15 00 16 07 00 17 07
  4. 0000040 00 18 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29
  5. 0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e
  6. 0000100 75 6d 62 65 72 54 61 62 6c 65 01 00 04 6d 61 69
  7. 0000120 6e 01 00 16 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67
  8. 0000140 2f 53 74 72 69 6e 67 3b 29 56 01 00 10 4d 65 74
  9. 0000160 68 6f 64 50 61 72 61 6d 65 74 65 72 73 01 00 04
  10. 0000200 61 72 67 73 01 00 0a 53 6f 75 72 63 65 46 69 6c
  11. 0000220 65 01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64 2e 6a
  12. 0000240 61 76 61 0c 00 07 00 08 07 00 19 0c 00 1a 00 1b
  13. 0000260 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64 07 00
  14. 0000300 1c 0c 00 1d 00 1e 01 00 37 6f 72 67 2f 6d 61 73
  15. 0000320 74 65 72 79 6f 75 72 73 65 6c 66 2f 74 75 74 6f
  16. 0000340 72 69 61 6c 2f 6a 76 6d 2f 63 6f 6e 73 74 61 6e
  17. 0000360 74 70 6f 6f 6c 2f 48 65 6c 6c 6f 57 6f 72 6c 64
  18. 0000400 01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a
  19. 0000420 65 63 74 01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f
  20. 0000440 53 79 73 74 65 6d 01 00 03 6f 75 74 01 00 15 4c
  21. 0000460 6a 61 76 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72
  22. 0000500 65 61 6d 3b 01 00 13 6a 61 76 61 2f 69 6f 2f 50
  23. 0000520 72 69 6e 74 53 74 72 65 61 6d 01 00 07 70 72 69
  24. 0000540 6e 74 6c 6e 01 00 15 28 4c 6a 61 76 61 2f 6c 61
  25. 0000560 6e 67 2f 53 74 72 69 6e 67 3b 29 56 00 21 00 05
  26. 0000600 00 06 00 00 00 00 00 02 00 01 00 07 00 08 00 01
  27. 0000620 00 09 00 00 00 1d 00 01 00 01 00 00 00 05 2a b7
  28. 0000640 00 01 b1 00 00 00 01 00 0a 00 00 00 06 00 01 00
  29. 0000660 00 00 0c 00 09 00 0b 00 0c 00 02 00 09 00 00 00
  30. 0000700 25 00 02 00 01 00 00 00 09 b2 00 02 12 03 b6 00
  31. 0000720 04 b1 00 00 00 01 00 0a 00 00 00 0a 00 02 00 00
  32. 0000740 00 0f 00 08 00 10 00 0d 00 00 00 05 01 00 0e 00
  33. 0000760 00 00 01 00 0f 00 00 00 02 00 10
  34. 0000773
  1. u4 magic
  2. u2 minor_version;
  3. u2 major_version;
  4. u2 constant_pool_count;
  5. cp_info constant_pool[constant_pool_count-1];
  6. u2 access_flags;
  7. u2 this_class;
  8. u2 super_class;
  9. u2 interfaces_count;
  10. u2 interfaces[interfaces_count];
  11. u2 fields_count;
  12. field_info fields[fields_count];
  13. u2 methods_count;
  14. method_info methods[methods_count];
  15. u2 attributes_count;
  16. attribute_info attributes[attributes_count];

2. 字节码指令

2.1 javap 工具

  1. javap -v HelloWorld.class

2.2 图解执行流程

  1. public class ByteCodeAnalysis {
  2. public static void main(String[] args) {
  3. int a = 10;
  4. int b = Short.MAX_VALUE + 1;
  5. int c = a + b;
  6. System.out.println(c);
  7. }
  8. }
  1. ruanrenzhao@MacBook-Pro bytecode % javap -v ByteCodeAnalysis.class
  2. // 类的基本信息
  3. Classfile /Users/ruanrenzhao/IdeaProjects/masteryourself/tutorial/tutorial-java/tutorial-jvm/target/classes/org/masteryourself/tutorial/jvm/bytecode/ByteCodeAnalysis.class
  4. Last modified 2022-5-4; size 692 bytes
  5. MD5 checksum 248004ee67b49c826cda4281b2cba3b8
  6. Compiled from "ByteCodeAnalysis.java"
  7. public class org.masteryourself.tutorial.jvm.bytecode.ByteCodeAnalysis
  8. minor version: 0
  9. major version: 52
  10. flags: ACC_PUBLIC, ACC_SUPER
  11. // 常量池
  12. Constant pool:
  13. #1 = Methodref #7.#25 // java/lang/Object."<init>":()V
  14. #2 = Class #26 // java/lang/Short
  15. #3 = Integer 32768
  16. #4 = Fieldref #27.#28 // java/lang/System.out:Ljava/io/PrintStream;
  17. #5 = Methodref #29.#30 // java/io/PrintStream.println:(I)V
  18. #6 = Class #31 // org/masteryourself/tutorial/jvm/bytecode/ByteCodeAnalysis
  19. #7 = Class #32 // java/lang/Object
  20. #8 = Utf8 <init>
  21. #9 = Utf8 ()V
  22. #10 = Utf8 Code
  23. #11 = Utf8 LineNumberTable
  24. #12 = Utf8 LocalVariableTable
  25. #13 = Utf8 this
  26. #14 = Utf8 Lorg/masteryourself/tutorial/jvm/bytecode/ByteCodeAnalysis;
  27. #15 = Utf8 main
  28. #16 = Utf8 ([Ljava/lang/String;)V
  29. #17 = Utf8 args
  30. #18 = Utf8 [Ljava/lang/String;
  31. #19 = Utf8 a
  32. #20 = Utf8 I
  33. #21 = Utf8 b
  34. #22 = Utf8 c
  35. #23 = Utf8 SourceFile
  36. #24 = Utf8 ByteCodeAnalysis.java
  37. #25 = NameAndType #8:#9 // "<init>":()V
  38. #26 = Utf8 java/lang/Short
  39. #27 = Class #33 // java/lang/System
  40. #28 = NameAndType #34:#35 // out:Ljava/io/PrintStream;
  41. #29 = Class #36 // java/io/PrintStream
  42. #30 = NameAndType #37:#38 // println:(I)V
  43. #31 = Utf8 org/masteryourself/tutorial/jvm/bytecode/ByteCodeAnalysis
  44. #32 = Utf8 java/lang/Object
  45. #33 = Utf8 java/lang/System
  46. #34 = Utf8 out
  47. #35 = Utf8 Ljava/io/PrintStream;
  48. #36 = Utf8 java/io/PrintStream
  49. #37 = Utf8 println
  50. #38 = Utf8 (I)V
  51. // 类的方法定义(包含虚拟机指令)
  52. {
  53. public org.masteryourself.tutorial.jvm.bytecode.ByteCodeAnalysis();
  54. descriptor: ()V
  55. flags: ACC_PUBLIC
  56. Code:
  57. stack=1, locals=1, args_size=1
  58. 0: aload_0
  59. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
  60. 4: return
  61. LineNumberTable:
  62. line 12: 0
  63. LocalVariableTable:
  64. Start Length Slot Name Signature
  65. 0 5 0 this Lorg/masteryourself/tutorial/jvm/bytecode/ByteCodeAnalysis;
  66. public static void main(java.lang.String[]);
  67. descriptor: ([Ljava/lang/String;)V
  68. flags: ACC_PUBLIC, ACC_STATIC
  69. Code:
  70. stack=2, locals=4, args_size=1
  71. 0: bipush 10
  72. 2: istore_1
  73. 3: ldc #3 // int 32768
  74. 5: istore_2
  75. 6: iload_1
  76. 7: iload_2
  77. 8: iadd
  78. 9: istore_3
  79. 10: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
  80. 13: iload_3
  81. 14: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
  82. 17: return
  83. LineNumberTable:
  84. line 15: 0
  85. line 16: 3
  86. line 17: 6
  87. line 18: 10
  88. line 19: 17
  89. LocalVariableTable:
  90. Start Length Slot Name Signature
  91. 0 18 0 args [Ljava/lang/String;
  92. 3 15 1 a I
  93. 6 12 2 b I
  94. 10 8 3 c I
  95. }
  96. SourceFile: "ByteCodeAnalysis.java"

2.2.1 常量池载入运行时常量池(这里只列举部分)

image.png

2.2.2 方法字节码载入方法区

image.png

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

stack=2, locals=4, args_size=1
image.png

2.2.4 执行引擎开始执行字节码

运行时栈帧中存储了以下内容

  • 局部变量
  • 操作数栈
  • 动态链接
  • 返回地址
  • 附加信息

    1. bipush 10

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

    2. istore_1

    将操作数栈顶数据弹出,存入局部变量表的 slot 1
    image.png
    image.png

    3. ldc #3

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

    4. istore_2

    image.png
    image.png

    5. iload_1

    image.png

    6. iload_2

    image.png

    7. iadd

    image.png
    image.png

    8. istore_3

    image.png
    image.png

    9. getstatic #4

    image.png
    image.png

    10. iload_3

    image.png
    image.png

    11. invokeevirtual #5

    找到常量池 #5 项
    定位到方法区 java/io/PrintStream.println:(I)V 方法
    生成新的栈帧(分配 locals、stack 等)
    传递参数,执行新栈帧中的字节码
    image.png
    执行完毕,弹出栈帧
    清除 main 操作数栈内容
    image.png

    12. return

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

    2.3 i++ 问题字节码分析

    ```java public class IncAnalysis {

    public static void main(String[] args) {

    1. int a = 10;
    2. int b = a++ + ++a + a--;
    3. // 11
    4. System.out.println(a);
    5. // 34
    6. System.out.println(b);

    }

}

  1. ```java
  2. {
  3. public org.masteryourself.tutorial.jvm.bytecode.IncAnalysis();
  4. descriptor: ()V
  5. flags: ACC_PUBLIC
  6. Code:
  7. stack=1, locals=1, args_size=1
  8. 0: aload_0
  9. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
  10. 4: return
  11. LineNumberTable:
  12. line 12: 0
  13. LocalVariableTable:
  14. Start Length Slot Name Signature
  15. 0 5 0 this Lorg/masteryourself/tutorial/jvm/bytecode/IncAnalysis;
  16. public static void main(java.lang.String[]);
  17. descriptor: ([Ljava/lang/String;)V
  18. flags: ACC_PUBLIC, ACC_STATIC
  19. Code:
  20. stack=2, locals=3, args_size=1
  21. // 将 10 压入操作数栈
  22. 0: bipush 10
  23. // 将操作数栈顶数据弹出,存入局部变量表的 slot 1(10)
  24. 2: istore_1
  25. // 从局部变量表的 slot 1 加载数据到操作数栈中(10)
  26. 3: iload_1
  27. // 注意 iinc 指令是直接在局部变量 slot 上进行计算,不需要经过操作数栈(此时 slot 1 数据为 11)
  28. 4: iinc 1, 1
  29. // 注意 iinc 指令是直接在局部变量 slot 上进行计算,不需要经过操作数栈(此时 slot 1 数据为 12)
  30. 7: iinc 1, 1
  31. // 从局部变量表的 slot 1 加载数据到操作数栈中(12)
  32. 10: iload_1
  33. // 在操作数栈上进行加法运算(结果是 22)
  34. 11: iadd
  35. // 从局部变量表的 slot 1 加载数据到操作数栈中(12)
  36. 12: iload_1
  37. // 注意 iinc 指令是直接在局部变量 slot 上进行计算,需要经过操作数栈(此时 slot 1 数据为 11)
  38. 13: iinc 1, -1
  39. // 在操作数栈上进行加法运算(结果是 34)
  40. 16: iadd
  41. // 将操作数栈顶数据弹出,存入局部变量表的 slot 2(34)
  42. 17: istore_2
  43. 18: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
  44. 21: iload_1
  45. // 打印 slot 1(a) 的结果为 11
  46. 22: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
  47. 25: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
  48. 28: iload_2
  49. // 打印 slot 2(b) 的结果为 34
  50. 29: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
  51. 32: return
  52. LineNumberTable:
  53. line 15: 0
  54. line 16: 3
  55. line 17: 18
  56. line 18: 25
  57. line 19: 32
  58. LocalVariableTable:
  59. Start Length Slot Name Signature
  60. 0 33 0 args [Ljava/lang/String;
  61. 3 30 1 a I
  62. 18 15 2 b I
  63. }

2.4 条件判断指令

  1. public class ConditionAnalysis {
  2. public static void main(String[] args) {
  3. int a = 0;
  4. if (a == 0) {
  5. a = 10;
  6. } else {
  7. a = 20;
  8. }
  9. }
  10. }
  1. public static void main(java.lang.String[]);
  2. Code:
  3. // 将常量 0 压入操作数栈中
  4. 0: iconst_0
  5. // 将操作数栈顶数据弹出,存入局部变量表的 slot 1(赋值给 a)
  6. 1: istore_1
  7. // 从局部变量表的 slot 1 加载数据到操作数栈中
  8. 2: iload_1
  9. // 判断是否 !=0,如果是跳转到 12 行
  10. 3: ifne 12
  11. // 将 10 压入操作数栈中
  12. 6: bipush 10
  13. // 将操作数栈顶数据弹出,存入局部变量表的 slot 1(赋值给 a)
  14. 8: istore_1
  15. // 跳转到 15 行结束程序
  16. 9: goto 15
  17. // 将 20 压入操作数栈中
  18. 12: bipush 20
  19. // 将操作数栈顶数据弹出,存入局部变量表的 slot 1(赋值给 a)
  20. 14: istore_1
  21. 15: return

2.5 循环控制指令

  1. public class WhileAnalysis {
  2. public static void main(String[] args) {
  3. int a = 0;
  4. while (a < 10) {
  5. a++;
  6. }
  7. }
  8. }
  1. public static void main(java.lang.String[]);
  2. Code:
  3. 0: iconst_0
  4. 1: istore_1
  5. 2: iload_1
  6. 3: bipush 10
  7. // 判断两个 int 是否 >=,如果是跳转到 14 行结束程序
  8. 5: if_icmpge 14
  9. // 如果不是,在局部变量表上进行 iinc 操作
  10. 8: iinc 1, 1
  11. // 注意这里是跳转到第 2 行,重新从局部变量上读取数据然后与 10 进行比较
  12. 11: goto 2
  13. 14: return

2.6 构造方法

2.6.1 <cinit>()V

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

编译器会按从上至下的顺序,收集所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方法 <cinit>()V
<cinit>()V 方法会在类加载的初始化阶段被调用

  1. static {};
  2. Code:
  3. 0: bipush 10
  4. 2: putstatic #2 // Field i:I
  5. 5: bipush 20
  6. 7: putstatic #2 // Field i:I
  7. 10: bipush 30
  8. 12: putstatic #2 // Field i:I
  9. 15: return

2.6.2 <init>()V

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

编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在最后

  1. public org.masteryourself.tutorial.jvm.bytecode.ConstructAnalysis(java.lang.String, int);
  2. Code:
  3. 0: aload_0
  4. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
  5. 4: aload_0
  6. 5: ldc #2 // String s1
  7. 7: putfield #3 // Field a:Ljava/lang/String;
  8. 10: aload_0
  9. 11: bipush 20
  10. 13: putfield #4 // Field b:I
  11. 16: aload_0
  12. 17: bipush 10
  13. 19: putfield #4 // Field b:I
  14. 22: aload_0
  15. 23: ldc #5 // String s2
  16. 25: putfield #3 // Field a:Ljava/lang/String;
  17. 28: aload_0
  18. 29: aload_1
  19. 30: putfield #3 // Field a:Ljava/lang/String;
  20. 33: aload_0
  21. 34: iload_2
  22. 35: putfield #4 // Field b:I
  23. 38: return

2.7 方法调用

  1. public class MethodInvokeAnalysis {
  2. public MethodInvokeAnalysis() {
  3. }
  4. private void test1() {
  5. }
  6. private final void test2() {
  7. }
  8. public void test3() {
  9. }
  10. public static void test4() {
  11. }
  12. public static void main(String[] args) {
  13. MethodInvokeAnalysis d = new MethodInvokeAnalysis();
  14. d.test1();
  15. d.test2();
  16. d.test3();
  17. d.test4();
  18. MethodInvokeAnalysis.test4();
  19. }
  20. }

new 是创建【对象】,给对象分配堆内存,执行成功会将【对象引用】压入操作数栈

dup 是赋值操作数栈栈顶的内容,本例即为【对象引用】,为什么需要两份引用呢,一个是要配合 invokespecial 调用该对象的构造方法 <init>:()V (会消耗掉栈顶一个引用),另一个要配合 astore_1 赋值给局部变量

最终方法(final),私有方法(private),构造方法都是由 invokespecial 指令来调用,属于静态绑定

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

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

比较有意思的是 d.test4() 是通过【对象引用】调用一个静态方法,可以看到在调用 invokestatic 之前执行了 pop 指令,把【对象引用】从操作数栈弹掉了

还有一个执行 invokespecial 的情况是通过 super 调用父类方法

  1. public static void main(java.lang.String[]);
  2. Code:
  3. 0: new #2 // class org/masteryourself/tutorial/jvm/bytecode/MethodInvokeAnalysis
  4. 3: dup
  5. 4: invokespecial #3 // Method "<init>":()V
  6. 7: astore_1
  7. 8: aload_1
  8. 9: invokespecial #4 // Method test1:()V
  9. 12: aload_1
  10. 13: invokespecial #5 // Method test2:()V
  11. 16: aload_1
  12. 17: invokevirtual #6 // Method test3:()V
  13. 20: aload_1
  14. 21: pop
  15. 22: invokestatic #7 // Method test4:()V
  16. 25: invokestatic #7 // Method test4:()V
  17. 28: return

2.8 多态原理

因为普通成员方法需要在运行时才能确定具体的内容,所以虚拟机需要调用 invokevirtual 指令

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

    2.9 异常原理

    2.9.1 try catch

    ```java public class TryCatchAnalysis {

    public static void main(String[] args) {

    1. int i = 0;
    2. try {
    3. i = 10;
    4. } catch (Exception e) {
    5. i = 20;
    6. }

    }

}

  1. 可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号
  2. 8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置
  3. ```java
  4. public static void main(java.lang.String[]);
  5. descriptor: ([Ljava/lang/String;)V
  6. flags: ACC_PUBLIC, ACC_STATIC
  7. Code:
  8. stack=1, locals=3, args_size=1
  9. 0: iconst_0
  10. 1: istore_1
  11. 2: bipush 10
  12. 4: istore_1
  13. 5: goto 12
  14. 8: astore_2
  15. 9: bipush 20
  16. 11: istore_1
  17. 12: return
  18. Exception table:
  19. from to target type
  20. 2 5 8 Class java/lang/Exception
  21. LineNumberTable:
  22. line 15: 0
  23. line 17: 2
  24. line 20: 5
  25. line 18: 8
  26. line 19: 9
  27. line 21: 12
  28. LocalVariableTable:
  29. Start Length Slot Name Signature
  30. 9 3 2 e Ljava/lang/Exception;
  31. 0 13 0 args [Ljava/lang/String;
  32. 2 11 1 i I
  33. StackMapTable: number_of_entries = 2
  34. frame_type = 255 /* full_frame */
  35. offset_delta = 8
  36. locals = [ class "[Ljava/lang/String;", int ]
  37. stack = [ class java/lang/Exception ]
  38. frame_type = 3 /* same */

2.9.2 多个 single catch

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

因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用

  1. public static void main(java.lang.String[]);
  2. descriptor: ([Ljava/lang/String;)V
  3. flags: ACC_PUBLIC, ACC_STATIC
  4. Code:
  5. stack=1, locals=3, args_size=1
  6. 0: iconst_0
  7. 1: istore_1
  8. 2: bipush 10
  9. 4: istore_1
  10. 5: goto 26
  11. 8: astore_2
  12. 9: bipush 30
  13. 11: istore_1
  14. 12: goto 26
  15. 15: astore_2
  16. 16: bipush 40
  17. 18: istore_1
  18. 19: goto 26
  19. 22: astore_2
  20. 23: bipush 50
  21. 25: istore_1
  22. 26: return
  23. Exception table:
  24. from to target type
  25. 2 5 8 Class java/lang/ArithmeticException
  26. 2 5 15 Class java/lang/NullPointerException
  27. 2 5 22 Class java/lang/Exception
  28. LineNumberTable:
  29. line 15: 0
  30. line 17: 2
  31. line 24: 5
  32. line 18: 8
  33. line 19: 9
  34. line 24: 12
  35. line 20: 15
  36. line 21: 16
  37. line 24: 19
  38. line 22: 22
  39. line 23: 23
  40. line 25: 26
  41. LocalVariableTable:
  42. Start Length Slot Name Signature
  43. 9 3 2 e Ljava/lang/ArithmeticException;
  44. 16 3 2 e Ljava/lang/NullPointerException;
  45. 23 3 2 e Ljava/lang/Exception;
  46. 0 27 0 args [Ljava/lang/String;
  47. 2 25 1 i I
  48. StackMapTable: number_of_entries = 4
  49. frame_type = 255 /* full_frame */
  50. offset_delta = 8
  51. locals = [ class "[Ljava/lang/String;", int ]
  52. stack = [ class java/lang/ArithmeticException ]
  53. frame_type = 70 /* same_locals_1_stack_item */
  54. stack = [ class java/lang/NullPointerException ]
  55. frame_type = 70 /* same_locals_1_stack_item */
  56. stack = [ class java/lang/Exception ]
  57. frame_type = 3 /* same */

2.9.3 multi catch

  1. public class MultiCatchAnalysis {
  2. public static void main(String[] args) {
  3. try {
  4. Method test = MultiCatchAnalysis.class.getMethod("test");
  5. test.invoke(null);
  6. } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
  7. e.printStackTrace();
  8. }
  9. }
  10. public static void test() {
  11. System.out.println("ok");
  12. }
  13. }
  1. public static void main(java.lang.String[]);
  2. descriptor: ([Ljava/lang/String;)V
  3. flags: ACC_PUBLIC, ACC_STATIC
  4. Code:
  5. stack=3, locals=2, args_size=1
  6. 0: ldc #2 // class org/masteryourself/tutorial/jvm/bytecode/ex/MultiCatchAnalysis
  7. 2: ldc #3 // String test
  8. 4: iconst_0
  9. 5: anewarray #4 // class java/lang/Class
  10. 8: invokevirtual #5 // Method java/lang/Class.getMethod:(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;
  11. 11: astore_1
  12. 12: aload_1
  13. 13: aconst_null
  14. 14: iconst_0
  15. 15: anewarray #6 // class java/lang/Object
  16. 18: invokevirtual #7 // Method java/lang/reflect/Method.invoke:(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
  17. 21: pop
  18. 22: goto 30
  19. 25: astore_1
  20. 26: aload_1
  21. 27: invokevirtual #11 // Method java/lang/ReflectiveOperationException.printStackTrace:()V
  22. 30: return
  23. Exception table:
  24. from to target type
  25. 0 22 25 Class java/lang/NoSuchMethodException
  26. 0 22 25 Class java/lang/IllegalAccessException
  27. 0 22 25 Class java/lang/reflect/InvocationTargetException
  28. LineNumberTable:
  29. line 19: 0
  30. line 20: 12
  31. line 23: 22
  32. line 21: 25
  33. line 22: 26
  34. line 24: 30
  35. LocalVariableTable:
  36. Start Length Slot Name Signature
  37. 12 10 1 test Ljava/lang/reflect/Method;
  38. 26 4 1 e Ljava/lang/ReflectiveOperationException;
  39. 0 31 0 args [Ljava/lang/String;
  40. StackMapTable: number_of_entries = 2
  41. frame_type = 89 /* same_locals_1_stack_item */
  42. stack = [ class java/lang/ReflectiveOperationException ]
  43. frame_type = 4 /* same */
  44. public static void test();
  45. descriptor: ()V
  46. flags: ACC_PUBLIC, ACC_STATIC
  47. Code:
  48. stack=2, locals=0, args_size=0
  49. 0: getstatic #12 // Field java/lang/System.out:Ljava/io/PrintStream;
  50. 3: ldc #13 // String ok
  51. 5: invokevirtual #14 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  52. 8: return
  53. LineNumberTable:
  54. line 27: 0
  55. line 28: 8

2.9.4 finally

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

可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程

  1. public static void main(java.lang.String[]);
  2. descriptor: ([Ljava/lang/String;)V
  3. flags: ACC_PUBLIC, ACC_STATIC
  4. Code:
  5. stack=1, locals=4, args_size=1
  6. 0: iconst_0
  7. 1: istore_1
  8. 2: bipush 10
  9. 4: istore_1
  10. 5: bipush 30
  11. 7: istore_1
  12. 8: goto 27
  13. 11: astore_2
  14. 12: bipush 20
  15. 14: istore_1
  16. 15: bipush 30
  17. 17: istore_1
  18. 18: goto 27
  19. 21: astore_3
  20. 22: bipush 30
  21. 24: istore_1
  22. 25: aload_3
  23. 26: athrow
  24. 27: return
  25. Exception table:
  26. from to target type
  27. 2 5 11 Class java/lang/Exception
  28. 2 5 21 any
  29. 11 15 21 any
  30. LineNumberTable:
  31. line 15: 0
  32. line 17: 2
  33. line 21: 5
  34. line 22: 8
  35. line 18: 11
  36. line 19: 12
  37. line 21: 15
  38. line 22: 18
  39. line 21: 21
  40. line 22: 25
  41. line 23: 27
  42. LocalVariableTable:
  43. Start Length Slot Name Signature
  44. 12 3 2 e Ljava/lang/Exception;
  45. 0 28 0 args [Ljava/lang/String;
  46. 2 26 1 i I
  47. StackMapTable: number_of_entries = 3
  48. frame_type = 255 /* full_frame */
  49. offset_delta = 11
  50. locals = [ class "[Ljava/lang/String;", int ]
  51. stack = [ class java/lang/Exception ]
  52. frame_type = 73 /* same_locals_1_stack_item */
  53. stack = [ class java/lang/Throwable ]
  54. frame_type = 5 /* same */

2.10 synchronized

方法级别的 synchronized 不会在字节码指令中有所体现

  1. public class SyncAnalysis {
  2. public static void main(String[] args) {
  3. Object lock = new Object();
  4. synchronized (lock) {
  5. System.out.println("ok");
  6. }
  7. }
  8. }
  1. public static void main(java.lang.String[]);
  2. descriptor: ([Ljava/lang/String;)V
  3. flags: ACC_PUBLIC, ACC_STATIC
  4. Code:
  5. stack=2, locals=4, args_size=1
  6. 0: new #2 // class java/lang/Object
  7. 3: dup
  8. 4: invokespecial #1 // Method java/lang/Object."<init>":()V
  9. 7: astore_1
  10. 8: aload_1
  11. 9: dup
  12. 10: astore_2
  13. // 正常加锁 + 解锁
  14. 11: monitorenter
  15. 12: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
  16. 15: ldc #4 // String ok
  17. 17: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  18. 20: aload_2
  19. 21: monitorexit
  20. 22: goto 30
  21. // 异常解锁
  22. 25: astore_3
  23. 26: aload_2
  24. 27: monitorexit
  25. 28: aload_3
  26. 29: athrow
  27. 30: return
  28. Exception table:
  29. // 12~22 或者 25~28 行出现任何异常,都会跳转到 25 行
  30. from to target type
  31. 12 22 25 any
  32. 25 28 25 any
  33. LineNumberTable:
  34. line 15: 0
  35. line 16: 8
  36. line 17: 12
  37. line 18: 20
  38. line 19: 30
  39. LocalVariableTable:
  40. Start Length Slot Name Signature
  41. 0 31 0 args [Ljava/lang/String;
  42. 8 23 1 lock Ljava/lang/Object;
  43. StackMapTable: number_of_entries = 2
  44. frame_type = 255 /* full_frame */
  45. offset_delta = 25
  46. locals = [ class "[Ljava/lang/String;", class java/lang/Object, class java/lang/Object ]
  47. stack = [ class java/lang/Throwable ]
  48. frame_type = 250 /* chop */
  49. offset_delta = 4

3. 编译期处理

所谓的 语法糖 ,其实就是指 java 编译器把 .java 源码编译为 .class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利

注意,以下代码的分析,借助了 javap 工具,idea 的反编译功能,idea 插件 jclasslib 等工具。另外, 编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了几乎等价 的 java 源码方式,并不是编译器还会转换出中间的 java 源码

3.1 默认构造器

  1. public class Candy1 {
  2. }
  3. // 编译成 class 后的代码
  4. public class Candy1 {
  5. // 这个无参构造器是java编译器帮我们加上的
  6. public Candy1() {
  7. // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object." <init>":()V
  8. super();
  9. }
  10. }

3.2 自动拆装箱

这个特性是 JDK 5 开始加入的

  1. public class Candy2 {
  2. public static void main(String[] args) {
  3. Integer x = 1;
  4. int y = x;
  5. }
  6. }
  7. // 等价于
  8. public class Candy2 {
  9. public static void main(String[] args) {
  10. //基本类型赋值给包装类型,称为装箱
  11. Integer x = Integer.valueOf(1);
  12. //包装类型赋值给基本类型,称谓拆箱
  13. int y = x.intValue();
  14. }
  15. }

3.3 泛型集合取值

泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行 泛型擦除 的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理

  1. public class Candy3 {
  2. public static void main(String[] args) {
  3. List<Integer> list = new ArrayList<>();
  4. // 实际调用的是 List.add(Object e)
  5. list.add(10);
  6. // 实际调用的是 Object obj = List.get(int index);
  7. Integer x = list.get(0);
  8. }
  9. }

泛型擦除的是字节码上的泛型信息, 但方法入参和返回值的泛型依旧可以获取

  1. public class GenericInfo {
  2. public Set<Integer> test(List<String> list, Map<Integer, Object> map) {
  3. return null;
  4. }
  5. /**
  6. * 输出结果:
  7. * 原始类型 - interface java.util.List
  8. * 泛型参数[0] - class java.lang.String
  9. * 原始类型 - interface java.util.Map
  10. * 泛型参数[0] - class java.lang.Integer
  11. * 泛型参数[1] - class java.lang.Object
  12. */
  13. public static void main(String[] args) throws Exception {
  14. Method test = GenericInfo.class.getMethod("test", List.class, Map.class);
  15. Type[] types = test.getGenericParameterTypes();
  16. for (Type type : types) {
  17. if (type instanceof ParameterizedType) {
  18. ParameterizedType parameterizedType = (ParameterizedType) type;
  19. System.out.println("原始类型 - " + parameterizedType.getRawType());
  20. Type[] arguments = parameterizedType.getActualTypeArguments();
  21. for (int i = 0; i < arguments.length; i++) {
  22. System.out.printf("泛型参数[%d] - %s\n", i, arguments[i]);
  23. }
  24. }
  25. }
  26. }
  27. }

3.4 可变参数

可变参数 String… args 其实是一个 String[] args ,从代码中的赋值语句中就可以看出来

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

  1. public class Candy4 {
  2. public static void foo(String... args) {
  3. String[] array = args; // 直接赋值
  4. System.out.println(array);
  5. }
  6. public static void main(String[] args) {
  7. foo("hello", "world");
  8. }
  9. }
  10. // 等价于
  11. public class Candy4 {
  12. public static void foo(String[] args) {
  13. String[] array = args; // 直接赋值
  14. System.out.println(array);
  15. }
  16. public static void main(String[] args) {
  17. foo(new String[]{"hello", "world"});
  18. }
  19. }

3.5 foreach 循环

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

  1. public class Candy5_1 {
  2. public static void main(String[] args) {
  3. int[] array = {1, 2, 3, 4, 5}; // 数组赋初值的简化写法也是语法糖哦 f
  4. for (int e : array) {
  5. System.out.println(e);
  6. }
  7. }
  8. }
  9. // 等价于
  10. public class Candy5_1 {
  11. public Candy5_1 {}
  12. public static void main(String[] args) {
  13. int[] arr = new int[]{1, 2, 3, 4, 5};
  14. for(int i=0; i<arr.length; ++i) {
  15. int x = arr[i];
  16. System.out.println(x);
  17. }
  18. }
  19. }
  1. public class Candy5_2 {
  2. public static void main(String[] args) {
  3. List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
  4. for (Integer i : list) {
  5. System.out.println(i);
  6. }
  7. }
  8. }
  9. // 等价于
  10. public class Candy5_2 {
  11. public Candy5_2() {
  12. }
  13. public static void main(String[] args) {
  14. List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
  15. Iterator iter = list.iterator();
  16. while (iter.hasNext()) {
  17. Integer e = (Integer) iter.next();
  18. System.out.println(e);
  19. }
  20. }
  21. }

3.6 switch 字符串

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

switch 配合 String 和枚举使用时,变量不能为 null,因为会调用对象的某些方法进行转换

  1. public class Candy6_1 {
  2. public static void choose(String str) {
  3. switch (str) {
  4. case "hello": {
  5. System.out.println("h");
  6. break;
  7. }
  8. case "world": {
  9. System.out.println("w");
  10. break;
  11. }
  12. }
  13. }
  14. }
  15. // 转换后
  16. public class Candy6_1 {
  17. public Candy6_1() {
  18. }
  19. public static void choose(String str) {
  20. byte x = -1;
  21. switch (str.hashCode()) {
  22. case 99162322: // hello 的 hashCode
  23. if (str.equals("hello")) {
  24. x = 0;
  25. }
  26. break;
  27. case 113318802: // world 的 hashCode
  28. if (str.equals("world")) {
  29. x = 1;
  30. }
  31. }
  32. switch (x) {
  33. case 0:
  34. System.out.println("h");
  35. break;
  36. case 1:
  37. System.out.println("w");
  38. }
  39. }
  40. }

可以看到,执行了两遍 switch,第一遍是根据字符串的 hashCode 和 equals 将字符串的转换为相应 byte 类型,第二遍才是利用 byte 执行进行比较。

为什么第一遍时必须既比较 hashCode,又利用 equals 比较呢?hashCode 是为了提高效率,减少可能的比较;而 equals 是为了防止 hashCode 冲突,例如 BMC. 这两个字符串的 hashCode 值都是 2123 ,如果有如下代码:

  1. public class Candy6_2 {
  2. public static void choose(String str) {
  3. switch (str) {
  4. case "BM": {
  5. System.out.println("h");
  6. break;
  7. }
  8. case "C.": {
  9. System.out.println("w");
  10. break;
  11. }
  12. }
  13. }
  14. }
  15. // 转换后
  16. public class Candy6_2 {
  17. public Candy6_2() {
  18. }
  19. public static void choose(String str) {
  20. byte x = -1;
  21. switch (str.hashCode()) {
  22. case 2123: // hashCode 值可能相同,需要进一步用 equals 比较
  23. if (str.equals("C.")) {
  24. x = 1;
  25. } else if (str.equals("BM")) {
  26. x = 0;
  27. }
  28. default:
  29. switch (x) {
  30. case 0:
  31. System.out.println("h");
  32. break;
  33. case 1:
  34. System.out.println("w");
  35. }
  36. }
  37. }
  38. }

3.7 switch 枚举

  1. enum Sex {
  2. MALE,
  3. FEMALE
  4. }
  5. public class Candy7 {
  6. public static void foo(Sex sex) {
  7. switch (sex) {
  8. case MALE:
  9. System.out.println("男");
  10. break;
  11. case FEMALE:
  12. System.out.println("女");
  13. break;
  14. }
  15. }
  16. }
  17. // 转换后
  18. public class Candy7 {
  19. /**
  20. * 定义一个合成类(仅 jvm 使用,对我们不可见)
  21. * 用来映射枚举的 ordinal 与数组元素的关系
  22. * 枚举的 ordinal 表示枚举对象的序号,从 0 开始
  23. * 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1
  24. */
  25. static class $MAP {
  26. // 数组大小即为枚举元素个数,里面存储case用来对比的数字
  27. static int[] map = new int[2];
  28. static {
  29. map[Sex.MALE.ordinal()] = 1;
  30. map[Sex.FEMALE.ordinal()] = 2;
  31. }
  32. }
  33. public static void foo(Sex sex) {
  34. int x = $MAP.map[sex.ordinal()];
  35. switch (x) {
  36. case 1:
  37. System.out.println("男");
  38. break;
  39. case 2:
  40. System.out.println("女");
  41. break;
  42. }
  43. }
  44. }

3.8 枚举类

  1. enum Sex {
  2. MALE,
  3. FEMALE
  4. }
  5. // 转换后
  6. public final class Sex extends Enum<Sex> {
  7. // 对应枚举类中的元素
  8. public static final Sex MALE;
  9. public static final Sex FEMALE;
  10. private static final Sex[] $VALUES;
  11. static {
  12. // 调用构造函数,传入枚举元素的值及 ordinal
  13. MALE = new Sex("MALE", 0);
  14. FEMALE = new Sex("FEMALE", 1);
  15. $VALUES = new Sex[]{MALE, FEMALE};
  16. }
  17. // 调用父类中的方法
  18. private Sex(String name, int ordinal) {
  19. super(name, ordinal);
  20. }
  21. public static Sex[] values() {
  22. return $VALUES.clone();
  23. }
  24. public static Sex valueOf(String name) {
  25. return Enum.valueOf(Sex.class, name);
  26. }
  27. }

3.9 try-with-resources

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

  1. try(资源变量 = 创建资源对象){
  2. } catch() {
  3. }

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

  1. public class Candy9 {
  2. public static void main(String[] args) {
  3. try (InputStream is = new FileInputStream("d:\\1.txt")) {
  4. System.out.println(is);
  5. } catch (IOException e) {
  6. e.printStackTrace();
  7. }
  8. }
  9. }
  10. // 等价于
  11. public class Candy9 {
  12. public Candy9() {
  13. }
  14. public static void main(String[] args) {
  15. try {
  16. InputStream is = new FileInputStream("d:\\1.txt");
  17. Throwable t = null;
  18. try {
  19. System.out.println(is);
  20. } catch (Throwable e1) {
  21. // t 是我们代码出现的异常
  22. t = e1;
  23. throw e1;
  24. } finally {
  25. // 判断了资源不为空
  26. if (is != null) {
  27. // 如果我们代码有异常
  28. if (t != null) {
  29. try {
  30. is.close();
  31. } catch (Throwable e2) {
  32. // 如果 close 出现异常,作为被压制异常添加
  33. t.addSuppressed(e2);
  34. }
  35. } else {
  36. // 如果我们代码没有异常,close 出现的异常就是最后 catch 块中的 e
  37. is.close();
  38. }
  39. }
  40. }
  41. } catch (IOException e) {
  42. e.printStackTrace();
  43. }
  44. }
  45. }

为什么要设计一个 addSuppressed(Throwable e) (添加被压制异常)的方法呢?是为了防止异常信息的丢失(想想 try-with-resources 生成的 fianlly 中如果抛出了异常)

  1. public class Test6 {
  2. public static void main(String[] args) {
  3. try (MyResource resource = new MyResource()) {
  4. int i = 1 / 0;
  5. } catch (Exception e) {
  6. e.printStackTrace();
  7. }
  8. }
  9. }
  10. class MyResource implements AutoCloseable {
  11. public void close() throws Exception {
  12. throw new Exception("close 异常");
  13. }
  14. }
  15. // 异常堆栈,两个异常都不会丢失
  16. java.lang.ArithmeticException: / by zero
  17. at test.Test6.main(Test6.java:7)
  18. Suppressed: java.lang.Exception: close 异常
  19. at test.MyResource.close(Test6.java:18)
  20. at test.Test6.main(Test6.java:6)

3.10 方法重写的桥接方法

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

  • 父子类的返回值完全一致
  • 子类返回值可以是父类返回值的子类 ```java class A {

    public Number m() {

    1. return 1;

    }

}

class B extends A {

  1. @Override
  2. // 子类 m 方法的返回值是 Integer 是父类 m 方法返回值 Number 的子类
  3. public Integer m() {
  4. return 2;
  5. }

}

// 等价于

class B extends A {

  1. public Integer m() {
  2. return 2;
  3. }
  4. // 此方法才是真正重写了父类 public Number m() 方法
  5. public synthetic bridge Number m() {
  6. // 调用 public Integer m()
  7. return m();
  8. }

}

  1. 其中桥接方法比较特殊,仅对 java 虚拟机可见,并且与原来的 public Integer m() 没有命名冲突,可以用下面反射代码来验证:
  2. ```java
  3. for (Method m : B.class.getDeclaredMethods()) {
  4. System.out.println(m);
  5. }
  6. // 会输出两个
  7. // public java.lang.Integer test.candy.B.m()
  8. // public java.lang.Number test.candy.B.m()

3.11 匿名内部类

  1. public class Candy11 {
  2. public static void main(String[] args) {
  3. Runnable runnable = new Runnable() {
  4. @Override
  5. public void run() {
  6. System.out.println("ok");
  7. }
  8. };
  9. }
  10. }
  11. // 转换后
  12. // 会额外生成一个类
  13. final class Candy11$1 implements Runnable {
  14. Candy11$1() {
  15. }
  16. public void run() {
  17. System.out.println("ok");
  18. }
  19. }
  20. public class Candy11 {
  21. public static void main(String[] args) {
  22. // 调用转换后的类
  23. Runnable runnable = new Candy11$1();
  24. }
  25. }

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

  1. public class Candy11 {
  2. public static void main(String[] args) {
  3. int x = 1;
  4. Runnable runnable = new Runnable() {
  5. @Override
  6. public void run() {
  7. System.out.println(x);
  8. }
  9. };
  10. }
  11. }
  12. // 转换后
  13. public class Candy11 {
  14. public static void main(String[] args) {
  15. int x = 1;
  16. Runnable runnable = new Runnable() {
  17. @Override
  18. public void run() {
  19. System.out.println(x);
  20. }
  21. };
  22. }
  23. }
  24. final class Candy11$1 implements Runnable {
  25. //多创建了一个变量
  26. int val$x;
  27. //变为了有参构造器
  28. public Demo8$1(int x) {
  29. this.val$x = x;
  30. }
  31. @Override
  32. public void run() {
  33. System.out.println(val$x);
  34. }
  35. }