Files
www/blogs/java/jvm/JVM-字节码技术.md
zhuhjay 22e48d9558 build(www): 添加 Drone CI 流水线配置
- 新增 .drone.yml 文件用于定义 CI/CD 流程
- 配置了基于 Docker 的部署步骤
- 设置了工作区和卷映射以支持持久化数据
- 添加了构建准备阶段和 Docker 部署阶段
- 定义了环境变量和代理设置
- 配置了 artifacts 目录的处理逻辑
- 添加了 timezone 映射以确保时间同步
- 设置了 docker.sock 映射以支持 Docker in Docker
2025-11-01 13:36:00 +08:00

2394 lines
71 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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; //类的访问控制符标识publicstaticfinalabstract等
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; //属性数包括InnerClassesEnclosingMethodSourceFile等
attribute_info attributes[attributes_count]; //属性表集合
}
```
### 1.1 魔数
0-3 字节表示它是否是【class】类型的文件
![1670046677428](/jvm/1670046677428.png)
### 1.2 版本
4-7 字节,表示类的版本 00 3452 表示是 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 | LengthByte | 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 项 0a10由映射表得知是一个 Method 信息,其通过后续的 00 066 和 00 1117 分别表示该方法的【所属类】引用常量池中 #6 项、【方法名】引用常量池中 #17 项。
![1670048081295](/jvm/1670048081295.png)
常量池中 #2 项 099由映射表得知是一个 Field 信息,其通过后续的 00 1218 和 00 1319 分别表示该成员变量的【所属类】引用常量池中 #18 项、【成员变量名】引用常量池中 #19 项。
![1670048493839](/jvm/1670048493839.png)
常量池中 #3 项 088由映射表得知是一个字符串常量名称其通过后续的 00 1420表示其引用了常量池中 #20 项。
![1670048714141](/jvm/1670048714141.png)
依此类推进行其他常量池的解析,这里就不演示了,了解一个解析形式即可。
以上分析结果的常量池大概就是使用反编译后的常量池结果,而且可以看到所有的字符串常量池中的信息一一对应
```
#1 = Methodref #6.#17 // java/lang/Object."<init>":()V
#2 = Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #20 // Hello World
#4 = Methodref #21.#22 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #23 // HelloWorld
#6 = Class #24 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 MethodParameters
#14 = Utf8 args
#15 = Utf8 SourceFile
#16 = Utf8 HelloWorld.java
#17 = NameAndType #7:#8 // "<init>":()V
#18 = Class #25 // java/lang/System
#19 = NameAndType #26:#27 // out:Ljava/io/PrintStream;
#20 = Utf8 Hello World
#21 = Class #28 // java/io/PrintStream
#22 = NameAndType #29:#30 // println:(Ljava/lang/String;)V
#23 = Utf8 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."<init>":()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."<init>":()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."<init>":()V
#2 = Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #20 // Hello World
#4 = Methodref #21.#22 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #23 // HelloWorld
#6 = Class #24 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 MethodParameters
#14 = Utf8 args
#15 = Utf8 SourceFile
#16 = Utf8 HelloWorld.java
#17 = NameAndType #7:#8 // "<init>":()V
#18 = Class #25 // java/lang/System
#19 = NameAndType #26:#27 // out:Ljava/io/PrintStream;
#20 = Utf8 Hello World
#21 = Class #28 // java/io/PrintStream
#22 = NameAndType #29:#30 // println:(Ljava/lang/String;)V
#23 = Utf8 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."<init>":()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."<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/zhuhjay/demo3/Demo3_1
#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/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 // "<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/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."<init>":()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)` 就会存储到常量池中。
<img src="/jvm/1670221697470.png" alt="1670221697470" style="zoom:50%;" />
#### 4) 方法字节码载入方法区
<img src="/jvm/1670222538332.png" alt="1670222538332" style="zoom:50%;" />
#### 5) main 线程开始运行,分配栈帧内存
会根据以下信息来进行栈帧分配操作数栈深度为2局部变量表长度为4
```
stack=2, locals=4, args_size=1
```
<img src="/jvm/1670222781062.png" alt="1670222781062" style="zoom:50%;" />
#### 6) 执行引擎开始执行字节码
**bipush 10**
- 将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有
- sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)
- ldc 将一个 int 压入操作数栈
- ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节)
- 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池
<img src="/jvm/1670223128743.png" alt="1670223128743" style="zoom:50%;" />
**istore_1**
- 将操作数栈顶数据弹出,存入局部变量表的 slot 1
<img src="/jvm/1670223335018.png" alt="1670223335018" style="zoom:50%;" />
<img src="/jvm/1670223376535.png" alt="1670223376535" style="zoom:50%;" />
以上操作代表源代码的 `int a = 10;` 这个赋值操作
**ldc #3**
- 从常量池加载 #3 数据到操作数栈
- **注意** `Short.MAX_VALUE` 是 32767所以 `32768 = Short.MAX_VALUE + 1` 实际是在编译期间计算好的(常量折叠优化)
<img src="/jvm/1670223577473.png" alt="1670223577473" style="zoom:50%;" />
**istore_2**
- 将操作数栈顶数据弹出,存入局部变量表的 slot 2
<img src="/jvm/1670223846251.png" alt="1670223846251" style="zoom:50%;" />
以上操作代表源代码的 `int b = Short.MAX_VALUE + 1;` 这个赋值操作
**iload_1**
- 将局部变量表中的 slot 1 数据读取复制到操作数栈中
<img src="/jvm/1670223953960.png" alt="1670223953960" style="zoom:50%;" />
**iload_2**
- 将局部变量表中的 slot 2 数据读取复制到操作数栈中
<img src="/jvm/1670224060014.png" alt="1670224060014" style="zoom:50%;" />
**iadd**
- 将操作数栈中的数据进行相加的操作
<img src="/jvm/1670224223804.png" alt="1670224223804" style="zoom:50%;" />
<img src="/jvm/1670224265031.png" alt="1670224265031" style="zoom:50%;" />
**istore_3**
- 将操作数栈顶数据弹出,存入局部变量表的 slot 3
<img src="/jvm/1670224443009.png" alt="1670224443009" style="zoom:50%;" />
以上操作代表源代码的 `int c = a + b;` 这个操作
**getstatic #4**
- 从常量池中获取成员变量 #4 找到堆中的对象,将该对象的引用放入到操作数栈中
<img src="/jvm/1670224527319.png" alt="1670224527319" style="zoom: 50%;" />
**iload_3**
- 将局部变量表中的 slot 3 数据读取复制到操作数栈中
<img src="/jvm/1670224818125.png" alt="1670224818125" style="zoom:50%;" />
**invokevirtual #5**
- 找到常量池 #5
- 定位到方法区 `java/io/PrintStream.println:(I)V` 方法
- 生成新的栈帧(分配 locals、stack等每执行一个方法都会有新的栈帧
- 传递参数,执行新栈帧中的字节码
<img src="/jvm/1670225026514.png" alt="1670225026514" style="zoom:50%;" />
- 执行完毕,弹出栈帧
- 清除 main 操作数栈内容
<img src="/jvm/1670225112947.png" alt="1670225112947" style="zoom:50%;" />
**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 压入到操作数栈中
<img src="/jvm/1670225735794.png" alt="1670225735794" style="zoom:50%;" />
- 将操作数栈顶数据弹出,存入局部变量表的 slot 1此时完成了代码 `int a = 10` 的操作
<img src="/jvm/1670225834483.png" alt="1670225834483" style="zoom:50%;" />
- 将局部变量表中的 slot 1 的数据读取复制到操作数栈中,`a++` 执行顺序是先读取复制,再进行自增的操作
<img src="/jvm/1670225887463.png" alt="1670225887463" style="zoom:50%;" />
- 对局部变量表中的 slot 1 进行长度为 1 的自增,是直接在局部变量表中直接进行操作,而不是在操作数栈中进行
- 此时局部变量表中的 a 和 操作数栈中的数值不是一个相等的值
<img src="/jvm/1670225973371.png" alt="1670225973371" style="zoom:50%;" />
- 现将进行 `++a` 的运算,会先对局部变量表中的 slot 1 中的数值进行自增,而后才将该结果读取复制到操作数栈中
<img src="/jvm/1670226003812.png" alt="1670226003812" style="zoom:50%;" />
- 读取局部变量表中 slot 1 的数值到操作数栈中
<img src="/jvm/1670226078219.png" alt="1670226078219" style="zoom:50%;" />
- 计算完 `a++``++a` 过后,操作数栈中已经存在两个数据了,所以会先对操作数栈中的数据进行相加的操作
<img src="/jvm/1670226107200.png" alt="1670226107200" style="zoom:50%;" />
- 现将进行 `a--` 的运算,先将局部变量表中 slot 1 读取复制到操作数栈中
<img src="/jvm/1670226176345.png" alt="1670226176345" style="zoom:50%;" />
- 将局部变量表中的 slot 1 进行自减操作
<img src="/jvm/1670226329153.png" alt="1670226329153" style="zoom:50%;" />
- 将操作数栈中的数据进行相加,到此为止 `a++ + ++a + a--` 的运算结束
<img src="/jvm/1670226379924.png" alt="1670226379924" style="zoom:50%;" />
- 最后将操作数栈中的数据存放到局部变量表中对应的位置上去
<img src="/jvm/1670226433126.png" alt="1670226433126" style="zoom:50%;" />
### 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 |
- 几点说明:
- byteshortchar 都会按 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:
>
> 以上比较指令中没有 longfloatdouble 的比较,那么它们要比较怎么办?
>
> 参考 [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) `<clinit>()V`
```java
public class Demo3_8 {
static int i = 10;
static {
i = 20;
}
static {
i = 30;
}
}
```
编译器会按从上至下的顺序,收集所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方法`<clinit>()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) `<init>()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 "<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` 是创建【对象】,给对象分配堆内存,执行成功会将【对象引用】压入操作数栈
- `dup` 是赋值操作数栈栈顶的内容,本例即为【对象引用】,为什么需要两份引用呢,一个是要配合 `invokespecial` 调用该对象的构造方法 `"<init>":()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效果如下
<img src="/jvm/1670338229940.png" alt="1670338229940" style="zoom:50%;" />
#### 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`
<img src="/jvm/1670338857348.png" alt="1670338857348" style="zoom:80%;" />
#### 5) 查看对象的内存地址
通过 `Windows -> Console` 进入命令行模式,执行 `mem 0x0000023338896cd0 2`
- `mem` 有两个参数,参数 1 是对象地址,参数 2 是查看 2 行(即 16 字节)
在结果中的第二行即为 Class 的内存地址
<img src="/jvm/1670339471963.png" alt="1670339471963" style="zoom:80%;" />
#### 6) 查看类的 vtable
- 方法一:`Alt+R` 进入 `Inspector` 工具,输入刚刚查询到的 Class 内存地址
<img src="/jvm/1670339740477.png" alt="1670339740477" style="zoom:80%;" />
- 方法二:打开 `Tools -> Class Browser` 输入 Dog 查找,也一样可以找到该 Class 对应的内存地址,然后使用方法一进行查询
<img src="/jvm/1670339913417.png" alt="1670339913417" style="zoom: 67%;" />
无论通过哪种方法,都可以找到 `Dog Class` 的 `vtable` 长度为 6意思就是 `Dog` 类有 6 个虚方法多态相关的finalstatic 不会列入)
那么这 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 -> <init>:()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."<init>":()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<Integer> 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."<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: ...
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;>;
```
使用反射,仍然能够获得这些信息(只能获取到方法参数上和返回值的泛型信息):
```java
public Set<Integer> test(List<String> list, Map<Integer, Object> 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<Integer> 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<Integer> 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()=0FEMALE 的 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<Sex> {
public static final Sex MALE;
public static final Sex FEMALE;
private static final Sex[] $VALUES;
static {
MALE = new Sex("MALE", 0);
FEMALE = new Sex("FEMALE", 1);
$VALUES = new Sex[]{MALE, FEMALE};
}
private Sex(String name, int ordinal) {
super(name, ordinal);
}
public static Sex[] values() {
return $VALUES.clone();
}
public static Sex valueOf(String name) {
return Enum.valueOf(Sex.class, name);
}
}
```
### 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` 修饰)