若文章内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系博主删除。
- 参考书籍:《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明 著
| 本博客内容概览 |

建议诸位看书:《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明 著,此处仅摘抄了书中的一部分。
类文件简述
Class 文件是一组以 8 个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件之中,中间没有添加任何分隔符,这使得整个 Class 文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。
当遇到需要占用 8 个字节以上空间的数据项时,则会按照高位在前的方式分割成若干个 8 个字节进行存储。
根据《Java虚拟机规范》的规定,Class 文件格式采用一种类似于 C 语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:“无符号数” 和 “表”。后面的解析都要以这两种数据类型为基础,所以这里笔者必须先解释清楚这两个概念。
无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时候称这一系列连续的某一类型的数据为某一类型的 “集合”。
魔数
每个 Class 文件的头 4 个字节被称为魔数(Magic Number)。
它的唯一作用是确定这个文件是否为一个能被虚拟机接受的 Class 文件。
Class 文件的魔数取得很有 “浪漫气息”,值为 0xCAFEBABE(咖啡宝贝?)
版本号
紧接着魔数的 4 个字节存储的是 Class 文件的版本号
常量池
紧接着主、次版本号之后的是常量池入口
常量池可以比喻为 Class 文件里的资源仓库
它是 Class 文件结构中与其他项目关联最多的数据,通常也是占用 Class 文件空间最大的数据项目之一
另外,它还是在 Class 文件中第一个出现的表类型数据项目。
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。
常量池中每一项常量都是一个表,截至 JDK 13,常量表中分别有 17 种不同类型的常量。
访问标志
在常量池结束之后,紧接着的 2 个字节代表访问标志(access_flags)
这个标志用于识别一些类或者接口层次的访问信息,包括:
类索引、父类索引与接口索引集合
类索引、父类索引和接口索引集合都按顺序排列在访问标志之后。
类索引(this_class)和父类索引(super_class)都是一个 u2 类型的数据,而接口索引集合(interfaces)是一组 u2 类型的数据的集合
Class 文件中由这三项数据来确定该类型的继承关系。
类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。
由于 Java 语言不允许多重继承,所以父类索引只有一个,除了 java.lang.Object 之外,所有的 Java 类都有父类
因此除了 java.lang.Object 外,所有 Java 类的父类索引都不为 0。
接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按 implements 关键字后的接口顺序从左到右排列在接口索引集合中。(如果这个 Class 文件表示的是一个接口,则应当是 extends 关键字)
字段表集合
字段表(field_info)用于描述接口或者类中声明的变量。
Java 语言中的 “字段”(Field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。
读者可以回忆一下在 Java 语言中描述一个字段可以包含哪些信息。
字段可以包括的修饰符有字段的作用域(public、private、protected 修饰符)、是实例变量还是类变量(static 修饰符)、可变性(final)、并发可见性(volatile 修饰符,是否强制从主内存读写)、可否被序列化(transient 修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。
上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。
而字段叫做什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。
字段修饰符放在 access_flags 项目中,它与类中的 access_flags 项目是非常类似的,都是一个 u2 的数据类型
跟随 access_flags 标志的是两项索引值:name_index 和 descriptor_index。
它们都是对常量池项的引用,分别代表着字段的简单名称以及字段和方法的描述符。
其中描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。
字段表所包含的固定数据项目到 descriptor_index 为止就全部结束了。
不过在 descrip-tor_index 之后跟随着一个属性表集合,用于存储一些额外的信息,字段表可以在属性表中附加描述零至多项的额外
信息。

方法表集合
Class 文件存储格式中对方法的描述与对字段的描述采用了几乎完全一致的方式,方法表的结构如同字段表一样,依次包括访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项 … …
方法的定义可以通过访问标志、名称索引、描述符索引来表达清楚,但方法里面的代码去哪里了?
方法里的 Java 代码,经过 Javac 编译器编译成字节码指令之后,存放在方法属性表集合中一个名为 “Code” 的属性里面。
属性表是 Class文件格式中最具扩展性的一种数据项目。

属性表集合
属性表(attribute_info)在前面的讲解之中已经出现过数次,Class文件、字段表、方法表都可以携带自己的属性表集合,以描述某些场景专有的信息。
与 Class 文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格顺序,并且《Java 虚拟机规范》允许只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java 虚拟机运行时会忽略掉它不认识的属性。
为了能正确解析 Class 文件,《Java 虚拟机规范》最初只预定义了 9 项所有 Java 虚拟机实现都应当能识别的属性。
而在最新的《Java 虚拟机规范》的 Java SE 12 版本中,预定义属性已经增加到 29 项
对于每一个属性,它的名称都要从常量池中引用一个 CONSTANT_Utf8_info 类型的常量来表示,而属性值的结构则是完全自定义的,只需要通过一个 u4 的长度属性去说明属性值所占用的位数即可。
官方文档链接:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html
这里推荐一个插件:jclasslib Bytecode Viewer,可借助此插件来帮助我们阅读字节码文件。


一个简单的 HelloWorld.java
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
执行 javac -parameters -d . HellowWorld.java
编译为 HelloWorld.class 后是这个样子的
[root@localhost ~]# od -t xC HelloWorld.class
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07
0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29
0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e
0000100 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63
0000120 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01
0000140 00 04 74 68 69 73 01 00 1d 4c 63 6e 2f 69 74 63
0000160 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c 6f
0000200 57 6f 72 6c 64 3b 01 00 04 6d 61 69 6e 01 00 16
0000220 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72
0000240 69 6e 67 3b 29 56 01 00 04 61 72 67 73 01 00 13
0000260 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69
0000300 6e 67 3b 01 00 10 4d 65 74 68 6f 64 50 61 72 61
0000320 6d 65 74 65 72 73 01 00 0a 53 6f 75 72 63 65 46
0000340 69 6c 65 01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64
0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e
0000400 00 1f 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64
0000420 07 00 20 0c 00 21 00 22 01 00 1b 63 6e 2f 69 74
0000440 63 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c
0000460 6f 57 6f 72 6c 64 01 00 10 6a 61 76 61 2f 6c 61
0000500 6e 67 2f 4f 62 6a 65 63 74 01 00 10 6a 61 76 61
0000520 2f 6c 61 6e 67 2f 53 79 73 74 65 6d 01 00 03 6f
0000540 75 74 01 00 15 4c 6a 61 76 61 2f 69 6f 2f 50 72
0000560 69 6e 74 53 74 72 65 61 6d 3b 01 00 13 6a 61 76
0000600 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d
0000620 01 00 07 70 72 69 6e 74 6c 6e 01 00 15 28 4c 6a
0000640 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
0000700 00 07 00 08 00 01 00 09 00 00 00 2f 00 01 00 01
0000720 00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a 00
0000740 00 00 06 00 01 00 00 00 04 00 0b 00 00 00 0c 00
0000760 01 00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e 00
0001000 0f 00 02 00 09 00 00 00 37 00 02 00 01 00 00 00
0001020 09 b2 00 02 12 03 b6 00 04 b1 00 00 00 02 00 0a
0001040 00 00 00 0a 00 02 00 00 00 06 00 08 00 07 00 0b
0001060 00 00 00 0c 00 01 00 00 00 09 00 10 00 11 00 00
0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00
0001120 00 00 02 00 14
根据 JVM 规范,类文件结构如下
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
研究一下两组字节码指令
public cn.itcast.jvm.t5.HelloWorld(); 构造方法的字节码指令public static void main(java.lang.String[]); 主方法的字节码指令public cn.itcast.jvm.t5.HelloWorld(); 构造方法的字节码指令2a b7 00 01 b1
Method java/lang/Object."":()V public static void main(java.lang.String[]); 主方法的字节码指令b2 00 02 12 03 b6 00 04 b1
Field java/lang/System.out:Ljava/io/PrintStream;String hello worldMethod java/io/PrintStream.println:(Ljava/lang/String;)V自己分析类文件结构太麻烦了,Oracle 提供了 javap (JAVA 字节码分析工具)来反编译 class 文件(javap -v HelloWorld.class)
D:\IdeaProjects\tests\test_7\test_7a\out\production\test_7a>javap -v HelloWorld.class
Classfile /D:/IdeaProjects/tests/test_7/test_7a/out/production/test_7a/HelloWorld.class
Last modified 2022-8-29; size 534 bytes
MD5 checksum f085642c9a175e363b8cde7b90538c83
Compiled from "HelloWorld.java"
public class HelloWorld
minor version: 0
major version: 52 # 代表 jdk8
flags: ACC_PUBLIC, ACC_SUPER # 类的访问修饰符
Constant pool: # 常量池
#1 = Methodref #6.#20 // java/lang/Object."":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // Hello World!
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // HelloWorld
#6 = Class #27 // java/lang/Object
#7 = Utf8
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 LHelloWorld;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 HelloWorld.java
#20 = NameAndType #7:#8 // "":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 Hello World!
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 HelloWorld
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
{ # 方法信息
public HelloWorld();
descriptor: ()V # 描述符:方法的参数列表和返回值。无返回值的 void 类型用一个大写字符 V 表示
flags: ACC_PUBLIC # 访问标志
Code: # 程序方法体里的代码
stack=1, locals=1, args_size=1 # 操作数栈的深度的最大值,局部变量表所需的存储空间,参数长度
0: aload_0 # 把局部变量的第 0 项加载到操作数栈
1: invokespecial #1 // Method java/lang/Object."":()V # 调用常量池中的第 1 项方法
4: return
LineNumberTable: # 用于描述 [Java 源码行号] 与 [字节码行号(字节码偏移量)] 之间的对应关系
line 1: 0
LocalVariableTable: # 用于描述 [栈帧中局部变量表的变量] 与 [JAVA 源码中定义的变量] 之间的关系
Start Length Slot Name Signature
0 5 0 this LHelloWorld;
# Start、Length:[局部变量表] 的 [生命周期] 开始的 [字节码偏移量] 及其 [作用范围覆盖的长度]
# 上述二者集合即为该 [局部变量] 在 [字节码] 中的 [作用域范围]
# Slot:这个局部变量在栈帧的局部变量表中变量槽的位置
# Name:局部变量的名称
# Signature:字段的特征签名,功能上算是描述符的加强版(添加了准确描述泛型类型的功能)
# 描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型、顺序)和返回值
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
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"
原始代码
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);
}
}
编译后的字节码文件
Classfile /D:/IdeaProjects/Study/wStudy2022/JVMStudies/chapter03/target/classes/Demo3_1.class
Last modified 2022-8-31; size 583 bytes
MD5 checksum 20278f2ddca7abcf4d365af690530fcf
Compiled from "Demo3_1.java"
public class Demo3_1
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#25 // java/lang/Object."":()V
#2 = Class #26 // java/lang/Short
#3 = Integer 32768
#4 = Fieldref #27.#28 // java/lang/System.out:Ljava/io/PrintStream;
#5 = Methodref #29.#30 // java/io/PrintStream.println:(I)V
#6 = Class #31 // Demo3_1
#7 = Class #32 // java/lang/Object
#8 = Utf8
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 LDemo3_1;
#15 = Utf8 main
#16 = Utf8 ([Ljava/lang/String;)V
#17 = Utf8 args
#18 = Utf8 [Ljava/lang/String;
#19 = Utf8 a
#20 = Utf8 I
#21 = Utf8 b
#22 = Utf8 c
#23 = Utf8 SourceFile
#24 = Utf8 Demo3_1.java
#25 = NameAndType #8:#9 // "":()V
#26 = Utf8 java/lang/Short
#27 = Class #33 // java/lang/System
#28 = NameAndType #34:#35 // out:Ljava/io/PrintStream;
#29 = Class #36 // java/io/PrintStream
#30 = NameAndType #37:#38 // println:(I)V
#31 = Utf8 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 Demo3_1(); # 构造方法
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 4: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LDemo3_1;
public static void main(java.lang.String[]); # main 方法
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 6: 0
line 7: 3
line 8: 6
line 9: 10
line 10: 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"
注意:运行时常量池也是方法区的组成部分,这里只为了区别突出运行时常量池,才在图中将其与方法区分开。
- 运行时常量池(Runtime Constant Pool)是方法区的一部分
- Class 文件中除了有类的版本、字段、方法、接口等描述信息外
- 还有一项信息是常量池表(Constant Pool Table)
- 用于存放编译期生成的各种字面量与符号引用
- 这部分内容将在类加载后存放到方法区的运行时常量池中

