- 新增 .drone.yml 文件用于定义 CI/CD 流程 - 配置了基于 Docker 的部署步骤 - 设置了工作区和卷映射以支持持久化数据 - 添加了构建准备阶段和 Docker 部署阶段 - 定义了环境变量和代理设置 - 配置了 artifacts 目录的处理逻辑 - 添加了 timezone 映射以确保时间同步 - 设置了 docker.sock 映射以支持 Docker in Docker
2394 lines
71 KiB
Markdown
2394 lines
71 KiB
Markdown
---
|
||
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++使用十六进制进行查看)
|
||
|
||

|
||
|
||
根据 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】类型的文件
|
||
|
||

|
||
|
||
|
||
|
||
### 1.2 版本
|
||
|
||
4-7 字节,表示类的版本 00 34(52) 表示是 Java 8
|
||
|
||

|
||
|
||
|
||
|
||
### 1.3 常量池
|
||
|
||
8-9 字节,表示常量池长度,00 1f (31) 表示常量池有 #1- #30 项,注意 #0 项不计入,也没有值,可以发现一个 Class 文件中最多具备 65535(2^16) 个常量,它包括了以下所有类型。
|
||
|
||

|
||
|
||
常量池类型映射表,目前为止 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 项。
|
||
|
||

|
||
|
||
|
||
|
||
常量池中 #2 项 09(9),由映射表得知是一个 Field 信息,其通过后续的 00 12(18) 和 00 13(19) 分别表示该成员变量的【所属类】引用常量池中 #18 项、【成员变量名】引用常量池中 #19 项。
|
||
|
||

|
||
|
||
|
||
|
||
常量池中 #3 项 08(8),由映射表得知是一个字符串常量名称,其通过后续的 00 14(20)表示其引用了常量池中 #20 项。
|
||
|
||

|
||
|
||
|
||
|
||
依此类推进行其他常量池的解析,这里就不演示了,了解一个解析形式即可。
|
||
|
||
|
||
|
||
以上分析结果的常量池大概就是使用反编译后的常量池结果,而且可以看到所有的字符串常量池中的信息一一对应
|
||
|
||
```
|
||
#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 入门
|
||
|
||
分析方法中的字节码指令信息
|
||
|
||
构造方法对应的字节码信息如下图,对应的反编译信息如下文本块所示
|
||
|
||

|
||
|
||
```
|
||
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方法对应的字节码信息如下图,对应的反编译信息如下文本块所示
|
||
|
||

|
||
|
||
```
|
||
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)
|
||
|
||

|
||
|
||
|
||
|
||
### 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 |
|
||
|
||
- 几点说明:
|
||
- 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) `<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 对象内存地址。
|
||
|
||

|
||
|
||
|
||
|
||
#### 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 个虚方法(多态相关的,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 -> <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 的一个静态内部类
|
||
|
||

|
||
|
||
字节码如下
|
||
|
||
```
|
||
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<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` 修饰)
|