--- title: JVM之字节码技术 date: 2022-12-09 sidebar: 'auto' tags: - JVM categories: - Java --- ## 1 类文件结构 一个简单的 HelloWorld.java 文件 ```java public class HelloWorld { public static void main(String[] args) { System.out.println("Hello World"); } } ``` 将其执行 `javac -parameters -d . HelloWorld.java` 编译后的 HelloWorld.class 文件如下(Notepad++使用十六进制进行查看) ![1670046032461](/jvm/1670046032461.png) 根据 JVM 规范,类文件结构如下,这是在 JVM 运行中的所有字节码文件都需要遵循的 ``` class-file { u4 magic; //魔数 u2 minor_version; //小版本号 u2 major_version; //大版本号 u2 constant_pool_count; //常量池中常量个数+1 cp_info constant_pool[constant_pool_count-1]; //常量池 u2 access_flags; //类的访问控制符标识(public,static,final,abstract等) u2 this_class; //该类的描述(值为对常量池的引用,引用的值为CONSTANT_Class_info) u2 super_class; //父类的描述(值为对常量池的引用,引用的值为CONSTANT_Class_info) 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; //属性数,包括InnerClasses,EnclosingMethod,SourceFile等 attribute_info attributes[attributes_count]; //属性表集合 } ``` ### 1.1 魔数 0-3 字节,表示它是否是【class】类型的文件 ![1670046677428](/jvm/1670046677428.png) ### 1.2 版本 4-7 字节,表示类的版本 00 34(52) 表示是 Java 8 ![1670046787159](/jvm/1670046787159.png) ### 1.3 常量池 8-9 字节,表示常量池长度,00 1f (31) 表示常量池有 #1- #30 项,注意 #0 项不计入,也没有值,可以发现一个 Class 文件中最多具备 65535(2^16) 个常量,它包括了以下所有类型。 ![1670047109146](/jvm/1670047109146.png) 常量池类型映射表,目前为止 JVM 一共定义了 14 种类型的常量。 | Constant Type | Value | Length(Byte) | Tip | | --------------------------- | ----- | -------------- | ------------------------------------------------------------ | | CONSTANT_Class | 7 | 2 | Class信息 | | CONSTANT_Fieldref | 9 | 4 | 成员变量信息,前2为 CONSTANT_Class,后2为 CONSTANT_NameAndType | | CONSTANT_Methodref | 10 | 4 | 方法信息,前2为 CONSTANT_Class,后2为 CONSTANT_NameAndType | | CONSTANT_InterfaceMethodref | 11 | 4 | 接口方法信息,前2为 CONSTANT_Class,后2为 CONSTANT_NameAndType | | CONSTANT_String | 8 | 2 | 字符串常量名称 | | CONSTANT_Integer | 3 | 4 | int值 | | CONSTANT_Float | 4 | 4 | float值 | | CONSTANT_Long | 5 | 8 | long值 | | CONSTANT_Double | 6 | 8 | double值 | | CONSTANT_NameAndType | 12 | 4 | 名称和类型信息,前2为 命名信息,后2为 类型信息 | | CONSTANT_Utf8 | 1 | 2 | 长度记录了该字符串的长度,通过该记录的长度来读取后续相应字节长度信息 | | CONSTANT_MethodHandle | 15 | - | 表示方法句柄(后续遇到再补充) | | CONSTANT_MethodType | 16 | - | 表示方法类型(后续遇到再补充) | | CONSTANT_InvokeDynamic | 18 | - | 表示一个动态方法调用点(后续遇到再补充) | 根据以上映射表来对其余字节码的分析 常量池中 #1 项 0a(10),由映射表得知是一个 Method 信息,其通过后续的 00 06(6) 和 00 11(17) 分别表示该方法的【所属类】引用常量池中 #6 项、【方法名】引用常量池中 #17 项。 ![1670048081295](/jvm/1670048081295.png) 常量池中 #2 项 09(9),由映射表得知是一个 Field 信息,其通过后续的 00 12(18) 和 00 13(19) 分别表示该成员变量的【所属类】引用常量池中 #18 项、【成员变量名】引用常量池中 #19 项。 ![1670048493839](/jvm/1670048493839.png) 常量池中 #3 项 08(8),由映射表得知是一个字符串常量名称,其通过后续的 00 14(20)表示其引用了常量池中 #20 项。 ![1670048714141](/jvm/1670048714141.png) 依此类推进行其他常量池的解析,这里就不演示了,了解一个解析形式即可。 以上分析结果的常量池大概就是使用反编译后的常量池结果,而且可以看到所有的字符串常量池中的信息一一对应 ``` #1 = Methodref #6.#17 // java/lang/Object."":()V #2 = Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream; #3 = String #20 // Hello World #4 = Methodref #21.#22 // java/io/PrintStream.println:(Ljava/lang/String;)V #5 = Class #23 // HelloWorld #6 = Class #24 // java/lang/Object #7 = Utf8 #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 main #12 = Utf8 ([Ljava/lang/String;)V #13 = Utf8 MethodParameters #14 = Utf8 args #15 = Utf8 SourceFile #16 = Utf8 HelloWorld.java #17 = NameAndType #7:#8 // "":()V #18 = Class #25 // java/lang/System #19 = NameAndType #26:#27 // out:Ljava/io/PrintStream; #20 = Utf8 Hello World #21 = Class #28 // java/io/PrintStream #22 = NameAndType #29:#30 // println:(Ljava/lang/String;)V #23 = Utf8 HelloWorld #24 = Utf8 java/lang/Object #25 = Utf8 java/lang/System #26 = Utf8 out #27 = Utf8 Ljava/io/PrintStream; #28 = Utf8 java/io/PrintStream #29 = Utf8 println #30 = Utf8 (Ljava/lang/String;)V ``` ### 1.4 访问标识与继承信息 解析完常量池后的8的字节,就是该部分的信息 - 2:该class类型和访问修饰符Flag - 2:通过常量池找到本类全限定名 - 2:通过常量池找到父类全限定名 - 2:表示接口数量 | Flag Name | Value | Interpretation | | -------------- | ------ | ------------------------------------------------------------ | | ACC_PUBLIC | 0x0001 | public 访问权限 | | ACC_FINAL | 0x0010 | final 修饰符 | | ACC_SUPER | 0x0020 | Treat `superclass` methods specially when invoked by the invokespecial instruction. 父类调用方式 | | ACC_INTERFACE | 0x0200 | interface 类型 | | ACC_ABSTRACT | 0x0400 | abstract 类型 | | ACC_SYNTHETIC | 0x1000 | 通过合成/生成的代码,没有源代码 | | ACC_ANNOTATION | 0x2000 | annotation 类型 | | ACC_ENUM | 0x4000 | enum 类型 | ### 1.5 Field 信息 解析完以上的信息的后续2字节,表示成员变量的数量,此案例文件并没有成员变量 | FieldType | Type | Interpretation | | ------------- | --------- | -------------- | | B | byte | | | C | char | | | D | double | | | F | float | | | I | int | | | J | long | | | L`ClassName`; | reference | 引用类型 | | S | short | | | Z | boolean | | | [ | reference | 一维数组 | ### 1.6 Method 信息 解析完以上的信息的后续2字节,表示方法的数量 一个方法由 访问修饰符,名称,参数描述,方法属性数量,方法属性组成 就是解析为以下反编译文件(使用十六进制分析过程过于庞大) ``` { public HelloWorld(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return LineNumberTable: line 1: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String Hello World 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 3: 0 line 4: 8 MethodParameters: Name Flags args } ``` ### 1.7 附加属性 解析为以下反编译文件的内容 ``` SourceFile: "HelloWorld.java" ``` 参考文献 https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html ## 2 字节码指令 ### 2.1 入门 分析方法中的字节码指令信息 构造方法对应的字节码信息如下图,对应的反编译信息如下文本块所示 ![1670054933674](/jvm/1670054933674.png) ``` public HelloWorld(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return LineNumberTable: line 1: 0 ``` 有以下对应关系 - 2a => aload_0:加载 slot 0 的局部变量,即 this,做为下面的 invokespecial 构造方法调用的参数 - b7 => invokespecial:预备调用构造方法,哪个方法呢? - 00 01:引用常量池中 #1 项,即【 Method java/lang/Object."":()V 】 - b1:表示返回 main方法对应的字节码信息如下图,对应的反编译信息如下文本块所示 ![1670055197971](/jvm/1670055197971.png) ``` public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String Hello World 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 3: 0 line 4: 8 MethodParameters: Name Flags args ``` 有以下对应关系 - b2 => getstatic:用来加载静态变量,哪个静态变量呢? - 00 02:引用常量池中 #2 项,即【Field java/lang/System.out:Ljava/io/PrintStream;】 - 12 => ldc:加载参数,哪个参数呢? - 03:引用常量池中 #3 项,即 【String Hello World】 - b6 => invokevirtual:预备调用成员方法,哪个方法呢? - 00 04:引用常量池中 #4 项,即【Method java/io/PrintStream.println:(Ljava/lang/String;)V】 - b1:表示返回 请参考 [https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5) ![1670055453251](/jvm/1670055453251.png) ### 2.2 javap 工具 自己分析类文件结构太麻烦了,Oracle 提供了 javap 工具来反编译 class 文件 使用命令 `javap -v HelloWorld.class`,查看完整的反编译信息 ``` Classfile /G:/Idea_workspace/JVM/src/main/java/HelloWorld.class Last modified 2022-12-3; size 462 bytes MD5 checksum f882f9f216fff39f1b95dd6b16b209a7 Compiled from "HelloWorld.java" public class HelloWorld minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #6.#17 // java/lang/Object."":()V #2 = Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream; #3 = String #20 // Hello World #4 = Methodref #21.#22 // java/io/PrintStream.println:(Ljava/lang/String;)V #5 = Class #23 // HelloWorld #6 = Class #24 // java/lang/Object #7 = Utf8 #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 main #12 = Utf8 ([Ljava/lang/String;)V #13 = Utf8 MethodParameters #14 = Utf8 args #15 = Utf8 SourceFile #16 = Utf8 HelloWorld.java #17 = NameAndType #7:#8 // "":()V #18 = Class #25 // java/lang/System #19 = NameAndType #26:#27 // out:Ljava/io/PrintStream; #20 = Utf8 Hello World #21 = Class #28 // java/io/PrintStream #22 = NameAndType #29:#30 // println:(Ljava/lang/String;)V #23 = Utf8 HelloWorld #24 = Utf8 java/lang/Object #25 = Utf8 java/lang/System #26 = Utf8 out #27 = Utf8 Ljava/io/PrintStream; #28 = Utf8 java/io/PrintStream #29 = Utf8 println #30 = Utf8 (Ljava/lang/String;)V { public HelloWorld(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return LineNumberTable: line 1: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String Hello World 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 3: 0 line 4: 8 MethodParameters: Name Flags args } SourceFile: "HelloWorld.java" ``` ### 2.3 图解方法执行流程 #### 1) 原始 Java 代码 ```java package com.zhuhjay.demo3; public class Demo3_1 { public static void main(String[] args) { int a = 10; int b = Short.MAX_VALUE + 1; int c = a + b; System.out.println(c); } } ``` #### 2) 编译后的字节码文件 使用命令 `javap -v Demo3_1.class` 进行反编译,查看字节码 ``` Classfile /G:/Idea_workspace/JVM/target/classes/com/zhuhjay/demo3/Demo3_1.class Last modified 2022-12-3; size 619 bytes MD5 checksum 2f036be3ec6ee9204c53f83b17dacc1f Compiled from "Demo3_1.java" public class com.zhuhjay.demo3.Demo3_1 minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #7.#25 // java/lang/Object."":()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/zhuhjay/demo3/Demo3_1 #7 = Class #32 // java/lang/Object #8 = Utf8 #9 = Utf8 ()V #10 = Utf8 Code #11 = Utf8 LineNumberTable #12 = Utf8 LocalVariableTable #13 = Utf8 this #14 = Utf8 Lcom/zhuhjay/demo3/Demo3_1; #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 Demo3_1.java #25 = NameAndType #8:#9 // "":()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/zhuhjay/demo3/Demo3_1 #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.zhuhjay.demo3.Demo3_1(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return LineNumberTable: line 7: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/zhuhjay/demo3/Demo3_1; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=4, args_size=1 0: bipush 10 2: istore_1 3: ldc #3 // int 32768 5: istore_2 6: iload_1 7: iload_2 8: iadd 9: istore_3 10: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 13: iload_3 14: invokevirtual #5 // Method java/io/PrintStream.println:(I)V 17: return LineNumberTable: line 9: 0 line 10: 3 line 11: 6 line 12: 10 line 13: 17 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: "Demo3_1.java" ``` #### 3) 常量池载入运行时常量池 JVM 通过类加载器将以上字节码数据读入,将常量池数据存储到运行时常量池(属于方法区的一部分)中 - #3 这个数据的来源是 `Short.MAX_VALUE + 1`,java源码中比较小的数值并不会存储在常量池中,而是与方法的字节码指令存储在一起,一旦超出了 `Short.MAX_VALUE(32767)` 就会存储到常量池中。 1670221697470 #### 4) 方法字节码载入方法区 1670222538332 #### 5) main 线程开始运行,分配栈帧内存 会根据以下信息来进行栈帧分配,操作数栈深度为2,局部变量表长度为4 ``` stack=2, locals=4, args_size=1 ``` 1670222781062 #### 6) 执行引擎开始执行字节码 **bipush 10** - 将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有 - sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节) - ldc 将一个 int 压入操作数栈 - ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节) - 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池 1670223128743 **istore_1** - 将操作数栈顶数据弹出,存入局部变量表的 slot 1 1670223335018 1670223376535 以上操作代表源代码的 `int a = 10;` 这个赋值操作 **ldc #3** - 从常量池加载 #3 数据到操作数栈 - **注意** `Short.MAX_VALUE` 是 32767,所以 `32768 = Short.MAX_VALUE + 1` 实际是在编译期间计算好的(常量折叠优化) 1670223577473 **istore_2** - 将操作数栈顶数据弹出,存入局部变量表的 slot 2 1670223846251 以上操作代表源代码的 `int b = Short.MAX_VALUE + 1;` 这个赋值操作 **iload_1** - 将局部变量表中的 slot 1 数据读取复制到操作数栈中 1670223953960 **iload_2** - 将局部变量表中的 slot 2 数据读取复制到操作数栈中 1670224060014 **iadd** - 将操作数栈中的数据进行相加的操作 1670224223804 1670224265031 **istore_3** - 将操作数栈顶数据弹出,存入局部变量表的 slot 3 1670224443009 以上操作代表源代码的 `int c = a + b;` 这个操作 **getstatic #4** - 从常量池中获取成员变量 #4 找到堆中的对象,将该对象的引用放入到操作数栈中 1670224527319 **iload_3** - 将局部变量表中的 slot 3 数据读取复制到操作数栈中 1670224818125 **invokevirtual #5** - 找到常量池 #5 项 - 定位到方法区 `java/io/PrintStream.println:(I)V` 方法 - 生成新的栈帧(分配 locals、stack等,每执行一个方法都会有新的栈帧) - 传递参数,执行新栈帧中的字节码 1670225026514 - 执行完毕,弹出栈帧 - 清除 main 操作数栈内容 1670225112947 **return** - 完成 main 方法调用,弹出 main 栈帧 - 程序结束 ### 2.4 练习 - 分析i++ 从字节码角度分析 i++ 相关题目 源码 ```java package com.zhuhjay.demo3; public class Demo3_2 { public static void main(String[] args) { int a = 10; int b = a++ + ++a + a--; System.out.println(a); System.out.println(b); } } ``` 字节码 ``` 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 9: 0 line 10: 3 line 11: 18 line 12: 25 line 13: 32 LocalVariableTable: Start Length Slot Name Signature 0 33 0 args [Ljava/lang/String; 3 30 1 a I 18 15 2 b I ``` 分析: - **注意** iinc 指令是直接在局部变量 slot 上进行运算 - a++ 和 ++a 的区别是 先执行 iload 还是 先执行 iinc - 将一个 byte 数值类型 10 压入到操作数栈中 1670225735794 - 将操作数栈顶数据弹出,存入局部变量表的 slot 1,此时完成了代码 `int a = 10` 的操作 1670225834483 - 将局部变量表中的 slot 1 的数据读取复制到操作数栈中,`a++` 执行顺序是先读取复制,再进行自增的操作 1670225887463 - 对局部变量表中的 slot 1 进行长度为 1 的自增,是直接在局部变量表中直接进行操作,而不是在操作数栈中进行 - 此时局部变量表中的 a 和 操作数栈中的数值不是一个相等的值 1670225973371 - 现将进行 `++a` 的运算,会先对局部变量表中的 slot 1 中的数值进行自增,而后才将该结果读取复制到操作数栈中 1670226003812 - 读取局部变量表中 slot 1 的数值到操作数栈中 1670226078219 - 计算完 `a++` 和 `++a` 过后,操作数栈中已经存在两个数据了,所以会先对操作数栈中的数据进行相加的操作 1670226107200 - 现将进行 `a--` 的运算,先将局部变量表中 slot 1 读取复制到操作数栈中 1670226176345 - 将局部变量表中的 slot 1 进行自减操作 1670226329153 - 将操作数栈中的数据进行相加,到此为止 `a++ + ++a + a--` 的运算结束 1670226379924 - 最后将操作数栈中的数据存放到局部变量表中对应的位置上去 1670226433126 ### 2.5 条件判断指令 | 指令 | 助记符 | 含义 | | ---- | --------- | ---------------- | | 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 | - 几点说明: - byte,short,char 都会按 int 比较,因为操作数栈都是 4 字节 - goto 用来进行跳转到指定行号的字节码 - 源码 ```java package com.zhuhjay.demo3; public class Demo3_3 { public static void main(String[] args) { int a = 0; if (a == 0) { a = 10; } else { a = 20; } } } ``` - 字节码 - 比较小的数是通过 `iconst` 指令 - `ifne` 指令判断操作数栈中的两个数值是否不相等,如果不相等,则跳转到序号为 `12` 的指令,否则继续往下执行 - 该程序 if 判断有两个执行路径 - `3 -> 6 -> 8 -> 9 -> 15`:执行到 `goto` 指令后会跳转到对应的序号指令继续执行 - `3 -> 12 -> 14 0> 15`:通过了 if 判断跳转到对应的序号指令继续执行 ``` 0: iconst_0 1: istore_1 2: iload_1 3: ifne 12 6: bipush 10 8: istore_1 9: goto 15 12: bipush 20 14: istore_1 15: return ``` > Tip: > > ​ 以上比较指令中没有 long,float,double 的比较,那么它们要比较怎么办? > > 参考 [https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.lcmp](https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.lcmp) ### 2.6 循环控制指令 其实循环控制还是前面介绍的那些指令,例如 while 循环: ```java package com.zhuhjay.demo3; public class Demo3_4 { public static void main(String[] args) { int a = 0; while (a < 10) { a++; } } } ``` 字节码,通过指令 `if_icmpge` 判断是否大于等于,是则跳出当前 while 循环,否则继续自增 ``` 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 ``` 再比如 do while 循环: ```java package com.zhuhjay.demo3; public class Demo3_5 { public static void main(String[] args) { int a = 0; do { a++; } while (a < 10); } } ``` 字节码,与 while 循环不同的地方就是,do while 会先进行自增操作,之后才进行判断 ``` 0: iconst_0 1: istore_1 2: iinc 1, 1 5: iload_1 6: bipush 10 8: if_icmplt 2 11: return ``` 最后再看看 for 循环: ```java package com.zhuhjay.demo3; public class Demo3_6 { public static void main(String[] args) { for (int i = 0; i < 10; i++) { } } } ``` 字节码,会发现 for 循环编译后的字节码和 while 循环是**一模一样**的 ``` 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 ``` ### 2.7 练习 - 判断结果 请从字节码角度分析,下列代码运行的结果: - 问题出现在 `x = x++`,这部分代码的字节码运行流程如下 - 从局部变量表中先获取 x 的数值并复制到操作数栈中,此时:`slot x:0; stack x:0` - 对局部变量表中的 x 所在的槽位数值自增1,此时:`slot x:1; stack x:0` - 将操作数栈中的值赋值给局部变量表中 x ,此时:`slot x:0; stack null` - 一直重复以上操作,x 的结果永远都是 0 - 若将以上问题代码改为 `x = ++x`,将不会出现以上的问题 ```java package com.zhuhjay.demo3; public class Demo3_7 { public static void main(String[] args) { int i = 0; int x = 0; while (i < 10) { x = x++; i++; } System.out.println(x); // 0 } } ``` ### 2.8 构造方法 #### 1) `()V` ```java public class Demo3_8 { static int i = 10; static { i = 20; } static { i = 30; } } ``` 编译器会按从上至下的顺序,收集所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方法`()V ` ,该方法会在类加载的初始化阶段被调用 ``` static {}; descriptor: ()V flags: ACC_STATIC Code: stack=1, locals=0, args_size=0 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 ``` #### 2) `()V` ```java public class Demo3_9 { private String a = "s1"; { b = 20; } private int b = 10; { a = "s2"; } public Demo3_9(String a, int b) { this.a = a; this.b = b; } public static void main(String[] args) { Demo3_9 d = new Demo3_9("s3", 30); System.out.println(d.a); // s3 System.out.println(d.b); // 30 } } ``` 编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在最后 ``` public com.zhuhjay.demo3.Demo3_9(java.lang.String, int); descriptor: (Ljava/lang/String;I)V flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=3 0: aload_0 1: invokespecial #1 // 调用父类的构造方法(Object) 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 // <- "s3" 30: putfield #3 // -> this.a 33: aload_0 34: iload_2 // <- 30 35: putfield #4 // -> this.b 38: return ``` ### 2.9 方法调用 看一下几种不同的方法调用对应的字节码指令 ```java public class Demo3_10 { public Demo3_10() { } private void test1() { } private final void test2() { } public void test3() { } public static void test4() { } public static void main(String[] args) { Demo3_10 d = new Demo3_10(); d.test1(); d.test2(); d.test3(); d.test4(); Demo3_10.test4(); } } ``` 字节码 ``` 0: new #2 // class com/zhuhjay/demo3/Demo3_10 3: dup 4: invokespecial #3 // Method "":()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` 是创建【对象】,给对象分配堆内存,执行成功会将【对象引用】压入操作数栈 - `dup` 是赋值操作数栈栈顶的内容,本例即为【对象引用】,为什么需要两份引用呢,一个是要配合 `invokespecial` 调用该对象的构造方法 `"":()V` (会消耗掉栈顶一个引用),另一个要配合 `astore_1` 赋值给局部变量 - 最终方法(final),私有方法(private),构造方法都是由 `invokespecial` 指令来调用,属于 静态绑定 - 普通成员方法是由 `invokevirtual` 调用,属于动态绑定,即支持多态成员方法与静态方法调用的另一个区别是,执行方法前是否需要【对象引用】 - 比较有意思的是 `d.test4();` 是通过【对象引用】调用一个静态方法,可以看到在调用 `invokestatic` 之前执行了 `pop` 指令,把【对象引用】从操作数栈弹掉了 - 还有一个执行 `invokespecial` 的情况是通过 `super` 调用父类方法 ### 2.10 多态的原理 ```java package com.zhuhjay.demo3; public class Demo3_11 { public static void test(Animal animal) { animal.eat(); System.out.println(animal.toString()); } public static void main(String[] args) throws IOException { test(new Dog()); test(new Cat()); System.in.read(); } } abstract class Animal { public abstract void eat(); @Override public String toString() { return "我是" + this.getClass().getSimpleName(); } } class Dog extends Animal { @Override public void eat() { System.out.println("啃骨头"); } } class Cat extends Animal { @Override public void eat() { System.out.println("吃鱼"); } } ``` #### 1) 运行代码 **注意**:运行代码前需要添加虚拟机参数 `-XX:-UseCompressedOops -XX:-UseCompressedClassPointers`,禁止指针压缩,省去了地址换算 停在 `System.in.read()` 方法上,这时运行 `jps` 获取进程 id #### 2) 运行 HSDB 工具 进入 JDK 目录,执行命令 `java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB` 进入图形界面后 Attach 到对应的 进程id,效果如下 1670338229940 #### 3) 查找内存中的对象 打开 `Tools -> Find Object By Query` 工具 输入 `select d from com.zhuhjay.demo3.Dog d` 并执行,语法同 SQL 一般,查询结果就是在内存中存在的 Dog 对象内存地址。 ![1670338644448](/jvm/1670338644448.png) #### 4) 查看对象内存结构 点击超链接可以看到对象的内存结构,此对象没有任何属性,因此只有对象头的 16 字节,前 8 字节是 MarkWord,后 8 字节就是对象的 Class 指针 但目前看不到它的实际地址,在底层是 C++ 的数据结构,可以看到类中的一些数据,子类父类等等,虚引用表长度 `_vtable_len` 1670338857348 #### 5) 查看对象的内存地址 通过 `Windows -> Console` 进入命令行模式,执行 `mem 0x0000023338896cd0 2` - `mem` 有两个参数,参数 1 是对象地址,参数 2 是查看 2 行(即 16 字节) 在结果中的第二行即为 Class 的内存地址 1670339471963 #### 6) 查看类的 vtable - 方法一:`Alt+R` 进入 `Inspector` 工具,输入刚刚查询到的 Class 内存地址 1670339740477 - 方法二:打开 `Tools -> Class Browser` 输入 Dog 查找,也一样可以找到该 Class 对应的内存地址,然后使用方法一进行查询 1670339913417 无论通过哪种方法,都可以找到 `Dog Class` 的 `vtable` 长度为 6,意思就是 `Dog` 类有 6 个虚方法(多态相关的,final,static 不会列入) 那么这 6 个方法都是谁呢?从 Class 的起始地址开始算,偏移 `0x1b8` 就是 `vtable` 的起始地址,进行计算得到: ``` 0x00000233675d3c90 0x1b8 + --------------------- 0x00000233675d3e48 ``` 然后使用 `Windows -> Console` 工具执行命令,就能够得到这 6 个虚方法的入口地址了 ``` mem 0x00000233675d3e48 6 0x00000233675d3e48: 0x00000233671d1b10 0x00000233675d3e50: 0x00000233671d15e8 0x00000233675d3e58: 0x00000233675d3758 0x00000233675d3e60: 0x00000233671d1540 0x00000233675d3e68: 0x00000233671d1678 0x00000233675d3e70: 0x00000233675d3c38 ``` #### 7) 验证方法地址 通过 `Tools -> Class Browser` 查看每个类的方法定义,比较可知每一个地址都对上了查询到的所有虚方法地址 ``` Dog - public void eat() @0x00000233675d3c38; Animal - public java.lang.String toString() @0x00000233675d3758; Object - protected void finalize() @0x00000233671d1b10; Object - public boolean equals(java.lang.Object) @0x00000233671d15e8; Object - public native int hashCode() @0x00000233671d1540; Object - protected native java.lang.Object clone() @0x00000233671d1678; ``` #### 8) 小结 当执行 `invokevirtual` 指令时 1. 先通过栈帧中的对象引用找到对象 2. 分析对象头,找到对象的实际 Class 3. Class 结构中有 `vtable`,它在类加载的链接阶段就已经根据方法的重写规则生成好了 4. 查表得到方法的具体地址 5. 执行方法的字节码 ### 2.11 异常处理 #### 1) try-catch ```java public class Demo3_12_1 { public static void main(String[] args) { int i = 0; try { i = 10; } catch (Exception e) { i = 20; } } } ``` 部分字节码如下 - 可以看到多出来一个 `Exception table` 的结构,`[from, to)` 是前闭后开的检测范围,一旦这个范围内的字节码执行出现异常,则通过 `type` 匹配异常类型,如果一致,进入 `target` 所指示行号 - 8 行的字节码指令 `astore_2` 是将异常对象引用存入局部变量表的 `slot 2` 位置,记录异常对象信息 ``` public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=3, args_size=1 0: iconst_0 1: istore_1 2: bipush 10 4: istore_1 5: goto 12 8: astore_2 9: bipush 20 11: istore_1 12: return Exception table: from to target type 2 5 8 Class java/lang/Exception LineNumberTable: ... LocalVariableTable: Start Length Slot Name Signature 9 3 2 e Ljava/lang/Exception; 0 13 0 args [Ljava/lang/String; 2 11 1 i I ``` #### 2) 多个 single-catch 块的情况 ```java public class Demo3_12_2 { 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; } } } ``` 部分字节码信息如下 - 因为异常出现时,只能进入 `Exception table` 中一个分支,所以局部变量表 `Slot 2` 位置被共用 ``` 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: ... 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 ``` #### 3) multi-catch 的情况 ```java public class Demo3_12_3 { public static void main(String[] args) { int i = 0; try { i = 10; } catch (ArithmeticException | NullPointerException | NumberFormatException e) { i = 20; } } } ``` 部分字节码如下 - 将异常入口进行一次收集,局部变量表 `Slot 2` 只收集一个,并且是这些异常的父类型 ``` 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/ArithmeticException 2 5 8 Class java/lang/NullPointerException 2 5 8 Class java/lang/NumberFormatException LineNumberTable: ... LocalVariableTable: Start Length Slot Name Signature 9 3 2 e Ljava/lang/RuntimeException; 0 13 0 args [Ljava/lang/String; 2 11 1 i I ``` #### 4) finally ```java public class Demo3_12_4 { public static void main(String[] args) { int i = 0; try { i = 10; } catch (Exception e) { i = 20; } finally { i = 30; } } } ``` 部分字节码如下 - 可以看到 finally 中的代码被**复制**了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程 - JVM 会自动捕捉其没有被 catch 块捕获的异常,且如果真的捕获到了这部分异常,同样也会执行 finally 中的代码,并且使用 `athrow` 指令将异常向外抛出 ``` 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 // finally | 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 // finally | 17: istore_1 // 30 -> i | 18: goto 27 // return ------------------- 21: astore_3 // catch any -> slot 3 ------ 22: bipush 30 // finally | 24: istore_1 // 30 -> i | 25: aload_3 // <- slot 3 | 26: athrow // throw -------------------- 27: return Exception table: from to target type 2 5 11 Class java/lang/Exception 2 5 21 any 11 15 21 any LineNumberTable: ... LocalVariableTable: Start Length Slot Name Signature 12 3 2 e Ljava/lang/Exception; 0 28 0 args [Ljava/lang/String; 2 26 1 i I ``` ### 2.12 练习 - finally 面试题 #### 1) finally 出现了 return ```java public class Demo3_13_1 { public static void main(String[] args) { System.out.println(test()); } public static int test() { try { return 10; } finally { return 20; } } } ``` 部分字节码如下 - 由于 finally 中的 `ireturn` 被插入了所有可能的流程,因此返回结果肯定以 finally 的为准 - 至于字节码中第 2 行,似乎没啥用,且留个伏笔,看下个例子 - 跟上例中的 finally 相比,发现没有 `athrow` 了,这告诉我们:**如果在 finally 中出现了 return,会吞掉异常** ``` public static int test(); descriptor: ()I flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=2, args_size=0 0: bipush 10 // <- 10 放入栈顶 2: istore_0 // 10 -> slot 0 (从栈顶移除) 3: bipush 20 // <- 20 放入栈顶 (finally代码) 5: ireturn // 返回栈顶 int(20) 6: astore_1 // catch any -> slot 1 7: bipush 20 // <- 20 放入栈顶 (finally代码) 9: ireturn // 返回栈顶 int(20) Exception table: from to target type 0 3 6 any ``` 若将案例代码改为如下 - 制造一个异常 `int i = 1/0`,运行后发现不会有异常出现,且结果与上个例子相同 ```java public class Demo3_13_1 { public static void main(String[] args) { System.out.println(test()); } public static int test() { try { int i = 1/0; return 10; } finally { return 20; } } } ``` #### 2) finally 对返回值影响 ```java public class Demo3_13_2 { public static void main(String[] args) { System.out.println(test()); } public static int test() { int i = 0; try { return i; } finally { i = 100; } } } ``` 部分字节码如下 - finally 中不带 return,就可以正常的捕获异常了(从字节码中 `athrow` 看出) ``` public static int test(); descriptor: ()I flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=3, args_size=0 0: iconst_0 // <- 0 放入栈顶 1: istore_0 // 0 -> i 2: iload_0 // <- i(0) 3: istore_1 // 0 -> slot 1,暂存至 slot 1,目的是为了固定返回值 4: bipush 100 // <- 100 放入栈顶 6: istore_0 // 100 -> i 7: iload_1 // <- i(0),读取返回值放置栈顶 8: ireturn // 返回栈顶 i(0) 9: astore_2 // catch 部分 10: bipush 100 12: istore_0 13: aload_2 14: athrow Exception table: from to target type 2 4 9 any LineNumberTable: ... LocalVariableTable: Start Length Slot Name Signature 2 13 0 i I ``` ### 2.13 synchronized ```java public class Demo3_14 { public static void main(String[] args) { Object lock = new Object(); synchronized (lock) { System.out.println("ok"); } } } ``` 部分字节码如下 - JVM 会自动给 `synchronized` 代码块中的代码进行异常捕获,当捕获到异常或者 `synchronized` 代码块运行结束时,都会进行锁的释放,并且锁的释放过程也会进行异常的捕获,确保锁的释放 ``` 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 // new Object 3: dup // 复制一份,用来初始化 4: invokespecial #1 // invokespecial -> :()V 7: astore_1 // 将 Object 引用 -> lock 8: aload_1 // <- lock (synchronized开始) 9: dup // 复制一份,一个用于加锁指令一个用于解锁指令 10: astore_2 // 将 Object 引用 -> slot 2 11: monitorenter // 加锁指令 12: getstatic #3 // <- System.out 15: ldc #4 // <- "ok" 17: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 20: aload_2 // <- slot 2(解锁指令使用的 lock引用) 21: monitorexit // 解锁指令 22: goto 30 25: astore_3 // catch any -> slot 3 26: aload_2 // <- slot 2(解锁指令使用的 lock引用) 27: monitorexit // 解锁指令 28: aload_3 29: athrow // 异常抛出 30: return Exception table: from to target type 12 22 25 any 25 28 25 any LineNumberTable: ... LocalVariableTable: Start Length Slot Name Signature 0 31 0 args [Ljava/lang/String; 8 23 1 lock Ljava/lang/Object; ``` > **注意**:方法级别的 `synchronized` 不会在字节码指令中有所体现 ## 3 编译器处理 所谓的 `语法糖` ,其实就是指 java 编译器把 `*.java` 源码编译为 `*.class` 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利 注意,以下代码的分析,借助了 `javap` 工具,idea 的反编译功能,idea 插件 `jclasslib` 等工具。另外, 编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了几乎等价 的 java 源码方式,并不是编译器还会转换出中间的 java 源码,切记。 ### 3.1 默认构造器 ```java public class Candy1 { } ``` 编译成class后的代码: ```java public class Candy1 { // 这个无参构造是编译器帮助我们加上的 public Candy1() { super(); // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object."":()V } } ``` ### 3.2 自动拆装箱 这个特性是 `JDK 5` 开始加入的:`代码一` ```java public class Candy2 { public static void main(String[] args) { Integer x = 1; int y = x; } } ``` 这段代码在 `JDK 5` 之前是无法编译通过的,必须改写为:`代码二` ```java public class Candy2 { public static void main(String[] args) { Integer x = Integer.valueOf(1); int y = x.intValue(); } } ``` 显然之前版本的代码太麻烦了,需要在基本类型和包装类型直接来回转换(尤其是集合类中操作的都是包装类型),因此这些转换的事情在 `JDK 5` 以后都由编译器在编译阶段完成,即 `代码一` 都会在编译阶段被转为 `代码二` ### 3.3 泛型集合取值 泛型也是在 `JDK 5` 开始加入的特性,但 java 在编译泛型代码后会执行 `泛型擦除` 的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 `Object` 类型来处理: ```java public class Candy3 { public static void main(String[] args) { List list = new ArrayList<>(); list.add(10); // 实际调用的是 List.add(Object e) Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index); } } ``` 所以在取值时,编译器真正生成的字节码中,还要额外做一个类型转换的操作: ```java // 需要将 Object 转为 Integer Integer x = (Integer)list.get(0); ``` 如果前面的 x 变量类型修改为 int 基本类型那么最终生成的字节码是: ```java // 需要将 Object 转为 Integer, 并执行拆箱操作 int x = ((Integer)list.get(0)).intValue(); ``` 擦除的是字节码上的泛型信息,可以看到 `LocalVariableTypeTable` 仍然保留了方法参数泛型的信息 - JVM 将代码 `Integer x = list.get(0);` 通过 `Object obj = List.get(int index);` 方式获取出来后,会使用 `checkcast` 指令对照泛型信息表来进行类型的转换 ``` 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."":()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: ... 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; ``` 使用反射,仍然能够获得这些信息(只能获取到方法参数上和返回值的泛型信息): ```java public Set test(List list, Map map) {return null} ``` ```java Method test = Candy3.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("\t泛型参数[%d] - %s\n", i, arguments[i]); } } } ``` 输出 ``` 原始类型 - interface java.util.List 泛型参数[0] - class java.lang.String 原始类型 - interface java.util.Map 泛型参数[0] - class java.lang.Integer 泛型参数[1] - class java.lang.Object ``` ### 3.4 可变参数 可变参数也是 `JDK 5` 开始加入的新特性: 例如: ```java public class Candy4 { public static void foo(String... args) { String[] array = args; // 直接赋值 System.out.println(array); } public static void main(String[] args) { foo("hello", "world"); } } ``` 可变参数 `String... args` 其实是一个 `String[] args` ,从代码中的赋值语句中就可以看出来。 同样 java 编译器会在编译期间将上述代码变换为: ```java public class Candy4 { public static void foo(String[] args) { String[] array = args; // 直接赋值 System.out.println(array); } public static void main(String[] args) { foo(new String[]{"hello", "world"}); } } ``` > **注意** 如果调用了 `foo()` 则等价代码为 `foo(new String[]{})` ,创建了一个空的数组,而不会传递 `null` 进去 ### 3.5 foreach 循环 仍是 `JDK 5` 开始引入的语法糖,数组的循环: ```java public class Candy5_1 { public static void main(String[] args) { int[] array = {1, 2, 3, 4, 5}; for (int e : array) { System.out.println(e); } } } ``` 字节码信息如下 - 数组赋初值的简化写法也是语法糖 - 数组的 `foreach` 会被编译成 `fori` ```java public class Candy5_1 { public Candy5_1() { } 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); } } } ``` 而集合的循环: ```java public class Candy5_2 { public static void main(String[] args) { List list = Arrays.asList(1, 2, 3, 4, 5); for (Integer i : list) { System.out.println(i); } } } ``` 实际被编译器转换为对迭代器的调用: - 使用迭代器进行遍历(只有实现了 `Iterable` 接口的集合才能使用 `foreach` 语法糖 ) ```java public class Candy5_2 { public Candy5_2() { } public static void main(String[] args) { List list = Arrays.asList(1, 2, 3, 4, 5); Iterator iter = list.iterator(); while(iter.hasNext()) { Integer e = (Integer)iter.next(); System.out.println(e); } } } ``` > **注意** foreach 循环写法,能够配合数组,以及所有实现了 `Iterable` 接口的集合类一起使用,其 中 `Iterable` 用来获取集合的迭代器( `Iterator` ) ### 3.6 switch 字符串 从 `JDK 7` 开始,switch 可以作用于字符串和枚举类,这个功能其实也是语法糖,例如: ```java public class Candy6_1 { public static void choose(String str) { switch (str) { case "hello": System.out.println("h"); break; case "world": System.out.println("w"); break; } } } ``` > **注意** switch 配合 String 和枚举使用时,变量不能为 `null`,原因分析完语法糖转换后的代码应当自然清楚 会被编译器转换为: ```java public class Candy6_1 { public Candy6_1() { } 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` 和 `equals` 将字符串的转换为相应 byte 类型,第二遍才是利用 byte 执行进行比较。 为什么第一遍时必须既比较 `hashCode`,又利用 `equals` 比较呢?`hashCode` 是为了提高效率,减少可能的比较;而 `equals` 是为了防止 `hashCode` 冲突,例如 `BM` 和 `C.` 这两个字符串的 `hashCode` 值都是 2123 ,如果有如下代码: ```java public class Candy6_1 { public static void choose(String str) { switch (str) { case "BM": System.out.println("h"); break; case "C.": System.out.println("w"); break; } } } ``` 编译结果为 ```java public class Candy6_1 { public Candy6_1() { } public static void choose(String str) { byte var2 = -1; switch(str.hashCode()) { case 2123: if (str.equals("C.")) { var2 = 1; } else if (str.equals("BM")) { var2 = 0; } default: switch(var2) { case 0: System.out.println("h"); break; case 1: System.out.println("w"); } } } } ``` ### 3.7 switch 枚举 switch 枚举的例子,原始代码 ```java public enum Sex { MALE, FEMALE; } ``` ```java public class Candy7 { public static void foo(Sex sex) { switch (sex) { case MALE: System.out.println("男");break; case FEMALE: System.out.println("女");break; } } } ``` 编译后会生成以下文件 - 其中 `Candy7$1.class` 文件是用来映射枚举类 Sex 的一个静态内部类 ![1670415162524](/jvm/1670415162524.png) 字节码如下 ``` static final int[] $SwitchMap$com$zhuhjay$jit$Sex; descriptor: [I flags: ACC_STATIC, ACC_FINAL, ACC_SYNTHETIC static {}; descriptor: ()V flags: ACC_STATIC Code: stack=3, locals=1, args_size=0 0: invokestatic #1 // Method com/zhuhjay/jit/Sex.values:()[Lcom/zhuhjay/jit/Sex; 3: arraylength 4: newarray int 6: putstatic #2 // Field $SwitchMap$com$zhuhjay$jit$Sex:[I 9: getstatic #2 // Field $SwitchMap$com$zhuhjay$jit$Sex:[I 12: getstatic #3 // Field com/zhuhjay/jit/Sex.MALE:Lcom/zhuhjay/jit/Sex; 15: invokevirtual #4 // Method com/zhuhjay/jit/Sex.ordinal:()I 18: iconst_1 19: iastore 20: goto 24 23: astore_0 24: getstatic #2 // Field $SwitchMap$com$zhuhjay$jit$Sex:[I 27: getstatic #6 // Field com/zhuhjay/jit/Sex.FEMALE:Lcom/zhuhjay/jit/Sex; 30: invokevirtual #4 // Method com/zhuhjay/jit/Sex.ordinal:()I 33: iconst_2 34: iastore 35: goto 39 38: astore_0 39: return ``` 也就是会生成以下代码 ```java public class Candy7 { /** * 定义一个合成类(仅 jvm 使用,对我们不可见) * 用来映射枚举的 ordinal 与数组元素的关系 * 枚举的 ordinal 表示枚举对象的序号,从 0 开始 * 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1 */ static class Candy7$1 { // 数组大小即为枚举元素个数,里面存储case用来对比的数字 static final int[] $SwitchMap$com$zhuhjay$jit$Sex = new int[2]; static { $SwitchMap$com$zhuhjay$jit$Sex[Sex.MALE.ordinal()] = 1; $SwitchMap$com$zhuhjay$jit$Sex[Sex.FEMALE.ordinal()] = 2; } } public static void foo(Sex sex) { int x = Candy7$1.$SwitchMap$com$zhuhjay$jit$Sex[sex.ordinal()]; switch (x) { case 1: System.out.println("男"); break; case 2: System.out.println("女"); break; } } } ``` ### 3.8 枚举类 `JDK 7` 新增了枚举类,以前面的性别枚举为例 ```java public enum Sex { MALE, FEMALE; } ``` 转换后的代码如下 - 继承 `Enum` 枚举类 - `final` 修饰类 - 实例个数都在本类中创建,并且不可修改,属于常量 - 私有构造方法,保护枚举类 - `Enum.valueOf(Sex.class, name)` 底层原理就是 `Map`,通过键查找对应的枚举 ```java public final class Sex extends Enum { public static final Sex MALE; public static final Sex FEMALE; private static final Sex[] $VALUES; static { MALE = new Sex("MALE", 0); FEMALE = new Sex("FEMALE", 1); $VALUES = new Sex[]{MALE, FEMALE}; } private Sex(String name, int ordinal) { super(name, ordinal); } public static Sex[] values() { return $VALUES.clone(); } public static Sex valueOf(String name) { return Enum.valueOf(Sex.class, name); } } ``` ### 3.9 try-with-resources `JDK 7` 开始新增了对需要关闭的资源处理的特殊语法 `try-with-resources`: 其中资源对象需要实现 `AutoCloseable` 接口,例如 `InputStream` 、 `OutputStream` 、 `Connection` 、 `Statement` 、 `ResultSet` 等接口都实现了 `AutoCloseable` ,使用 `try-with-resources` 可以不用写 `finally` 语句块,编译器会帮助生成关闭资源代码,例如: ```java public class Candy9 { public static void main(String[] args) { try (InputStream is = new FileInputStream("E:\\10067\\Documents\\dist\\index.html")) { System.out.println(is); } catch (Exception e) { e.printStackTrace(); } } } ``` 编译后代码如下 - 更完整的关流操作,甚至可以保留压制异常 `var2.addSuppressed(var11);` ```java public class Candy9 { public Candy9() { } public static void main(String[] args) { try { InputStream is = new FileInputStream("E:\\10067\\Documents\\dist\\index.html"); Throwable var2 = null; try { System.out.println(is); } catch (Throwable var12) { // var2 是我们代码出现的异常 var2 = var12; throw var12; } finally { if (is != null) { // 如果我们代码有异常 if (var2 != null) { try { is.close(); } catch (Throwable var11) { // 当关流的时候也出现了异常,那么作为压制异常添加 var2.addSuppressed(var11); } } else { // 我们代码没有异常,关流出现了异常,那么 var14 关流的异常 is.close(); } } } } catch (Exception var14) { var14.printStackTrace(); } } } ``` 为什么要设计一个 `addSuppressed(Throwable e)` (添加被压制异常)的方法呢?是为了防止异常信息的丢失(想想 `try-with-resources` 生成的 `fianlly` 中如果抛出了异常): ```java public class Test1 { public static void main(String[] args) { try (MyResource resource = new MyResource()) { int i = 1/0; } catch (Exception e) { e.printStackTrace(); } } } class MyResource implements AutoCloseable { @Override public void close() throws Exception { throw new Exception("关流异常"); } } ``` 执行以上代码有以下结果,可以发现异常信息被完整的保留了下来 ``` java.lang.ArithmeticException: / by zero at com.zhuhjay.jit.Test1.main(Test1.java:10) Suppressed: java.lang.Exception: 关流异常 at com.zhuhjay.jit.MyResource.close(Test1.java:20) at com.zhuhjay.jit.Test1.main(Test1.java:11) ``` ### 3.10 方法重写时的桥接方法 我们都知道,方法重写时对返回值分两种情况: - 父子类的返回值完全一致 - 子类返回值可以是父类返回值的子类(比较绕口,见下面的例子) ```java class A { public Number m() { return 1; } } class B extends A { @Override public Integer m() { return 2; } } ``` 对于子类,java 编译器会做如下处理: ```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 for (Method declaredMethod : B.class.getDeclaredMethods()) { System.out.println(declaredMethod); } ``` 发现有两个 `m()` 方法,证明了字节码中桥接方法的存在 ``` public java.lang.Integer com.zhuhjay.jit.B.m() public java.lang.Number com.zhuhjay.jit.B.m() ``` ### 3.11 匿名内部类 ```java public class Candy11 { public static void main(String[] args) { Runnable runnable = new Runnable() { @Override public void run() { System.out.println("ok"); } }; } } ``` 源代码编译后会生成一个额外的类 ```java // 额外生成的类 final class Candy11$1 implements Runnable { Candy11$1() { } public void run() { System.out.println("ok"); } } ``` ```java public class Candy11 { public static void main(String[] args) { Runnable runnable = new Candy11$1(); } } ``` 引用局部变量的匿名内部类,源代码: ```java public class Candy11 { public static void main(final String[] args) { Runnable runnable = new Runnable() { @Override public void run() { System.out.println("ok" + args.length); } }; } } ``` 会在额外生成的类中添加一个有参构造方法,通过参数的方式传递给该类的成员变量的存储,后续让方法获取成员变量的信息来达到目的 ```java // 额外生成的类 final class Candy11$1 implements Runnable { final String[] val$args; Candy11$1(String[] args) { this.val$args = args; } public void run() { System.out.println("ok" + val$args.length); } } ``` ```java public class Candy11 { public static void main(final String[] args) { Runnable runnable = new Candy11$1(args); } } ``` > **注意** 这同时解释了为什么匿名内部类引用局部变量时,局部变量必须是 `final` 的:因为在创建 `Candy11$1` 对象时,将 `args` 的值赋值给了 `Candy11$1` 对象的 `val$args` 属性,所以 `args` 不应该再发生变化,如果变化了,那么 `val$args` 属性没有机会跟着一起变化。(当不对变量进行修改时,编译器会将局部变量用 `final` 修饰)