当 Java 代码被执行时,Java 虚拟机的类加载器,会对上面代码中的 main 方法所在的类进行类加载的操作。
- Java 虚拟机把描述类的数据从 Class 文件加载到内存
- 并对数据进行校验、转换解析和初始化
- 最终形成可以被虚拟机直接使用的 Java 类型
- 该过程被称为虚拟机的类加载机制
字节码文件中的常量池的数据会放入运行时常量池中
Java 源码中一些较小的数值并不是存储在常量池的,而是跟着这个方法的字节码指令存在一起。
一旦这个数值的大小超过了这个 short 的最大值(32767),那么它就会存储在常量池中

main 线程开始运行,分配栈帧内存(stack=2, locals=4)

接下来就是执行引擎执行字节码的内容了
| 执行引擎执行字节码 |



Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算好的


a + b 运算操作是要栈中完成的,在局部变量表中不能完成运算










java/io/PrintStream.println:(I)V 方法

最终整个程序结束
/**
* 从字节码角度分析 a ++ 相关题目
*/
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: (0x0009) 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_
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 8: 0
line 9: 3
line 10: 18
line 11: 25
line 12: 32
LocalVariableTable:
Start Length Slot Name Signature
0 33 0 args [Ljava/lang/String;
3 30 1 a I
18 15 2 b I
a++ 和 ++a 的区别是先执行 ①iload 还是先执行 ②iinc
a++ 是先①后②,a-- 是先②后①# 接下来我们分析的主要是下面这段字节码,后面的打印部分的字节码就不看了
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_
17: istore_2
bipush 10

istore_1

自此,a = 10 操作完成
iload 1

iinc 1,1:该指令有两个参数,第一个 1 是代表要执行该指令的槽位,第二个 1 是要自增的值

iinc 1,1

iload 1

a++ + ++a
a++ 和 ++a 的操作之前已经演示过了,故此处直接演示 iadd
iadd

iload 1

iinc 1,-1

a++ + ++a + a--
a++ + ++a 的运算和 a– 的自减运算,前面都已经做过了,故此处直接演示第二次运算
iadd

istore 2

几点说明
更多指令可查阅:https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.lcmp
源码
public class Demo3_3 {
public static void main(String[] args) {
int a = 0;
if (a == 0) {
a = 10;
} else {
a = 20;
}
}
}
字节码
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=1
0: iconst_0 # iconst 可以将 -1~5 之间的常数加载至操作数栈
1: istore_1
2: iload_1
3: ifne 12 # 判断条件(此处为 a=0),条件不成立则跳转到下面的 12 行(12:bipush 20),条件成立则继续往下
6: bipush 10
8: istore_1
9: goto 15
12: bipush 20
14: istore_1
15: return
public class Demo3_4 {
public static void main(String[] args) {
int a = 0;
while (a < 10) {
a++;
}
}
}
Code:
stack=2, locals=2, args_size=1
0: iconst_0
1: istore_1
2: iload_1
3: bipush 10
5: if_icmpge 14 # 判断条件是否成立,成立则继续向下执行;否则,直接跳转到 14 行(14:return)
8: iinc 1, 1
11: goto 2
14: return
public class Demo3_5 {
public static void main(String[] args) {
int a = 0;
do {
a++;
} while (a < 10);
}
}
Code:
stack=2, locals=2, args_size=1
0: iconst_0
1: istore_1
2: iinc 1, 1
5: iload_1
6: bipush 10
8: if_icmplt 2
11: return
public class Demo3_6 {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
}
}
}
Code:
stack=2, locals=2, args_size=1
0: iconst_0
1: istore_1
2: iload_1
3: bipush 10
5: if_icmpge 14
8: iinc 1, 1
11: goto 2
14: return
比较 while 和 for 的字节码,发现它们是一模一样的,殊途也能同归
public class Demo3_7 {
public static void main(String[] args) {
int i = 0;
int x = 0;
while (i < 10) {
// x 先在局部变量表上的槽位 自增(a 变为 1)
// 之后将操作数栈的值(也就是初始值 0)赋值给 x
// 故 x 值并未改变,依旧为 0
x = x++;
i++;
}
System.out.println(x); // 结果是 0
}
}
复习回顾相关的指令(以下命令中 i 开头的都是指 int 类型)
iconst:将一个常量加载到操作数栈iload:将一个局部变量加载到操作数栈istore:将一个数值从操作数栈加载到局部变量表中bipush:将一个 byte 类型的局部变量加载到操作数栈中,本质上是使用相应的对 int 类型作为运算类型来进行的ifcpmge:条件分支判断iinc:局部变量自增(在局部变量表的槽上进行该操作)gestatic:访问类字段(static 字段,或者称为类变量)invokevirtual:用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),是 Java 语言中最常见的方法分派方式
Code:
stack=2, locals=3, args_size=1 # 操作数栈分配 2 个空间,局部变量表分配 3 个空间,参数的长度是 1
0: iconst_0 # 准备一个常数 0(将一个常量加载到操作数栈)
1: istore_1 # 将常数 0 放入局部变量表的 1 号槽位,此时 i = 0
# 将操作数栈的栈顶元素存储到局部变量表中
2: iconst_0 # 准备一个常数 0
3: istore_2 # 将常数 0 放入局部变量的 2 号槽位 x = 0
4: iload_1 # 将局部变量表 1 号槽位的数放入操作数栈中
5: bipush 10 # 将数字 10 放入操作数栈中,此时操作数栈中有 2 个数
7: if_icmpge 21 # 比较操作数栈中的两个数,如果下面的数大于上面的数,就跳转到 21 。
# 这里的比较是将两个数做减法。
# 因为涉及运算操作,所以会将两个数弹出操作数栈来进行运算。
# 运算结束后操作数栈为空
10: iload_2 # 将局部变量 2 号槽位的数放入操作数栈中,放入的值是 0
11: iinc 2, 1 # 将局部变量 2 号槽位的数加 1 ,自增后,槽位中的值变为了 1
14: istore_2 # 将操作数栈中的数放入到局部变量表的 2 号槽位,2 号槽位的值又变为了 0
15: iinc 1, 1 # 1 号槽位的值自增 1
18: goto 4 # 跳转到第 4 条指令
21: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
24: iload_2
25: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
28: return
public class Demo3_8_1 {
static int i = 10;
static {
i = 20;
}
static {
i = 30;
}
//最终 i 的值是 30
}
编译器会按从上至下的顺序,收集所有 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
方法会在类加载的初始化阶段被调用
:每个实例对象的构造方法
public class Demo3_8_2 {
private String a = "s1";
{
b = 20;
}
private int b = 10;
{
a = "s2";
}
public Demo3_8_2(String a, int b) {
this.a = a;
this.b = b;
}
public static void main(String[] args) {
Demo3_8_2 d = new Demo3_8_2("s3", 30);
System.out.println(d.a); // s3
System.out.println(d.b); // 30
}
}
编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法
但原始构造方内的代码也会附加到新的构造方法中,且总是在最后
public cn.itcast.jvm.t3.bytecode.Demo3_8_2(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 // super.()V
4: aload_0
5: ldc #2 // <- "s1"
7: putfield #3 // -> this.a
10: aload_0
11: bipush 20 // <- 20
13: putfield #4 // -> this.b
16: aload_0
17: bipush 10 // <- 10
19: putfield #4 // -> this.b
22: aload_0
23: ldc #5 // <- "s2"
25: putfield #3 // -> this.a
28: aload_0 // ------------------------------
29: aload_1 // <- slot 1(a) "s3" |
30: putfield #3 // -> this.a |
33: aload_0 |
34: iload_2 // <- slot 2(b) 30 |
35: putfield #4 // -> this.b --------------------
38: return
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
0 39 0 this Lcn/itcast/jvm/t3/bytecode/Demo3_8_2;
0 39 1 a Ljava/lang/String;
0 39 2 b I
MethodParameters: ...
参考书籍:《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明 著
实例构造器 方法 和 类构造器 方法 就是在字节码生成阶段被添加到语法树之中的。
请注意这里的实例构造器并不等同于默认构造函数。
如果用户代码中没有提供任何构造函数:
)与当前类型一致的默认构造函数 和 这两个构造器的产生实际上是一种代码收敛的过程,编译期会进行如下操作:
() 和 () 方法之中
() 方法中无须调用父类的 () 方法,Java 虚拟机会自动保证父类构造器的正确执行() 方法中经常会生成调用 java.lang.Object 的 () 方法的代码除了生成构造器以外,还有其他的一些代码替换工作用于优化程序某些逻辑的实现方式。
如把字符串的加操作替换为 StringBuffer 或 StringBuilder(取决于目标代码的版本是否大于或等于 JDK 5)的 append() 操作,等等。
看一下几种不同的方法调用对应的字节码指令
public class Demo3_9 {
public Demo3_9() { }
private void test1() { }
private final void test2() { }
public void test3() { }
public static void test4() { }
@Override
public String toString() { return super.toString(); }
public static void main(String[] args) {
Demo3_9 d = new Demo3_9();
d.test1();
d.test2();
d.test3();
d.test4();
Demo3_9.test4();
d.toString();
}
}
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #3 // class bytecode/Demo3_9
# new 指令实际上做了两部操作
# 1.在堆空间分配内存
# 2.分配成功后会把对象的引用放入操作数栈
3: dup # 复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶
4: invokespecial #4 // Method "":()V
7: astore_1
8: aload_1
9: invokespecial #5 // Method test1:()V
12: aload_1
13: invokespecial #6 // Method test2:()V
16: aload_1
17: invokevirtual #7 // Method test3:()V
20: aload_1
21: pop # 将操作数栈的栈顶一个或两个元素出栈
# 此处执行 pop 指令,是因为 test4() 是静态方法
# 静态方法不需要对象来调用,所以静态方法一调用后就直接出栈了
# 所以平时也不要使用对象来调用静态方法了
# 不然会多产生一些不必要的指令。比如此处的 aload 和 pop
22: invokestatic #8 // Method test4:()V
25: invokestatic #8 // Method test4:()V
28: aload_1
29: invokevirtual #9 // Method toString:()Ljava/lang/String;
32: pop
33: return
new 是创建 对象,给对象分配堆内存,执行成功会将 对象引用 压入操作数栈dup 是赋值操作数栈栈顶的内容
invokespecial 调用该对象的构造方法 "":()V (会消耗掉栈顶一个引用)astore_1 赋值给局部变量invokespecial 指令来调用,属于静态绑定invokespecial 的情况是通过 super 调用父类方法invokevirtual 调用,属于动态绑定,即支持多态invokestatic 之前执行了 pop 指令,把 对象引用 从操作数栈弹掉了参考书籍:《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明 著
这里仅列举以下五条指令用于方法调用:
invokevirtual:用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这是 Java 语言中最常见的方法分派方式invokeinterface:用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用invokespecial:用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法invokestatic:用于调用类静态方法(static 方法)invokedynamic:用于在运行时动态解析出调用点限定符所引用的方法。并执行该方法前面四条调用指令的分派逻辑都固化在 Java 虚拟机内部,用户无法改变。
而 invokedynamic 指令的分派逻辑是由用户所设定的引导方法决定的。
方法调用指令与数据类型无关。
而方法返回指令是根据返回值的类型区分的
return 指令供声明为 void 的方法、实例初始化方法、类和接口的类初始化方法使用。import java.io.IOException;
/**
* 演示多态原理,注意加上下面的 JVM 参数,禁用指针压缩
* -XX:-UseCompressedOops -XX:-UseCompressedClassPointers
*/
public class Demo3_10 {
public static void test(Animal animal) {
animal.eat();
System.out.println(animal.toString());
}
public static void main(String[] args) throws IOException {
test(new Cat());
test(new Dog());
System.in.read();
}
}
abstract class Animal {
public abstract void eat();
@Override
public String toString() {
return "我是" + this.getClass().getSimpleName();
}
}
class Dog extends Animal {
@Override
public void eat() {
System.out.println("啃骨头");
}
}
class Cat extends Animal {
@Override
public void eat() {
System.out.println("吃鱼");
}
}
上述代码块中使用了 System.in.read() 方法,旨在此处暂停程序运行,此时可以运行 jps 获取进程 id

进入 JDK 安装目录(可以用 java -verbose 输出信息,最后一行即是 jdk 安装位置),执行下面的命令
java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB
进入图形界面 attach 进程 id

关于 HSDB 连不上进程的解决办法
若 idea 的控制台上报如下的错误,可以根据控制台的输出信息来找到解决办法
C:\Program Files\Java\jdk1.8.0_144>java -cp .\lib\sa-jdi.jar sun.jvm.hotspot.HSDB
Exception in thread "main" java.lang.UnsatisfiedLinkError: Can't load library: C:\Program Files\Java\jre1.8.0_144\bin\sawindbg.dll
at java.lang.ClassLoader.loadLibrary(Unknown Source)
at java.lang.Runtime.load0(Unknown Source)
at java.lang.System.load(Unknown Source)
at sun.jvm.hotspot.debugger.windbg.WindbgDebuggerLocal.<clinit>(WindbgDebuggerLocal.java:661)
at sun.jvm.hotspot.HotSpotAgent.setupDebuggerWin32(HotSpotAgent.java:567)
at sun.jvm.hotspot.HotSpotAgent.setupDebugger(HotSpotAgent.java:335)
at sun.jvm.hotspot.HotSpotAgent.go(HotSpotAgent.java:304)
at sun.jvm.hotspot.HotSpotAgent.attach(HotSpotAgent.java:156)
at sun.jvm.hotspot.HSDB.attach(HSDB.java:1236)
at sun.jvm.hotspot.HSDB.run(HSDB.java:432)
at sun.jvm.hotspot.HSDB.main(HSDB.java:55)
根据控制台的输出信息,提示:Can't load library: C:\Program Files\Java\jre1.8.0_144\bin\sawindbg.dll
可以去这个目录看看情况。发现该目录下并没有 sawindbg.dll 文件。
我自己是在 C:\Program Files\Java\jdk1.8.0_144\jre\bin 的目录下发现的 sawindbg.dll 文件。
复制该文件到之前无法加载的 C:\Program Files\Java\jre1.8.0_144\bin\ 的目录下就行了。
之后重新输入:java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB 这个命令就行了。

打开 Tools -> Find Object By Query
输入 select d from cn.itcast.jvm.t3.bytecode.Dog d 点击 Execute 执行,查出对象的内存地址。
上面的 select 语句根据自己的目录(Path From Source Root)就行,我的语句就是:select d from bytecode.Dog d
点击超链接可以看到对象的内存结构
此对象没有任何属性,因此只有对象头的 16 字节
前 8 字节是 MarkWord,后 8 字节就是对象的 Class 指针
但目前看不到它的实际地址

补充介绍
对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data) 和 对齐填充(Padding)。
HotSpot 虚拟机对象的对象头部分包括两类信息。
实例数据部分是对象真正存储的有效信息
对象的第三部分是对齐填充
可以通过 Windows -> Console 进入命令行模式,执行下面的命令
mem 0x00000001d38d9500 2
mem 有两个参数,参数 1 是对象地址,参数 2 是查看 2 行(即 16 字节)
输出的结果中,第一行是 mark word,中第二行 0x0000000025734028 即为 Class 的内存地址

方法1:Alt + R 进入 Inspector 工具,输入刚才的 Class 内存地址(0x0000000025734028)
下面的界面是对象结构的完整表示,处于方法区
- 方法区(Method Area) 与 Java 堆一样,是各个线程共享的内存区域
- 它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

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

无论通过哪种方法,都可以找到 Dog Class 的 vtable 长度为 6,意思就是 Dog 类有 6 个虚方法(多态相关的,final,static 不会列入)
那么这 6 个方法都是谁呢?从 Class 的起始地址开始算,偏移 0x1b8 就是 vtable 的起始地址,进行计算得到:0x00000000257341e0
0x0000000025734028
1b8 +
---------------------
0x00000000257341e0
通过 Windows -> Console 进入命令行模式,执行

mem 0x00000000257341e0 6
输出结构即为 Dog 类所有的支持重写方法的入口地址
0x00000000257341e0: 0x0000000025331b10
0x00000000257341e8: 0x00000000253315e8
0x00000000257341f0: 0x00000000257335e8
0x00000000257341f8: 0x0000000025331540
0x0000000025734200: 0x0000000025331678
0x0000000025734208: 0x0000000025733fa8
通过 Tools -> Class Browser 查看每个类的方法定义



比较各个类的方法定义可知
public void eat() @0x0000000025733fa8;
public java.lang.String toString() @0x00000000257335e8;
protected void finalize() @0x0000000025331b10;
public boolean equals(java.lang.Object) @0x00000000253315e8;
public native int hashCode() @0x0000000025331540;
protected native java.lang.Object clone() @0x0000000025331678;
对号入座,发现如下情况
因为普通成员方法需要在运行时才能确定具体的内容,所以虚拟机需要调用 invokevirtual 指令
在执行 invokevirtual 指令时,经历了以下几个步骤
public class Demo3_11_1 {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
}
}
}
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 # 把异常对象的引用地址存储到局部变量表中的 2 号槽位上(即 e)
9: bipush 20
11: istore_1
12: return
Exception table: # 异常表
from to target type # 此处的 target 和上面的行号对应
2 5 8 Class java/lang/Exception # 异常检测范围:[from, to)
LineNumberTable: # 用于描述 Java 源码行号与字节码行号(字节码偏移量)之间的对应关系
... ...
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
StackMapTable: ... ...
... ...
可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开的检测范围
一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型。
如果 type 匹配异常类型一致,进入 target 所指示行号 8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置
public class Demo3_11_2 {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (ArithmeticException e) {
i = 30;
} catch (NullPointerException e) {
i = 40;
} catch (Exception e) {
i = 50;
}
}
}
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 30
11: istore_1
12: goto 26
15: astore_2
16: bipush 40
18: istore_1
19: goto 26
22: astore_2
23: bipush 50
25: istore_1
26: return
Exception table:
from to target type
2 5 8 Class java/lang/ArithmeticException
2 5 15 Class java/lang/NullPointerException
2 5 22 Class java/lang/Exception
LineNumberTable:
line 5: 0
line 7: 2
line 14: 5
line 8: 8
line 9: 9
line 14: 12
line 10: 15
line 11: 16
line 14: 19
line 12: 22
line 13: 23
line 15: 26
LocalVariableTable:
Start Length Slot Name Signature # 几个 Exception 都放到了 2 号槽位里(复用)
# 这是因为这些 Exception 同一时刻只能发生一种错误
# 所以完全没有必要创建多个槽位来存储异常对象
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
StackMapTable: number_of_entries = 4
frame_type = 255 /* full_frame */
offset_delta = 8
locals = [ class "[Ljava/lang/String;", int ]
stack = [ class java/lang/ArithmeticException ]
frame_type = 70 /* same_locals_1_stack_item */
stack = [ class java/lang/NullPointerException ]
frame_type = 70 /* same_locals_1_stack_item */
stack = [ class java/lang/Exception ]
frame_type = 3 /* same */
因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用
即这些异常同一时刻只能发生一种,所以没必要创建多个槽位来存储异常对象
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class Demo3_11_3 {
public static void main(String[] args) {
try {
Method test = Demo3_11_3.class.getMethod("test");
test.invoke(null);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
public static void test() {
System.out.println("ok");
}
}
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: ldc #2 // class bytecode/Demo3_11_3
2: ldc #3 // String test
4: iconst_0
5: anewarray #4 // class java/lang/Class
8: invokevirtual #5 // Method java/lang/Class.getMethod:
// (Ljava/lang/String[Ljava/lang/Class;)Ljava/lang/reflect/Method;
11: astore_1
12: aload_1
13: aconst_null
14: iconst_0
15: anewarray #6 // class java/lang/Object
18: invokevirtual #7 // Method java/lang/reflect/Method.invoke:
// (Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
21: pop
22: goto 30
25: astore_1
26: aload_1
27: invokevirtual #11 // Method java/lang/ReflectiveOperationException.printStackTrace:()V
30: return
Exception table:
from to target type
0 22 25 Class java/lang/NoSuchMethodException
0 22 25 Class java/lang/IllegalAccessException
0 22 25 Class java/lang/reflect/InvocationTargetException
LineNumberTable:
line 10: 0
line 11: 12
line 14: 22
line 12: 25
line 13: 26
line 15: 30
LocalVariableTable:
Start Length Slot Name Signature
12 10 1 test Ljava/lang/reflect/Method;
26 4 1 e Ljava/lang/ReflectiveOperationException;
0 31 0 args [Ljava/lang/String;
StackMapTable: number_of_entries = 2
frame_type = 89 /* same_locals_1_stack_item */
stack = [ class java/lang/ReflectiveOperationException ]
frame_type = 4 /* same */
相当于多个 single-catch 的优化,把平级的异常都写在一起,让这些异常的入口是一样的
Exception table 中的 target 属性的值都一样
public class Demo3_11_4 {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
} finally {
i = 30;
}
}
}
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 # 这里之所以有 4 个槽位而不是 3 个槽位
# 因为存在 catch 不到 Exception 的情况
# 多加一个槽位是字节码指令的一个保障(在异常表中多捕获一个异常)
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 Exceptin -> e ----------------------
12: bipush 20 // |
14: istore_1 // 20 -> i |
15: bipush 30 // finally |
17: istore_1 // 30 -> i |
18: goto 27 // return -----------------------------------
# 其实 Exception 并不能捕获所有的异常
# 或许会抛出 Exception 的父类型或者平级类型
# 即使 Excpetion 捕获不到异常
# finally 部分的代码仍要被执行
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 // 剩余的异常类型,比如 Error
11 15 21 any // 剩余的异常类型,比如 Error
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
StackMapTable: ...
MethodParameters: ...
可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程
注意:虽然从字节码指令看来,每个块中都有 finally 块,但是 finally 块中的代码只会被执行一次
public class Demo3_12_1 {
public static void main(String[] args) {
int result = test();
System.out.println(result);
}
public static int test() {
try {
return 10;
} finally {
return 20;
}
}
}
程序运行后,在控制台上输出的信息是 20
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 放入栈顶(fianlly)
5: ireturn # 返回栈顶 int(20)
6: astore_1 # catch any -> slot 1(finally)
7: bipush 20 # <- 20 放入栈顶
9: ireturn # 返回栈顶 int(20)
Exception table:
from to target type
0 3 6 any
ireturn 被插入了所有可能的流程,因此返回结果肯定以 finally 的为准athrow 了,这告诉我们:如果在 finally 中出现了 return,会吞掉异常下方的代码块在执行时,就不会出现任何异常(因为异常已经被吞掉了)
public class Demo3_12_1S {
public static void main(String[] args) {
int result = test();
System.out.println(result); // 20
}
public static int test() {
try {
int i = 1 / 0;
return 10;
} finally {
return 20;
}
}
}
public class Demo3_12_2 {
public static void main(String[] args) {
int result = test();
System.out.println(result); // 10
}
public static int test() {
int i = 10;
try {
return i;
} finally {
i = 20;
}
}
}
public static int test();
descriptor: ()I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=0
# try 模块
0: bipush 10 # <- 10 放入栈顶
2: istore_0 # 10 -> i
3: iload_0 # <- i(10)
4: istore_1 # 10 -> slot 1,暂存至 slot 1,目的是为了 [固定] 返回值
# finally 模块
5: bipush 20 # <- 20 放入栈顶
7: istore_0 # 20 -> i
8: iload_1 # <- slot 1(10) 载入 slot 1 暂存的值
9: ireturn # 返回栈顶的 int(10)
10: astore_2 # try 模块代码中间如果出现了异常会跳到这一步
11: bipush 20
13: istore_0
14: aload_2
15: athrow # 存在 athrow,如果有异常,会报错
Exception table:
from to target type
3 5 10 any
我们发现如果在 try 模块中存在 return 变量,那么即使 finally 模块中的变量发生了变化,返回的依旧是 try 模块中的变量值
这是因为我们可以从字节码指令看到 try 中的变量会先被备份一次用来返回
但是如果 fianlly 模块中也存在 return 的话,那就优先执行 finally 模块中的 return
而且 finally 模块中存在 return 的时候会吞掉异常
回顾知识
同步代码块解决数据安全问题
- 为什么出现问题?(这也是我们判断多线程程序是否会有数据安全问题的标准)
- 是否是多线程环境
- 是否有共享数据
- 是否有多条语句操作共享语句
- 如何解决多线程安全问题
- 基本思想:让程序没有安全问题的环境
- 怎么实现?
- 把多条语句操作的代码给锁起来,让任意时刻只能有一个线程执行即可
- 同步代码块
- 锁多条语句操作共享数据,可以使用同步代码块实现
- 格式:
synchronized(任意对象){ 多条语句操作共享数据的代码 }
synchronized(任意对象)就相当于给代码加锁了,任意对象都可以看成是一把锁- 同步的好处:解决了多线程的数据安全问题
- 同步的弊端:
- 当线程很多时,因为每个线程都会上去判断同步上的锁
- 这是很耗费资源的,无形中会降低程序的运行效率
public class Demo3_13 {
public static void main(String[] args) {
Object lock = new Object();
synchronized (lock) {
System.out.println("ok");
}
}
}
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
# #####################################################################################################
# 第一行代码:Object lock = new Object();
0: new #2 // class java/lang/Object
# 即 new 了一个对象
3: dup # 这里是复制对象的引用,因为栈顶会消耗一份该对象的引用
4: invokespecial #1 // Method java/lang/Object."":()V
# 此处调用了构造方法
7: astore_1 # [lock 对象]的引用 —> lock
# 将第二份 对象的引用 赋值 给局部变量表中的 lock
# 至此,第一行代码执行完毕
# #####################################################################################################
8: aload_1 # <- [lock 对象]的引用(synchronized 开始)
# 此处进入了 synchronized 代码块,这里需要把对象的引用加载到操作数栈
9: dup # 复制 lock 对象的引用,现在我们就有 lock 对象的两个引用了
# 分别对应着 monitorenter 和 monitorexit 两个指令使用的阶段
10: astore_2 # 将刚刚新创建出来的对象引用存储到局部变量表的 2 号槽位上
11: monitorenter # monitorenter(lock 引用)
# 该指令会消耗掉栈顶元素(lock 对象的引用)
# 并对 lock 对象加锁
# #####################################################################################################
# 现在可以开始安全的执行 synchronized 代码块中的语句了(System.out.println("ok");)
12: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
15: ldc #4 // String ok
17: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
# 打印结束
# #####################################################################################################
# 怎么确保锁一定解开呢?这里利用了异常处理的操作
# #####################################################################################################
# 1.没有出现异常的情况
# #####################################################################################################
20: aload_2 # <- slot 2(lock 引用)
# 这里会把之前暂存于 slot 2 的 lock 对象的引用再次加载到栈顶
# 即将局部变量表中的 2 号槽位的数据 加载至 操作数栈中
21: monitorexit # monitorexit(lock 引用)
# 给 lock 对象解锁
# #####################################################################################################
# 2.如果期间出现了异常的情况,则会利用到异常表
# #####################################################################################################
22: goto 30
25: astore_3 # any -> slot 3
# 将异常对象的引用抛到局部变量表中的 3 号槽位上
26: aload_2 # <- slot 2(lock 引用)
# 加载之前暂存在局部变量表上的 2 号槽位的对象的引用
27: monitorexit # monitorexit(lock 引用)
# 给 lock 对象解锁
28: aload_3 # 加载之前在 3 号槽位上的异常对象
29: athrow # 抛出异常对象
# #####################################################################################################
30: return
Exception table:
from to target type
12 22 25 any
25 28 25 any
LineNumberTable:
... ...
LocalVariableTable:
... ...
StackMapTable: ... ...
注意:方法级别的 synchronized 不会在字节码指令中有所体现
语法糖
*.java 源码编译为 *.class 字节码的过程中,自动生成和转换的一些代码注意:
public class Candy1 { }
编译成 class 后的代码(前提是在该类中没有额外实现任何构造器的情况下)
public class Candy1 {
//这个无参构造器是 java 编译器帮我们加上的
public Candy1() {
//即调用父类 Object 的无参构造方法,即调用 java/lang/Object." ":()V
super();
}
}
拆装箱:基本类型和包装类型之间来回转换
这个特性是 JDK 5 开始加入的,下面为代码块 1
public class Candy2 {
public static void main(String[] args) {
Integer x = 1;
int y = x;
}
}
上面的代码块 1 在 JDK 5 之前的版本是无法编译通过的,必须改写为如下所示的代码块 2
public class Candy2 {
public static void main(String[] args) {
//装箱:基本类型转换为给包装类型
Integer x = Integer.valueOf(1);
//拆箱:包装类型转换为基本类型
int y = x.intValue();
}
}
显然之前版本的代码太麻烦了,需要在基本类型和包装类型之间来回转换(尤其是集合类中操作的都是包装类型)
因此这些转换的事情在 JDK 5 以后都由编译器在编译阶段完成。
即 代码块 1 都会在编译阶段被转换为 代码块 2
参考博客:《Integer 和 int 的区别》
Integer i1 = 127 时,会将 127 进行缓存Integer i2 = 127 时,就会直接从缓存中取,不会新 new 一个 Integeri1 和 i2 用 == 进行比较时,会为 true。泛型也是在 JDK 5 开始加入的特性
但 java 在编译泛型代码后会执行 泛型擦除 的动作
即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理
public class Candy3 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(10); // 实际上是调用 List.add(Object o)
Integer x = list.get(0); // 实际上是调用 Object bbj = List.get(int index);
}
}
所以在取值时,编译器真正生成的字节码中,还要额外做一个类型转换的操作
// 需要将 Object 转为 Integer
Integer x = (Integer)list.get(0);
如果前面的 x 变量类型修改为 int 基本类型那么最终生成的字节码是
// 需要将 Object 转为 Integer, 并执行拆箱操作
int x = ((Integer)list.get(0)).intValue();
还好这些麻烦事都不用自己做。
[擦除] 的是 [字节码] 上的 [泛型] 信息,可以看到 [LocalVariableTypeTable] 仍然保留了方法参数泛型的信息
Code:
stack=2, locals=3, args_size=1
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."":()V
7: astore_1
8: aload_1
9: bipush 10
11: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
# 装箱
//这里进行了 [泛型擦除],实际调用的是 add(Objcet o)
14: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
19: pop # 将操作数栈的栈顶元素出栈
20: aload_1
21: iconst_0 # iconst 是一个入栈指令
# 其作用是用来将 int 类型的数字、取值在 -1 到 5 之间的整数压入栈中
# 此处是设置下标(方便 List.get(int index); 获取数据)
# 这里也进行了 [泛型擦除],实际调用的是 get(Object o)
22: invokeinterface #6, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
# 这里进行了 [类型转换],将 Object 转换成了 Integer
27: checkcast #7 // class java/lang/Integer
30: astore_2
31: returnCopy
LineNumberTable: # Java 源码的行号与字节码指令的对应关系
line 8: 0
line 9: 8
line 10: 20
line 11: 31
LocalVariableTable: # 局部变量表(方法的局部变量的描述)
Start Length Slot Name Signatur
0 32 0 args [Ljava/lang/String;
8 24 1 list Ljava/util/List;
LocalVariableTypeTable: # 局部变量类型表
Start Length Slot Name Signature
8 24 1 list Ljava/util/List<Ljava/lang/Integer;>;
使用反射,仍然能够获得这些信息
缺陷是只能得到方法参数上泛型信息,和返回值上的泛型信息
public Set<Integer> test(List<String> list, Map<Integer, Object> map) { }
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("泛型参数[%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
可变参数也是 JDK 5 开始加入的新特性
public class Candy4 {
public static void foo(String... args) {
String[] arr = args; // 直接赋值
System.out.println(arr.length);
}
public static void main(String[] args) {
foo("hello", "world");
}
}
可变参数 String… args 其实是一个 String[] args ,从代码中的赋值语句中就可以看出来。
同样 java 编译器会在编译期间将上述代码变换为
public class Candy4 {
public Demo4 {}
public static void foo(String[] args) {
String[] arr = args;
System.out.println(arr.length);
}
public static void main(String[] args) {
foo(new String[]{"hello", "world"});
}
}
注意:如果调用了 foo() 则等价代码为 foo(new String[]{}) ,创建了一个长度为 0 的空的数组,而不会传递 null 进去
仍是 JDK 5 开始引入的语法糖,数组的循环
public class Candy5_1 {
public static void main(String[] args) {
//数组赋初值的简化写法也是法糖
int[] arr = {1, 2, 3, 4, 5};
for(int x : arr) { //编译之后就是 for 循环
System.out.println(x);
}
}
}
会被编译器转换为
public class Candy5_1 {
public Candy5_1 {}
public static void main(String[] args) {
int[] arr = new int[]{1, 2, 3, 4, 5};
for(int i=0; i<arr.length; ++i) {
int x = arr[i];
System.out.println(x);
}
}
}
而集合的循环
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);
}
}
}
实际被编译器转换为对迭代器的调用
public class Candy5_2 {
public Candy5_2 {}
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
//获得该集合的迭代器
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()) {
Integer x = iterator.next();
System.out.println(x);
}
}
}
注意
从 JDK 7 开始,switch 可以作用于字符串和枚举类,这个功能也是语法糖
public class Canydy6_1 {
public static void main(String[] args) {
String str = "hello";
switch (str) {
case "hello" :
System.out.println("h");
break;
case "world" :
System.out.println("w");
break;
default:
break;
}
}
}
注意:switch 配合 String 和枚举使用时,变量不能为 null,原因分析完语法糖转换后的代码应当自然清楚
编译期会将上述代码转换为下面的代码块中的情况
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 比较呢?
例如 BM 和 C. 这两个字符串的 hashCode 值都是 2123
如果有如下代码
public class Candy6_2 {
public static void choose(String str) {
switch (str) {
case "BM": {
System.out.println("h");
break;
}
case "C.": {
System.out.println("w");
break;
}
}
}
}
上面的代码会被编译期转换为下面的情况(伪代码)
public class Candy6_2 {
public Candy6_2() {
}
public static void choose(String str) {
byte x = -1;
switch (str.hashCode()) {
case 2123: // hashCode 值可能相同,需要进一步用 equals 比较
if (str.equals("C.")) {
x = 1;
} else if (str.equals("BM")) {
x = 0;
}
default:
switch (x) {
case 0:
System.out.println("h");
break;
case 1:
System.out.println("w");
}
}
}
}
switch 枚举的例子
enum Sex {
MALE, FEMALE;
}
public class Candy7 {
public static void foo(Sex sex) {
switch (sex) {
case MALE:
System.out.println("男");
break;
case FEMALE:
System.out.println("女");
break;
}
}
}
编译转换后的伪代码
public class Candy7 {
/**
* 定义一个 [合成类](仅 jvm 使用,对我们不可见)
* 用来映射枚举的 ordinal 与数组元素的关系
* 枚举的 ordinal 表示枚举对象的序号,从 0 开始
* 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1
*/
static class $MAP {
// 数组大小即为枚举元素个数,里面存储 case 用来对比的数字
static int[] map = new int[2];
static {
map[Sex.MALE.ordinal()] = 1;
map[Sex.FEMALE.ordinal()] = 2;
}
}
public static void foo(Sex sex) {
int x = $MAP.map[sex.ordinal()];
switch (x) {
case 1:
System.out.println("男");
break;
case 2:
System.out.println("女");
break;
}
}
}
JDK 7 新增了枚举类,以前面的性别枚举为例
enum Sex {
/* 下面一行就是 class 的两个对象,它们和普通对象的区别是:
* * 枚举类的实例是有限的
* * 而不同类的实例对象是无限的(可以通过 new 关键字来不断创建)
*/
MALE, FEMALE; // 两个静态常量
}
编译转换后的伪代码
public final class Sex extends Enum<Sex> { // 此处被 final 修饰,表示枚举类不能被继承
public static final Sex MALE;
public static final Sex FEMALE;
private static final Sex[] $VALUES;
static {
// 调用构造函数,传入枚举元素的值及 ordinal
MALE = new Sex("MALE", 0);
FEMALE = new Sex("FEMALE", 1);
$VALUES = new Sex[]{MALE, FEMALE};
}
/**
* Sole constructor. Programmers cannot invoke this constructor.
* It is for use by code emitted by the compiler in response to
* enum type declarations.
*
* @param name - The name of this enum constant, which is the identifier
* used to declare it.
* @param ordinal - The ordinal of this enumeration constant (its position
* in the enum declaration, where the initial constant is
* assigned
*/
private Sex(String name, int ordinal) {
super(name, ordinal);
}
public static Sex[] values() {
return $VALUES.clone();
}
public static Sex valueOf(String name) {
return Enum.valueOf(Sex.class, name);
}
}
JDK 7 开始新增了对需要关闭的资源处理的特殊语法 try-with-resources
try(资源变量 = 创建资源对象){
//......
} catch( ) {
//......
}
其中资源对象需要实现 AutoCloseable 接口
try-with-resources,都可以不用写 finally 语句块,因为编译器会帮助生成关闭资源代码例如:
public class Candy9 {
public static void main(String[] args) {
try (InputStream is = new FileInputStream("d:\\1.txt")) {
System.out.println(is);
} catch (IOException e) {
e.printStackTrace();
}
}
}
会被转换为(伪代码)
public class Candy9 {
public Candy9() {
}
public static void main(String[] args) {
try {
InputStream is = new FileInputStream("d:\\1.txt");
Throwable t = null;
try {
System.out.println(is);
} catch (Throwable e1) {
// t 是我们代码出现的异常
t = e1;
throw e1;
} finally {
// 判断了资源不为空
if (is != null) {
// 如果我们代码有异常
if (t != null) {
try {
is.close();
} catch (Throwable e2) {
// 如果 close 出现异常,作为被压制异常添加
t.addSuppressed(e2);
}
} else {
// 如果我们代码没有异常,close 出现的异常就是最后 catch 块中的 e
is.close();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
为什么要设计一个 addSuppressed(Throwable e) (添加被压制异常)的方法呢?
是为了防止异常信息的丢失(想想 try-with-resources 生成的 fianlly 中,即关闭资源的时候,抛出了异常)
public class Test6 {
public static void main(String[] args) {
try (MyResource resource = new MyResource()) {
int i = 1 / 0;
} catch (Exception e) {
e.printStackTrace();
}
}
}
class MyResource implements AutoCloseable {
public void close() throws Exception {
throw new Exception("close 异常");
}
}
输出
java.lang.ArithmeticException: / by zero
at test.Test6.main(Test6.java:7)
Suppressed: java.lang.Exception: close 异常
at test.MyResource.close(Test6.java:18)
at test.Test6.main(Test6.java:6)
如以上代码所示,两个异常信息都不会丢。
我们都知道,方法重写时对返回值分两种情况:
class A {
public Number m() {
return 1;
}
}
class B extends A {
@Override
// 子类 m 方法的返回值是 Integer 是父类 m 方法返回值 Number 的子类
public Integer m() {
return 2;
}
}
对于子类,java 编译器会做如下处理(下面代码块其实是便于我们理解的伪代码)
class B extends A {
public Integer m() {
return 2;
}
// 此方法才是真正重写了父类 public Number m() 方法
public synthetic bridge Number m() {
// [synthetic bridge]:java 编译器生成的合成方法,在 JVM 内部使用。对我们是不可见的
// 调用 public Integer m()
return m();
}
}
其中桥接方法比较特殊,仅对 java 虚拟机可见,并且与原来的 public Integer m() 没有命名冲突
可以用下面反射代码来验证:
for (Method m : B.class.getDeclaredMethods()) {
System.out.println(m);
}
输出
public java.lang.Integer test.candy.B.m()
public java.lang.Number test.candy.B.m()
源代码
public class Candy11 {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("ok");
}
};
}
}
转换后的情况
// 额外生成的类
final class Candy11$1 implements Runnable {
Candy11$1() { }
public void run() {
System.out.println("ok");
}
}
public class Candy11 {
public static void main(String[] args) {
//用额外创建的类来创建匿名内部类对象
Runnable runnable = new Candy11$1();
}
}
引用局部变量的匿名内部类,源代码
public class Candy11 {
public static void test(final int x) {
/* 变量是引用类型的时候,final 修饰它
* 指的是引用类型的地址值不能发生改变
* 但地址里面的内容可以发生改变
*/
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("ok:" + x);
}
};
}
}
转换后
// 额外生成的类
final class Candy11$1 implements Runnable {
int val$x;
Candy11$1(int x) {
this.val$x = x;
}
public void run() {
System.out.println("ok:" + this.val$x);
}
}
public class Candy11 {
public static void test(final int x) {
Runnable runnable = new Candy11$1(x);
}
}
Candy11$1 对象时,将 x 的值赋值给了 Candy11$1 对象的 val$x 属性,所以 x 不应该再发生变化了val$x 属性没有机会再跟着一起变化将类的字节码载入方法区中,内部采用 instanceKlass(C++ 的一种数据结构)描述 java 类
_java_mirror 即 java 的类镜像,起到了一个桥梁的作用(C++ 和 Java)
_super 即父类_fields 即成员变量_methods 即方法_constants 即常量池_class_loader 即类加载器_vtable 虚方法表_itable 接口方法表JDK 8 以后,方法区位于元空间中,而元空间又位于本地内存中

类对象和 instanceKlass 之间的关系
_java_mirror 也持有堆中的类对象(如图中的 Person.class) 的指针地址
_java_mirror 就是 java 的类镜像如果以后通过 new 创建了一系列的 实例对象,那么它们之间是如何联系的呢?
注意: instanceKlass 这样的元数据是存储在方法区(1.8 后的元空间内),类对象是存储在堆中的
验证阶段:验证类是否符合 JVM 规范,安全性检查
可以使用用 UE 等支持二进制的编辑器修改 HelloWorld.class 的魔数
修改完成后在控制台运行,发现报错
E:\git\jvm\out\production\jvm>java cn.itcast.jvm.t5.HelloWorld
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.ClassFormatError: Incompatible magic value 3405691578 in class file cn/itcast/jvm/t5/HelloWorld
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)
准备阶段:为 static 变量分配空间,设置默认值
_java_mirror 末尾// 演示 final 对静态变量的影响
public class Load8 {
static int a;
static int b = 10;
static final int c = 20;
static final String d = "hello";
static final Object e = new Object();
}
使用 javac Load8.java 编译上述代码
之后再借助 javap 工具(javap -v -p Load8.class)查看字节码信息
static int a;
descriptor: I
flags: ACC_STATIC
static int b;
descriptor: I
flags: ACC_STATIC
#####################################################################################
static final int c;
descriptor: I
flags: ACC_STATIC, ACC_FINAL
ConstantValue: int 20
#####################################################################################
static final java.lang.String d;
descriptor: Ljava/lang/String;
flags: ACC_STATIC, ACC_FINAL
ConstantValue: String hello
#####################################################################################
static final java.lang.Object e;
descriptor: Ljava/lang/Object;
flags: ACC_STATIC, ACC_FINAL
public zOthers.Load8();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 4: 0
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
#####################################################################################
0: bipush 10
2: putstatic #2 // Field b:I
#####################################################################################
5: new #3 // class java/lang/Object
8: dup
9: invokespecial #1 // Method java/lang/Object."":()V
12: putstatic #4 // Field e:Ljava/lang/Object;
#####################################################################################
15: return
LineNumberTable:
line 6: 0
line 9: 5
解析阶段:将常量池中的符号引用解析为直接引用
- 因为符号引用仅仅就是一个符号引用,JVM 不知道它的具体含义是什么
- 但是经过实际解析后 JVM 就可以知道这个类,方法在内存中实实在在的位置了
- 未解析时:常量池中的看到的对象仅是符号,未真正的存在于内存中
- 解析以后:会将常量池中的符号引用解析为直接引用
package zOthers;
import java.io.IOException;
/**
* 解析的含义
*/
public class Load2 {
public static void main(String[] args) throws ClassNotFoundException, IOException {
ClassLoader classloader = Load2.class.getClassLoader();
// loadClass 方法不会导致类的解析和初始化
Class> c = classloader.loadClass("zOthers.C");
// new C();
System.in.read();
}
}
class C { D d = new D();}
class D { }
启动上述代码块的程序后,使用 HSDB 工具查看地址信息
(懒得贴图演示了,知道那么个意思就行)
初始化即调用 ,虚拟机会保证这个类的『构造方法』的线程安全
clinit() 方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{} 块)中的语句合并产生的所以验证类是否被初始化,可以看该类的静态代码块是否被执行
概括得说,类初始化是懒惰的
main 方法所在的类,总会被首先初始化Class.forNamenew 会导致初始化不会导致类初始化的情况
static final 静态常量(基本类型和字符串)不会触发初始化类对象.class 不会触发初始化loadClass 方法Class.forName 的参数 2 为 false 时package load;
import java.io.IOException;
public class Load3 {
static {
System.out.println("main init");
}
public static void main(String[] args) throws ClassNotFoundException, IOException {
/* 不会触发的情况 */
// 1. 静态常量不会触发初始化
//System.out.println(B.b);
// 2. 类对象.class 不会触发初始化
//System.out.println(B.class);
// 3. 创建该类的数组不会触发初始化
//System.out.println(new B[0]);
// 4. 不会初始化类 B,但会加载 B、A
//ClassLoader cl = Thread.currentThread().getContextClassLoader();
//cl.loadClass("load.B");
// 5. 不会初始化类 B,但会加载 B、A
//ClassLoader c2 = Thread.currentThread().getContextClassLoader();
//Class.forName("load.B", false, c2);
//System.in.read();
/* 会触发初始化的情况 */
// 1. 首次访问这个类的静态变量或静态方法时
//System.out.println(A.a);
// 2. 子类初始化,如果父类还没初始化,会引发
//System.out.println(B.c);
// 3. 子类访问父类静态变量,只触发父类初始化
//System.out.println(B.a);
// 4. 会初始化类 B,并先初始化类 A
//Class.forName("load.B");
}
}
class A {
static int a = 0;
static {
System.out.println("a init");
}
}
class B extends A {
final static double b = 5.0;
static boolean c = false;
static {
System.out.println("b init");
}
}
具体演示还是看视频吧:https://www.bilibili.com/video/BV1yE411Z7AP?p=147
参考书籍:《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明 著
类的初始化阶段是类加载过程的最后一个步骤。
之前介绍的几个类加载的动作里
直到初始化阶段,Java 虚拟机才真正开始执行类中编写的 Java 程序代码,将主导权移交给应用程序。
进行准备阶段时,变量已经赋过一次系统要求的初始零值。
而在初始化阶段,则会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源。
我们也可以从另外一种更直接的形式来表达:初始化阶段就是执行类构造器 方法的过程。
并不是程序员在 Java 代码中直接编写的方法,它是 Javac 编译器的自动生成物。
但我们非常有必要了解这个方法具体是如何产生的,以及 方法执行过程中各种可能会影响程序运行行为的细节。
这部分比起其他类加载过程更贴近于普通的程序开发人员的实际工作。
注意:这里的讨论只限于 Java 语言编译产生的 Class 文件,不包括其他 Java 虚拟机语言。
方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{} 块)中的语句合并产生的。
编译器收集的顺序是由语句在源文件中出现的顺序决定的。
静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。
方法与类的构造函数(即在虚拟机视角中的实例构造器 方法)不同
() 方法执行前,父类的 () 方法已经执行完毕。因此在 Java 虚拟机中第一个被执行的 方法的类型肯定是 java.lang.Object。
由于父类的 方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
方法对于类或接口来说并不是必需的。
如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成 方法。
接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成 方法。
但接口与类不同的是,执行接口的 方法不需要先执行父接口的 方法
() 方法。Java 虚拟机必须保证一个类的 方法在多线程环境中被正确地加锁同步。
如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的 方法,
其他线程都需要阻塞等待,直到活动线程执行完毕 方法。
如果在一个类的 方法中有耗时很长的操作,那就可能造成多个进程阻塞,在实际应用中这种阻塞往往是很隐蔽的。
注意
() 方法的那条线程退出 () 方法后() 方法。问题:从字节码分析,使用 a,b,c 这三个常量是否会导致 E 初始化?
public class Load4 {
public static void main(String[] args) {
System.out.println(E.a);
System.out.println(E.b);
System.out.println(E.c);
}
}
class E {
public static final int a = 10;
public static final String b = "hello";
public static final Integer c = 20; // Integer.valueOf(20)
static {
System.out.println("init E");
}
}
答案
这里我们只需要看 E.class 的字节码信息即可(javap -v E.class)
public static final int a;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 10
public static final java.lang.String b;
descriptor: Ljava/lang/String;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: String hello
public static final java.lang.Integer c;
descriptor: Ljava/lang/Integer;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: bipush 20
2: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
5: putstatic #3 // Field c:Ljava/lang/Integer;
8: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
11: ldc #5 // String init E
13: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
16: return
典型应用:完成懒惰加载初始化单例模式
public class Load9 {
public static void main(String[] args) {
// Singleton.test();
Singleton.getInstance();
}
}
class Singleton {
public static void test() {
System.out.println("test");
}
private Singleton() { }
private static class LazyHolder {
private static final Singleton SINGLETON = new Singleton();
static { System.out.println("lazy holder init"); }
}
public static Singleton getInstance() {
return LazyHolder.SINGLETON;
}
}
以上的实现特点是:
参考博客:https://blog.csdn.net/weixin_53142722/article/details/125423522
- 类加载的特性,就是只有第一次使用这个类才会去加载这个类,触发类的加载链接;
- 只有访问这个内部类才会去加载这个类
- 即你第一次去调用这个 getInstance 方法的时候,才会导致内部类加载和初始化其静态成员(INSTANCE);
- 这种方式是线程安全性的,由类加载器来保证这个单例的线程安全性。
以 JDK 8为例
| 名称 | 加载哪的类 | 说明 |
|---|---|---|
| Bootstrap ClassLoader | JAVA_HOME/jre/lib | 无法直接访问 |
| Extension ClassLoader | JAVA_HOME/jre/lib/ext | 上级为 Bootstrap,显示为 null |
| Application ClassLoader | classpath | 上级为 Extension |
| 自定义类加载器 | 自定义 | 上级为 Application |
参考书籍:《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明 著
Java 虚拟机设计团队有意把类加载阶段中的 “通过一个类的全限定名来获取描述该类的二进制字节流” 这个动作,
放到 Java 虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。
实现这个动作的代码被称为 “类加载器”(Class Loader)。
类加载器虽然只用于实现类的加载动作,但它在 Java 程序中起到的作用却远超类加载阶段。
对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在 Java 虚拟机中的唯一性。
每一个类加载器,都拥有一个独立的类名称空间。
这句话可以表达得更通俗一些:比较两个类是否 “相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义。
否则,即使这两个类来源于同一个 Class 文件,被同一个 Java 虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
这里所指的 “相等”
如果没有注意到类加载器的影响,在某些情况下可能会产生具有迷惑性的结果。
站在 Java 虚拟机的角度来看,只存在两种不同的类加载器:
站在 Java 开发人员的角度来看,类加载器就应当划分得更细致一些。
自 JDK 1.2 以来,Java 一直保持着三层类加载器、双亲委派的类加载架构。
尽管这套架构在 Java 模块化系统出现后有了一些调整变动,但依然未改变其主体结构。
用 Bootstrap 类加载器加载类
public class F {
static {
System.out.println("bootstrap F init");
}
}
执行
public class Load5_1 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.F");
System.out.println(aClass.getClassLoader());
}
}
输出
E:\git\jvm\out\production\jvm>java -Xbootclasspath/a:. cn.itcast.jvm.t3.load.Load5
bootstrap F init
null # null 说明是启动类加载器加载的
-Xbootclasspath 表示设置 bootclasspath/a:. 表示将当前目录追加至 bootclasspath 之后java -Xbootclasspath:java -Xbootclasspath/a:<追加路径>java -Xbootclasspath/p:<追加路径>参考书籍:《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明 著
启动类加载器(Bootstrap Class Loader)
\lib 目录,或者被 -Xbootclasspath 参数所指定的路径中存放的,lib 目录中也不会被加载)public class G {
static {
//System.out.println("ext G init");
System.out.println("classpath G init");
}
}
/**
* 演示 扩展类加载器
* 在 C:\Program Files\Java\jdk1.8.0_91 下有一个 my.jar
* 里面也有一个 G 的类,观察到底是哪个类被加载了
*/
public class Load5_2s {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.G");
System.out.println(aClass.getClassLoader());
}
}
先运行一遍代码,之后重写 G.java
public class G {
static {
System.out.println("ext G init");
//System.out.println("classpath G init");
}
}
输出
classpath G init
sun.misc.Launcher$AppClassLoader@18b4aac2 # 显然,这是应用程序类加载器加载的
再打个 jar 包(jar -cvf my.jar cn\itcast\jvm\t3\load\G.class)
E:\git\jvm\out\production\jvm>jar -cvf my.jar cn\itcast\jvm\t3\load\G.class
已添加清单
正在添加: cn/itcast/jvm/t3/load/G.class(输入 = 481) (输出 = 322)(压缩了 33%)
将 jar 包拷贝到 JAVA_HOME/jre/lib/ext
之后重新执行 Load5_2.java
再输出
ext G init
sun.misc.Launcher$ExtClassLoader@29453f44 # 显然,加载的是扩展类加载器的 G
结论
classpath 和 JAVA_HOME/jre/lib/ext 下有同名类,加载时会使用扩展类加载器加载。参考书籍:《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明 著
扩展类加载器(Extension Class Loader)
\lib\ext 目录中,或者被 java.ext.dirs 系统变量所指定的路径中所有的类库。ext 目录里以扩展 Java SE 的功能。public class G {
static {
//System.out.println("ext G init");
System.out.println("classpath G init");
}
}
public class Load5_2 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.G");
System.out.println(aClass.getClassLoader());
}
}
输出
classpath G init
sun.misc.Launcher$AppClassLoader@18b4aac2 # 显然,这是应用程序类加载器加载的
参考书籍:《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明 著
应用程序类加载器(Application Class Loader)
所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则
注意:这里的双亲,翻译为上级似乎更为合适,因为它们并没有继承关系
参考书籍:《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明 著
- 双亲委派模型的工作过程是
- 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类
- 而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此
- 因此所有的加载请求最终都应该传送到最顶层的启动类加载器中
- 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时
- 子加载器才会尝试自己去完成加载
- 使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处:Java 中的类随着它的类加载器一起具备了一种带有优先级的层次关系。
- 例如类
java.lang.Object,它存放在rt.jar之中。
- 无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载
- 因此
Object类在程序的各种类加载器环境中都能够保证是同一个类。- 反之,如果没有使用双亲委派模型,都由各个类加载器自行去加载的话
- 如果用户自己也编写了一个名为
java.lang.Object的类,并放在程序的ClassPath中- 那系统中就会出现多个不同的
Object类,Java 类型体系中最基础的行为也就无从保证,应用程序将会变得一片混乱。- 双亲委派模型对于保证 Java 程序的稳定运作极为重要,但它的实现却异常简单
- 用以实现双亲委派的代码只有短短十余行,全部集中在
java.lang.ClassLoader的loadClass()方法之中- 该方法的代码的逻辑清晰易懂:
- 先检查请求加载的类型是否已经被加载过
- 若没有则调用父加载器的
loadClass()方法- 若父加载器为空,则默认使用启动类加载器作为父加载器。
- 假如父类加载器加载失败,抛出
ClassNotFoundException异常的话,才调用自己的findClass()方法尝试进行加载。
源码
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 1.检查该类是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 2.有上级的话,则委派上级 loadclass
c = parent.loadClass(name, false);
} else {
// 3.如果没有上级了(ExtClassLoader),则委派 BootstrapClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found from the non-null parent class loader
}
if (c == null) {
long t1 = System.nanoTime();
// 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载
c = findClass(name);
// 5.记录耗时
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
演示
public class Load5_3 {
public static void main(String[] args) throws ClassNotFoundException {
System.out.println(Load5_3.class.getClassLoader());
Class<?> aClass = Load5_3.class.getClassLoader().loadClass("cn.itcast.jvm.t3.load.H");
System.out.println(aClass.getClassLoader());
}
}
通过 Idea 的 Debug 工具来查看执行流程为:
sun.misc.Launcher$AppClassLoader // 1 处, 开始查看已加载的类,结果没有sun.misc.Launcher$AppClassLoader // 2 处,委派上级 sun.misc.Launcher$ExtClassLoader.loadClass()sun.misc.Launcher$ExtClassLoader // 1 处,查看已加载的类,结果没有sun.misc.Launcher$ExtClassLoader // 3 处,没有上级了,则委派 BootstrapClassLoader 查找JAVA_HOME/jre/lib 下找 H 这个类,显然没有sun.misc.Launcher$ExtClassLoader // 4 处,调用自己的 findClass 方法,是在 JAVA_HOME/jre/lib/ext 下找 H 这个类,显然没有。sun.misc.Launcher$AppClassLoader 的 // 2 处sun.misc.Launcher$AppClassLoader // 4 处,调用它自己的 findClass 方法,在 classpath 下查找,找到了我们在使用 JDBC 时,都需要加载 Driver 驱动,不知道你注意到没有,不写 Class.forName("com.mysql.jdbc.Driver")
也是可以让 com.mysql.jdbc.Driver 正确加载的,你知道是怎么做的吗?
追踪一下源码
public class DriverManager {
// 注册驱动的集合
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
// 初始化驱动
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
//... ...
}
不看别的,看看 DriverManager 的类加载器
System.out.println(DriverManager.class.getClassLoader());
打印结果是 null
这表示它的类加载器是 Bootstrap ClassLoader,会到 JAVA_HOME/jre/lib 下搜索类
但 JAVA_HOME/jre/lib 下显然没有 mysql-connector-java-5.1.47.jar 包
这样问题来了,在 DriverManager 的静态代码块中,怎么能正确加载 com.mysql.jdbc.Driver 呢?
继续查看 loadInitilaDrivers 方法
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// 1.使用 ServiceLoader 机制加载驱动,即 SPI */
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
// 2.使用 jdbc.drivers 定义的驱动名加载驱动
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
// 这里的 ClassLoader.getSystemClassLoader() 就是应用程序类加载器
Class.forName(aDriver, true, ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
先看 2 处,发现它最后是使用 Class.forName 完成类的加载和初始化,关联的是应用程序类加载器,因此可以顺利完成类加载
再看 1 处,它就是大名鼎鼎的 Service Provider Interface(SPI)
SPI 约定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名为文件,文件内容是实现类名称

这样就可以使用下面的代码块来得到实现类
ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class);
Iterator<接口类型> iter = allImpls.iterator();
while(iter.hasNext()) { iter.next(); }
这里体现的是 面向接口编程 + 解耦 的思想,许多框架中都用到了该思想
接着看 ServiceLoader.load 方法
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取线程上下文类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器
它内部又是由 Class.forName 调用了线程上下文类加载器完成类加载
具体代码在 ServiceLoader 的内部类 LazyIterator 中
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
参考书籍:《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明 著
线程上下文类加载器(Thread Context ClassLoader)。
有了线程上下文类加载器,程序就可以做一些 “舞弊” 的事情了。
JNDI 服务使用这个线程上下文类加载器去加载所需的 SPI 服务代码
Java 中涉及 SPI 的加载基本上都采用这种方式来完成,例如 JNDI、JDBC、JCE、JAXB 和 JBI 等。
不过,当 SPI 的服务提供者多于一个的时候,代码就只能根据具体提供者的类型来硬编码判断
为了消除这种极不优雅的实现方式,在 JDK 6 时,JDK 提供了 java.util.ServiceLoader 类
META-INF/services 中的配置信息,辅以责任链模式这才算是给 SPI 的加载提供了一种相对合理的解决方案。
什么时候需要自定义类加载器?
步骤:
准备好两个类文件放入 E:\myclasspath,它实现了 java.util.Map 接口,可以先反编译看一下
E:lmyclasspath>javap MapInp11.c1ass
Compi1ed from"MapInp11.java"
public class MapImp11 extends java.uti1.AbstractMap imp1ements java.uti1.Map{
pub1ic MapInp11();
public java.uti1.Set<java.uti1.Map$Entry> entrySet();
pub1ic java.1ang.String toString();
static {};
}
Java 代码
package cn.itcast.jvm.t3.load;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
public class Load7 {
public static void main(String[] args) throws Exception {
MyClassLoader classLoader = new MyClassLoader();
Class<?> c1 = classLoader.loadClass("MapImpl1");
Class<?> c2 = classLoader.loadClass("MapImpl1");
System.out.println(c1 == c2); // true
MyClassLoader classLoader2 = new MyClassLoader();
Class<?> c3 = classLoader2.loadClass("MapImpl1");
/* 比较两个类是否 “相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义。
* 否则,即使这两个类来源于同一个 Class 文件,被同一个 Java 虚拟机加载
* * 只要加载它们的类加载器不同,那这两个类就必定不相等。
* */
System.out.println(c1 == c3); // false
c1.newInstance();
}
}
class MyClassLoader extends ClassLoader {
@Override // name 就是类名称
protected Class<?> findClass(String name) throws ClassNotFoundException {
String path = "e:\\myclasspath\\" + name + ".class";
try {
ByteArrayOutputStream os = new ByteArrayOutputStream();
Files.copy(Paths.get(path), os);
// 得到字节数组
byte[] bytes = os.toByteArray();
// byte[] -> *.class
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException("类文件未找到", e);
}
}
}
打印结果
ture
false
Map impl1 init...
参考博客:JVM学习
怎么做打才能破双亲委派模型?
列举一些你知道的打破双亲委派机制的例子,为什么要打破?
Tomcat 之所以造了一堆自己的 classloader,大致是出于下面三类目的:
Tomcat 类加载器如下图

参考博客:https://blog.csdn.net/weixin_53142722/article/details/125423522
双亲委派模型的第一次 “被破坏” 其实发生在双亲委派模型出现之前(即 JDK1.2 面世以前的 “远古” 时代)
findClass() 方法,在类加载器中的 loadClass() 方法中也会调用该方法双亲委派模型的第二次 “被破坏” 是由这个模型自身的缺陷导致的
双亲委派模型的第三次 “被破坏” 是由于用户对程序动态性的追求而导致的
Hot Swap)、模块热部署(Hot Deployment)等分层编译(TieredCompilation)
先跑一段代码,查看控制台输出情况,发现所耗时间有明显的分层现象
public class JIT1 {
// -XX:+PrintCompilation -XX:-DoEscapeAnalysis
public static void main(String[] args) {
for (int i = 0; i < 200; i++) {
long start = System.nanoTime();
for (int j = 0; j < 1000; j++) {
new Object();
}
long end = System.nanoTime();
System.out.printf("%d\t%d\n",i,(end - start));
}
}
}
JVM 将执行状态分成了 5 个层次
profiling 是指在运行过程中收集一些程序执行状态的数据
即时编译器(JIT)与解释器的区别
对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;
另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。
执行效率上简单比较一下 Interpreter < C1 < C2
C1 可以提升 5 倍左右到效率,C2 可以提高大约 10-100 倍的效率
刚才的一种优化手段称之为 逃逸分析,发现新建的对象是否逃逸。
逃逸分析简单来讲就是
例如上述的情况:
可以使用 -XX:-DoEscapeAnalysis 关闭逃逸分析,再运行刚才的示例观察结果
可以发现后续的速度虽然变快了,但是没有明显的多次分层现象了
参考书籍:《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明 著
逃逸分析(Escape Analysis)是目前 Java 虚拟机中比较前沿的优化技术。
它与类型继承关系分析一样,并不是直接优化代码的手段,而是为其他优化措施提供依据的分析技术。
逃逸分析的基本原理是
如果能证明一个对象不会逃逸到方法或线程之外(换句话说是别的方法或线程无法通过任何途径访问到这个对象),
或者逃逸程度比较低(只逃逸出方法而不会逃逸出线程),则可能为这个对象实例采取不同程度的优化。
如:栈上分配、标量替换、同步消除
如果有需要,或者确认对程序运行有益,
用户也可以使用参数 -XX:+DoEscapeAnalysis 来手动开启逃逸分析,
开启之后可以通过参数 -XX:+PrintEscapeAnalysis 来查看分析结果。
有了逃逸分析支持之后,
用户可以使用参数 -XX:+EliminateAllocations 来开启标量替换,
使用 +XX:+EliminateLocks 来开启同步消除,
使用参数 -XX:+PrintEliminateAllocations 查看标量的替换情况。
方法内联(Inlining)
private static int square(final int i) {
return i * i;
}
System.out.println(square(9));
如果发现 square 是热点方法,并且长度不太长时,会进行内联。
所谓的内联就是把方法内代码拷贝、粘贴到调用者的位置:
System.out.println(9 * 9);
还能够进行常量折叠(constant folding)的优化
System.out.println(81);
实验
public class JIT2 {
// -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining -XX:CompileCommand=dontinline,*JIT2.square
// -XX:+PrintCompilation
public static void main(String[] args) {
int x = 0;
for (int i = 0; i < 500; i++) {
long start = System.nanoTime();
for (int j = 0; j < 1000; j++) {
x = square(9);
}
long end = System.nanoTime();
System.out.printf("%d\t%d\t%d\n",i,x,(end - start));
}
}
private static int square(final int i) {
return i * i;
}
}
JMH 基准测试请参考:http://openjdk.java.net/projects/code-tools/jmh/
创建 maven 工程,添加依赖如下
<dependency>
<groupId>org.openjdk.jmhgroupId>
<artifactId>jmh-coreartifactId>
<version>1.0version>
dependency>
<dependency>
<groupId>org.openjdk.jmhgroupId>
<artifactId>jmh-generator-annprocessartifactId>
<version>1.0version>
<scope>providedscope>
dependency>
编写基准测试代码
package test;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
@Warmup(iterations = 2, time = 1)
@Measurement(iterations = 5, time = 1)
@State(Scope.Benchmark)
public class Benchmark1 {
int[] elements = randomInts(1_000);
private static int[] randomInts(int size) {
Random random = ThreadLocalRandom.current();
int[] values = new int[size];
for (int i = 0; i < size; i++) {
values[i] = random.nextInt();
}
return values;
}
@Benchmark
public void test1() {
for (int i = 0; i < elements.length; i++) {
doSum(elements[i]);
}
}
@Benchmark
public void test2() {
int[] local = this.elements;
for (int i = 0; i < local.length; i++) {
doSum(local[i]);
}
}
@Benchmark
public void test3() {
for (int element : elements) {
doSum(element);
}
}
static int sum = 0;
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
static void doSum(int x) {
sum += x;
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(Benchmark1.class.getSimpleName())
.forks(1)
.build();
new Runner(opt).run();
}
}
具体分析见视频:运行期优化_字段优化_1、运行期优化_字段_2
首先启用 doSum 的方法内联,测试结果如下(每秒吞吐量,分数越高的更好)
Benchmark Mode Samples Score Score error Units
t.Benchmark1.test1 thrpt 5 2420286.539 390747.467 ops/s
t.Benchmark1.test2 thrpt 5 2544313.594 91304.136 ops/s
t.Benchmark1.test3 thrpt 5 2469176.697 450570.647 ops/s
接下来禁用 doSum 方法内联
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
static void doSum(int x) {
sum += x;
}
测试结果如下
Benchmark Mode Samples Score Score error Units
t.Benchmark1.test1 thrpt 5 296141.478 63649.220 ops/s
t.Benchmark1.test2 thrpt 5 371262.351 83890.984 ops/s
t.Benchmark1.test3 thrpt 5 368960.847 60163.391 ops/s
分析
@Benchmark
public void test1() {
// elements.length 首次读取会缓存起来 -> int[] local
for (int i = 0; i < elements.length; i++) { // 后续 999 次 求长度 <- local
sum += elements[i]; // 1000 次取下标 i 的元素 <- local
}
}
可以节省 1999 次 Field 读取操作
但如果 doSum 方法没有内联,则不会进行上面的优化
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class Reflect1 {
public static void foo() {
System.out.println("foo...");
}
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {
Method foo = Reflect1.class.getMethod("foo");
for (int i = 0; i <= 16; i++) {
System.out.printf("%d\t", i);
foo.invoke(null);
}
System.in.read();
}
}
foo.invoke 前面 0 ~ 15 次调用使用的是 MethodAccessor 的 NativeMethodAccessorImpl 实现
package sun.reflect;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import sun.reflect.misc.ReflectUtil;
class NativeMethodAccessorImpl extends MethodAccessorImpl {
private final Method method;
private DelegatingMethodAccessorImpl parent;
private int numInvocations;
NativeMethodAccessorImpl(Method var1) {
this.method = var1;
}
// inflationThreshold 膨胀阈值,默认 15
public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
// 使用 ASM 动态生成的新实现代替本地实现,速度较本地实现快 20 倍左右
MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());
this.parent.setDelegate(var3);
}
// 调用本地实现
return invoke0(this.method, var1, var2);
}
void setParent(DelegatingMethodAccessorImpl var1) {
this.parent = var1;
}
private static native Object invoke0(Method var0, Object var1, Object[] var2);
}
当调用到第 16 次(从 0 开始算)时,会采用运行时生成的类代替掉最初的实现
可以通过 debug 得到类名为 sun.reflect.GeneratedMethodAccessor1
可以使用阿里的 arthas 工具
java -jar arthas-boot.jar
[INFO] arthas-boot version: 3.1.1
[INFO] Found existing java process, please choose one and hit RETURN.
* [1]: 13065 cn.itcast.jvm.t3.reflect.Reflect1
选择 1,回车,表示分析该进程
1
[INFO] arthas home: /root/.arthas/lib/3.1.1/arthas
[INFO] Try to attach process 13065
[INFO] Attach process 13065 success.
[INFO] arthas-client connect 127.0.0.1 3658
, --- . ,------. ,--------.,--. ,--. , --- . ,---.
/ O \ | .--. ''--. .--'| '--' | / O \ ' .-'
| . - . || '--'. ' | | | .--. || .-. | `. `-.
| | | || |\ \ | | | | | || | | | .-' |
`--' `--'` -- ' '--' `--' `--' `--'`--' `--' `-----'
wiki https://alibaba.github.io/arthas
tutorials https://alibaba.github.io/arthas/arthas-tutorials
version 3.1.1
pid 13065
time 2019-06-10 12:23:54
再输入 jad + 类名 来进行反编译
$ jad sun.reflect.GeneratedMethodAccessor1
ClassLoader:
+-sun.reflect.DelegatingClassLoader@15db9742
+-sun.misc.Launcher$AppClassLoader@4e0e2f2a
+-sun.misc.Launcher$ExtClassLoader@2fdb006e
Location:
/*
* Decompiled with CFR 0_132.
*
* Could not load the following classes:
* cn.itcast.jvm.t3.reflect.Reflect1
*/
package sun.reflect;
import cn.itcast.jvm.t3.reflect.Reflect1;
import java.lang.reflect.InvocationTargetException;
import sun.reflect.MethodAccessorImpl;
public class GeneratedMethodAccessor1 extends MethodAccessorImpl {
/*
* Loose catch block
* Enabled aggressive block sorting
* Enabled unnecessary exception pruning
* Enabled aggressive exception aggregation
* Lifted jumps to return sites
*/
public Object invoke(Object object, Object[] arrobject) throws InvocationTargetException {
// 比较奇葩的做法,如果有参数,那么抛非法参数异常
block4 : {
if (arrobject == null || arrobject.length == 0) break block4;
throw new IllegalArgumentException();
}
try {
// 可以看到,已经是直接调用了
Reflect1.foo();
// 因为没有返回值
return null;
}catch (Throwable throwable) {
throw new InvocationTargetException(throwable);
}catch (ClassCastException | NullPointerException runtimeException) {
throw new IllegalArgumentException(Object.super.toString());
}
}
}
Affect(row-cnt:1) cost in 1540 ms.
注意
ReflectionFactory 源码可知 sun.reflect.noInflation
GeneratedMethodAccessor1,但首次生成比较耗时,如果仅反射调用一次,不划算)sun.reflect.inflationThreshold 可以修改膨胀阈值这个学习视频的 运行期优化 的部分讲的是非常的浅显的 (内容太少了)

若是需要更深入了解运行期优化部分的话,还请多多看书:《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》
这里的运行期优化的内容大致对应着书中的第四部分:程序编译与代码优化(主要是书中第 11 章)

两者对比,发现这段教程(p159 ~ p164)的内容确实蛮少的(缺的内容太多了)。所以强烈建议看完教程后,仍然要多多看书