3 类加载与字节码技术
3.1 类文件结构
- 类文件结构
- 字节码指令
- 编译期处理
- 类加载阶段
- 类加载器
- 运行期优化
根据 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; // 访问修饰 public project private
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];
}
3.1.1 魔数
0~3 字节,表示它是否是【class】类型的文件
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
(咖啡宝贝?:)
3.1.2 版本
4~7 字节,表示类的版本 00 34(52) 表示是 Java 8
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
3.1.3 常量池
8-9字节表示常量池的长度(constant_pool_count)
00 23 (35)表示常量池有 #1~#34项,#0项不计入,也没有值;
第一个字节表示常量类型,一共有 17 种类型:每种类型有着完全独立的数据结构
第#1项 0a 表示一个 Method 信息,00 06 和 00 15(21) 表示它引用了常量池中 #6 和 #21 项来获得这个方法的【所属类】和【方法名】
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
第#2项 09 表示一个 Field 信息,00 16(22)和 00 17(23) 表示它引用了常量池中 #22 和 # 23 项来获得这个成员变量的【所属类】和【成员变量名】
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
第#6项 07 表示一个 Class 信息,00 1c(28) 表示它引用了常量池中 #28 项
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
第#7项 01 表示一个 utf8 串,00 06 表示长度,3c 69 6e 69 74 3e 是【<init>
】表示构造方法
0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29
第#8项 01 表示一个 utf8 串,00 03 表示长度,28 29 56 是【()V】其实就是表示无参、无返回值
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
第#21项 0c 表示一个 【名+类型】,00 07 00 08 引用了常量池中 #7 #8 两项
0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e
第#22项 07 表示一个 Class 信息,00 1d(29) 引用了常量池中 #29 项
0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e
第#23项 0c 表示一个 【名+类型】,00 1e(30) 00 1f (31)引用了常量池中 #30 #31 两项
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
第#28项 01 表示一个 utf8 串,00 10(16) 表示长度,是【java/lang/Object】
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
第#29项 01 表示一个 utf8 串,00 10(16) 表示长度,是【java/lang/System】
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
第#30项 01 表示一个 utf8 串,00 03 表示长度,是【out】
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
第#31项 01 表示一个 utf8 串,00 15(21) 表示长度,是【Ljava/io/PrintStream;】
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
3.1.4 访问标识与继承信息
21 表示该 class 是一个类,公共的
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
05 表示根据常量池中 #5 找到本类全限定名
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
06 表示根据常量池中 #6 找到父类全限定名
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
表示接口的数量,本类为 0
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
3.1.5 Field 信息用于表示成员变量
表示成员变量数量,本类为 0
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
字节码中表示类型信息的方法
3.1.6 Method 信息
表示方法数量,本类为 2
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
一个方法由 访问修饰符,名称,参数描述,方法属性数量,方法属性组成
- 红色代表访问修饰符(本类中是 public)
- 蓝色代表引用了常量池 #07 项作为方法名称
- 绿色代表引用了常量池 #08 项作为方法参数描述
- 黄色代表方法属性数量,本方法是 1
- 红色代表方法属性
- 00 09 表示引用了常量池 #09 项,发现是【Code】属性
- 00 00 00 2f 表示此属性的长度是 47
- 00 01 表示【操作数栈】最大深度
- 00 01 表示【局部变量表】最大槽(slot)数
- 00 00 00 05 表示字节码长度,本例是 5
- 2a b7 00 01 b1 是字节码指令
- 00 00 00 02 表示方法细节属性数量,本例是 2
- 00 0a 表示引用了常量池 #10 项,发现是【LineNumberTable】属性
- 00 00 00 06 表示此属性的总长度,本例是 6
- 00 01 表示【LineNumberTable】长度
- 00 00 表示【字节码】行号 00 04 表示【java 源码】行号
- 00 0b 表示引用了常量池 #11 项,发现是【LocalVariableTable】属性
- 00 00 00 0c 表示此属性的总长度,本例是 12
- 00 01 表示【LocalVariableTable】长度
- 00 00 表示局部变量生命周期开始,相对于字节码的偏移量
- 00 05 表示局部变量覆盖的范围长度
- 00 0c 表示局部变量名称,本例引用了常量池 #12 项,是【this】
- 00 0d 表示局部变量的类型,本例引用了常量池 #13 项,是【Lcn/itcast/jvm/t5/HelloWorld;】
- 00 00 表示局部变量占有的槽位(slot)编号,本例是 0
3.1.7 附加属性
00 01 表示附加属性数量
00 13 表示引用了常量池 #19 项,即【SourceFile】
00 00 00 02 表示此属性的长度
00 14 表示引用了常量池 #20 项,即【HelloWorld.java】
0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00
0001120 00 00 02 00 14
参考文献
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html
3.2 字节码指令
3.2.1 入门
public cn.itcast.jvm.t5.HelloWorld(); 构造方法的字节码指令
2a b7 00 01 b1
- 2a => aload_0 加载 slot 0 的局部变量,即 this,做为下面的 invokespecial 构造方法调用的参数
- b7 => invokespecial 预备调用构造方法,哪个方法呢?
- 00 01 引用常量池中 #1 项,即【Method java/lang/Object.“”😦)V 】
- b1 表示返回
另一个是 public static void main(java.lang.String[]); 主方法的字节码指令
b2 00 02 12 03 b6 00 04 b1
- 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 表示返回
3.2.2 javap 工具反编译字节码文件
自己分析类文件结构太麻烦了,Oracle 提供了 javap 工具来反编译 class 文件:
javap -v class文件路径
D:\CodeProject\Java\java_virtual_machine\target\classes\com\rainsun\d3_class_structure> javap -v .\d1_HelloWorld.class
Classfile /D:/CodeProject/Java/java_virtual_machine/target/classes/com/rainsun/d3_class_structure/d1_HelloWorld.class
Last modified 2023年12月25日; size 604 bytes
SHA-256 checksum f4c26de1e0291f2f0984d894624592a6287a89c87805cc57f0b2e658b5a796c7
Compiled from "d1_HelloWorld.java"
public class com.rainsun.d3_class_structure.d1_HelloWorld
minor version: 0
major version: 65
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #21 // com/rainsun/d3_class_structure/d1_HelloWorld
super_class: #2 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Fieldref #8.#9 // java/lang/System.out:Ljava/io/PrintStream;
#8 = Class #10 // java/lang/System
#9 = NameAndType #11:#12 // out:Ljava/io/PrintStream;
#10 = Utf8 java/lang/System
#11 = Utf8 out
#12 = Utf8 Ljava/io/PrintStream;
#13 = String #14 // hello world
#14 = Utf8 hello world
#15 = Methodref #16.#17 // java/io/PrintStream.println:(Ljava/lang/String;)V
#16 = Class #18 // java/io/PrintStream
#17 = NameAndType #19:#20 // println:(Ljava/lang/String;)V
#18 = Utf8 java/io/PrintStream
#19 = Utf8 println
#20 = Utf8 (Ljava/lang/String;)V
#21 = Class #22 // com/rainsun/d3_class_structure/d1_HelloWorld
#22 = Utf8 com/rainsun/d3_class_structure/d1_HelloWorld
#23 = Utf8 Code
#24 = Utf8 LineNumberTable
#25 = Utf8 LocalVariableTable
#26 = Utf8 this
#27 = Utf8 Lcom/rainsun/d3_class_structure/d1_HelloWorld;
#28 = Utf8 main
#29 = Utf8 ([Ljava/lang/String;)V
#30 = Utf8 args
#31 = Utf8 [Ljava/lang/String;
#32 = Utf8 SourceFile
#33 = Utf8 d1_HelloWorld.java
{
public com.rainsun.d3_class_structure.d1_HelloWorld();
descriptor: ()V
LineNumberTable:
line 5: 0
line 6: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "d1_HelloWorld.java"
3.2.3 图解方法执行流程
1)原始的 Java 代码
package com.rainsun.d3_class_structure;
/**
* 演示 字节码指令 和 操作数栈、常量池的关系
*/
public class d2_method_runflow {
public static void main(String[] args) {
int a = 10;
int b = Short.MAX_VALUE + 1;
int c = a + b;
System.out.println(c);
}
}
2)编译后的字节码文件
D:\CodeProject\Java\java_virtual_machine\target\classes\com\rainsun\d3_class_structure> javap -v .\d2_method_runflow.class
Classfile /D:/CodeProject/Java/java_virtual_machine/target/classes/com/rainsun/d3_class_structure/d2_method_runflow.class
Last modified 2023年12月26日; size 675 bytes
SHA-256 checksum 090cee1c8efae1a0d3b54af310b799f33efc90463f218ea25bdb4cea5aef56ef
Compiled from "d2_method_runflow.java"
public class com.rainsun.d3_class_structure.d2_method_runflow
minor version: 0
major version: 65
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #22 // com/rainsun/d3_class_structure/d2_method_runflow
super_class: #2 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Class #8 // java/lang/Short
#8 = Utf8 java/lang/Short
#9 = Integer 32768
#10 = Fieldref #11.#12 // java/lang/System.out:Ljava/io/PrintStream;
#11 = Class #13 // java/lang/System
#12 = NameAndType #14:#15 // out:Ljava/io/PrintStream;
#13 = Utf8 java/lang/System
#14 = Utf8 out
#15 = Utf8 Ljava/io/PrintStream;
#16 = Methodref #17.#18 // java/io/PrintStream.println:(I)V
#17 = Class #19 // java/io/PrintStream
#18 = NameAndType #20:#21 // println:(I)V
#19 = Utf8 java/io/PrintStream
#20 = Utf8 println
#21 = Utf8 (I)V
#22 = Class #23 // com/rainsun/d3_class_structure/d2_method_runflow
#23 = Utf8 com/rainsun/d3_class_structure/d2_method_runflow
#24 = Utf8 Code
#25 = Utf8 LineNumberTable
#26 = Utf8 LocalVariableTable
#27 = Utf8 this
#28 = Utf8 Lcom/rainsun/d3_class_structure/d2_method_runflow;
#29 = Utf8 main
#30 = Utf8 ([Ljava/lang/String;)V
#31 = Utf8 args
#32 = Utf8 [Ljava/lang/String;
#33 = Utf8 a
#34 = Utf8 I
#35 = Utf8 b
#36 = Utf8 c
#37 = Utf8 SourceFile
#38 = Utf8 d2_method_runflow.java
{
public com.rainsun.d3_class_structure.d2_method_runflow();
descriptor: ()V
flags: (0x0001) 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 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/rainsun/d3_class_structure/d2_method_runflow;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: bipush 10
2: istore_1
3: ldc #9 // int 32768
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream;
13: iload_3
14: invokevirtual #16 // Method java/io/PrintStream.println:(I)V
17: return
LineNumberTable:
line 8: 0
line 9: 3
line 10: 6
line 11: 10
line 12: 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: "d2_method_runflow.java"
3)常量池放入运行时常量池
首先加载main方法所在的类,加载类需要将类中的常量池放入加载到运行时常量池
4)方法字节码载入方法区
5)main 线程开始运行,分配栈帧内存
(stack=2,locals=4)
绿色:局部变量表,有 4 个槽
蓝绿色:操作数栈,深度为 2
6)执行引擎开始执行字节码
bipush 10
- 将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有
- sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)
- ldc 将一个 int 压入操作数栈
- ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节)
- 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池
istore_1
将操作数栈顶数据弹出,存入局部变量表的 slot 1
ldc #3
- 从常量池加载 #3 数据到操作数栈
- 注意 Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算好的
istore_2
将操作数栈顶数据弹出,存入局部变量表的 slot 2
iload_1
加载 slot 1 中的数据到操作数栈
iload_2
iadd
执行加法
istore_3
将栈顶的执行结果存入局部变量表的 slot 3
getstatic #10
getstatic获取一个成员变量的引用,将该对象加载到堆中,并将堆中的引用放入操作数栈
iload_3
将操作数变量表中的 slot 3 位置的变量放入操作数栈,传递给out对象
invokevirtual #16
调用println函数
- 找到常量池 #5 项
- 定位到方法区 java/io/PrintStream.println:(I)V 方法
- 生成新的栈帧(分配 locals、stack等)
- 传递参数,执行新栈帧中的字节码
- 执行完毕,弹出栈帧
- 清除 main 操作数栈内容
return
- 完成 main 方法调用,弹出 main 栈帧
- 程序结束
3.2.9 方法调用
public class Demo3_9 {
public Demo3_9() { } // 构造方法
private void test1() { } // 私有方法
private final void test2() { } // final 方法
public void test3() { } // 普通 public 成员方法
public static void test4() { } // 静态方法
public static void main(String[] args) {
Demo3_9 d = new Demo3_9();
d.test1();
d.test2();
d.test3();
d.test4();
Demo3_9.test4();
}
}
字节码:
0: new #2 // class cn/itcast/jvm/t3/bytecode/Demo3_9
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
Demo3_9 d = new Demo3_9();
- new 是创建【对象】,给对象分配堆内存,执行成功会将【对象引用】压入操作数栈
- dup 是赋值操作数栈栈顶的内容,本例即为【对象引用】,为什么需要两份引用呢,一个是要配合 invokespecial 调用该对象的构造方法 “
<init>
”😦)V (会消耗掉栈顶一个引用),另一个要配合 astore_1 赋值给局部变量
test1
test2
最终方法(final),私有方法(private),构造方法都是由 invokespecial 指令来调用,属于静态绑定
test3
:
普通成员方法是由 invokevirtual 调用,属于动态绑定,即支持多态
成员方法与静态方法调用的另一个区别是,执行方法前是否需要【对象引用】
比较有意思的是 d.test4(); 是通过【对象引用】调用一个静态方法,可以看到在调用 invokestatic 之前执行了 pop 指令,把【对象引用】从操作数栈弹掉了
还有一个执行 invokespecial 的情况是通过 super 调用父类方法
3.2.10 多态的实现原理
(HSDB工具的使用)
对象的内存结构是由基础的16字节加上存储属性所花费的字节组成的
16字节中,前8字节为 MarkWord 用于计算类的 hashcode,后 8 字节为对象的Class指针
查看该对象的 class 指针指向的内存地址,可以找到其中关联一个 vtable(虚函数表),里面存储着虚方法。从 Class 的起始地址开始算,偏移 0x1b8 就是 vtable 的起始地址
通过 Tools -> Class Browser 查看每个类的方法定义,比较可知
Dog - public void eat() @0x000000001b7d3fa8
Animal - public java.lang.String toString() @0x000000001b7d35e8;
Object - protected void finalize() @0x000000001b3d1b10;
Object - public boolean equals(java.lang.Object) @0x000000001b3d15e8;
Object - public native int hashCode() @0x000000001b3d1540;
Object - protected native java.lang.Object clone() @0x000000001b3d1678;
- eat() 方法是 Dog 类自己的
- toString() 方法是继承 String 类的
- finalize() ,equals(),hashCode(),clone() 都是继承 Object 类的
当执行 invokevirtual 指令时,
- 先通过栈帧中的对象引用找到对象
- 分析对象头,找到对象的实际 Class
- Class 结构中有 vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了
- 查表得到方法的具体地址
- 执行方法的字节码
3.2.11 异常处理
try catch 原理:
- 可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号
- 8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置
多个 single-catch 块的情况:
因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用
监听[2, 5)之间的字节码,如果出现了异常就转到对应的 target 行进行处理
multi-catch 的情况
catch 多个不同类型的异常,那么 target 跳转的行就会相同
finally原理:
finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程
- 在没有异常会在try后执行finally中的代码
- 捕获了异常的时候就catch执行后执行finally中的代码
- 在出现了异常但是和异常类型不匹配时,也就是没有捕获成功时
- 由于JVM增加了一个异常检测,还会检测 catch 是否出现了异常
- 这时如果 catch中没有捕获异常或者出现了新的异常就会跳转到 finally字节码的地方执行。
finally 的面试题
finally 出现了 return
public class Demo3_12_2 {
public static void main(String[] args) {
int result = test();
System.out.println(result);
}
public static int test() {
try {
return 10;
} finally {
return 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 放入栈顶
5: ireturn // 返回栈顶 int(20)
6: astore_1 // catch any -> slot 1
7: bipush 20 // <- 20 放入栈顶
9: ireturn // 返回栈顶 int(20)
Exception table:
from to target type
0 3 6 any
LineNumberTable: ...
StackMapTable: ...
因为 finally 块中的代码被插入了所有可能的流程(当然包括 try 流程)。所以在try 中代码执行后,执行了 finally 中的代码,由于最后执行的 finally 代码,finally 中的 20 被放入栈顶了,最后 return 就是栈顶的 20。
这也说明了 finally 的代码是在 return 前插入的
- 由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯定以 finally 的为准
- 至于字节码中第 2 行,似乎没啥用,且留个伏笔,看下个例子
- 跟上例中的 finally 相比,发现没有 athrow 了,这告诉我们:
- 如果在 finally 中出现了 return,会吞掉异常😱😱😱,可以试一下下面的代码。1/0不会抛出异常,只返回了一个 20
public class Demo3_12_1 {
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;
}
}
}
finally 对返回值影响
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: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=0
0: bipush 10 // <-10放入栈顶
2: istore_0 // 10-> slot 0
3: iload_0 // <- slot 0 的10加载到栈顶
4: istore_1 // 栈顶10暂存到 solt 1,目的是为了固定返回值
5: bipush 20 // 20 放入栈顶
7: istore_0 // 20 放入solt 0
8: iload_1 // solt 1 里的 10 加载到栈顶
9: ireturn // 返回栈顶的 10
10: astore_2
11: bipush 20
13: istore_0
14: aload_2
15: athrow
Exception table:
from to target type
3 5 10 any
LineNumberTable:
line 9: 0
line 11: 3
line 13: 5
line 11: 8
line 13: 10
line 14: 14
LocalVariableTable:
Start Length Slot Name Signature
3 13 0 i I
StackMapTable: number_of_entries = 1
frame_type = 255 /* full_frame */
offset_delta = 10
locals = [ int ]
stack = [ class java/lang/Throwable ]
}
如果 finally 中没有 return,对返回值的修改是没有影响的,因为在return前会对返回值进行暂存
同时由于 finally 中没有 return 抛出异常的 athrow 也不会被吞掉
synchronized
public class Demo3_13 {
public static void main(String[] args) {
Object lock = new Object();
synchronized (lock) {
System.out.println("ok");
}
}
}
monitorenter 指令对对象进行加锁
monitorexit 指令对对象进行解锁,在正常执行和异常处理的部分都会放置一份,确保即使出现异常也可以解锁成功
3.3 编译期处理
所谓的 语法糖,其实就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成和转换的一些代码
编译器转换的结果直接就是 class 字节码,只是为了便于阅读,这里给出了 几乎等价 的 java 源码方式,
原本的源码->优化->源码优化后生成的字节码->反编译生成优化后的源码
3.3.1 默认构造器
public class Candy1 {
}
//编译成class后的代码:
public class Candy1 {
// 这个无参构造是编译器帮助我们加上的
public Candy1() {
super(); // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object." <init>":()V
}
}
3.3.2 自动拆装箱
从JDK 5开始:
之前版本的代码太麻烦了,需要在基本类型和包装类型之间来回转换(尤其是集合类中操作的都是包装类型),因此这些转换的事情在 JDK 5 以后都由编译器在编译阶段完成。即 代码片段1 都会在编译阶段被转换为 代码片段2
//1:
public class Candy2 {
public static void main(String[] args) {
Integer x = 1;
int y = x;
}
}
// 2:
public class Candy2 {
public static void main(String[] args) {
Integer x = Integer.valueOf(1);
int y = x.intValue;
}
}
3.3.3 泛型集合取值
泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行 泛型擦除 的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理:
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);
}
}
擦除的是字节码上的泛型信息,可以看到 LocalVariableTypeTable 仍然保留了方法参数泛型的信息
使用反射,仍然能够获得这些信息:
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
可变参数,foreach,switch,枚举…
方法重写时的桥接方法
方法重写时对返回值分两种情况:
- 父子类的返回值完全一致
- 子类返回值可以是父类返回值的子类(比较绕口,见下面的例子)
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() {
// 调用 public Integer m()
return m();
}
}
其中桥接方法比较特殊,仅对 java 虚拟机可见,并且与原来的 public Integer 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) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("ok:" + x);
}
};
}
}
转换后代码:
// 额外生成的类
final class Candy11$1 implements Runnable {
int val$x; // 外部的局部变量变成了类的成员变量,无法感知外部的变化,所以引用的外部变量必须是 final 的
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);
}
}
这同时解释了为什么匿名内部类引用局部变量时,局部变量必须是 final 的:因为在创建
Candy11$1
对象时,将 x 的值赋值给了 Candy11$1
对象的 val$x
属性,所以 x 不应该再发生变化了,如果变化,那么 val$x
属性没有机会再跟着一起变化
3.4 类加载阶段
3.4.1 加载
-
通过类加载器将类的字节码载入方法区,内部采用 C++的 instanceKlass 描述 java 类,它的重要的 field(域)有:
- _java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴露给 java 使用
- _super 即父类
- _fields 即成员变量
- _methods 即方法
- _constants 即常量池
- _class_loader 即哪一个 类加载器 加载的它
- _vtable 虚方法表
- _itable 接口方法表
-
如果这个类还有父类没有加载,则加载父类
-
加载和连接可能是交替运行的
注意
- instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror是存储在堆中
- 可以通过前面介绍的 HSDB 工具查看
3.4.2 链接
验证
验证字节码是否符合JVM的规范,进行安全性检查
准备
为 static 变量分配空间,设置默认值
- static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾
- static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
- 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
- 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成
解析
将常量池中的符号引用解析为直接引用
没解析之前仅仅是一组符号,而解析后,这些符号被替换成了具体的地址
3.4.3 初始化
初始化即调用 <cinit>()V
,虚拟机会保证这个类的『构造方法』的线程安全
<clinit>()
并不是程序员在Java代码中直接编写的方法,它是Javac编译器的自动生成物;
<clinit>()
方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问
<clinit>()
方法与类的构造函数(即在虚拟机视角中的实例构造器<init>()
方法)不同,它不需要显式地调用父类构造器,Java虚拟机会保证在子类的<clinit>()
方法执行前,父类的<clinit>()
方法已经执行完毕。因此在Java虚拟机中第一个被执行的<clinit>()
方法的类型肯定是java.lang.Object
初始化发生的时机:
概括得说,类初始化是【懒惰的】
- main 方法所在的类,总会被首先初始化
- 首次访问这个类的静态变量或静态方法时
- 子类初始化,如果父类还没初始化,会引发
- 子类访问父类的静态变量,只会触发父类的初始化
- Class.forName
- new 会导致初始化
不会导致类初始化的情况
- 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化,赋值在准备阶段就已经完成了。
- 访问类的 static final 的 包装类型的静态常量会触发类的初始化
- 类对象.class 不会触发初始化,在加载阶段已经完成
- 创建该类的数组不会触发初始化
- 类加载器的 loadClass 方法
- Class.forName 的参数 2 为 false 时
懒惰初始化单例模式
利用类的类加载机制,JVM保证了类加载过程是线程互斥的;
通过静态内部类实现的多线程环境中的单例模式:
package com.rainsun.d3_class_structure;
public class d4_lazySingleton {
public static void main(String[] args) {
Singleton.test(); // 仅仅输出 test
// Singleton.getInstance();
}
}
class Singleton{
private Singleton(){}
public static void test(){
System.out.println("test");
}
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;
}
}
保证了实例只会被创建一次,而且也只会在第一次调用的时候使用互斥机制,避免了每次加锁的低效问题。
- 调用 getInstance 时候才会去初始化 LazyHolder 这个类,且只会初始化一次,并保证了线程安全,保证了实例只被创建一次
- 由于类初始化是懒惰的,只有在用到这个类的静态变量或者方法的时候才会初始化。避免了加载阶段就创建了实例
3.5 类加载器
JVM有不同层级的类加载器,加载不同类型的类,JDK 8 为例:
启动类加载器->扩展类加载器->应用程序加载器
如果上级类加载器都没有加载,下级才可以加载
3.5.1 启动类加载器
加载JAVA_HOME/jre/lib 目录下的类
可以使用 java -Xbootclasspath:<new bootclasspath>
追加路径,交给启动类加载器加载
- -Xbootclasspath 表示设置 bootclasspath
- 其中 /a:. 表示将当前目录追加至 bootclasspath 之后
- 可以用这个办法替换核心类
- java -Xbootclasspath:
<new bootclasspath>
- java -Xbootclasspath/a:<追加路径>
- java -Xbootclasspath/p:<追加路径>
- java -Xbootclasspath:
3.5.2 扩展类加载器
加载 JAVA_HOME/jre/ext 目录下的类
它的类加载器是 sun.misc.Launcher$ExtClassLoader
3.5.3 双亲委派模式
所谓双亲委派就是指调用类加载器的loadClass方法时,查找类的规则:首先委派上级优先进行类的加载,上级没有这个类才有本级进行加载
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 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) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 4. 如果上一层找不到,则调用 findClass方法在本层寻找
c = findClass(name);
// this is the defining class loader; record the stats
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
public class d5_classLoad {
public static void main(String[] args) throws ClassNotFoundException {
System.out.println(d5_classLoad.class.getClassLoader());
Class<?> aClass = d5_classLoad.class.getClassLoader().loadClass("com.rainsun.d3_class_structure.H");
System.out.println(aClass.getClassLoader());
}
}
执行流程为:
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
查找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 下查找,找到了
3.5.4 线程上下文类加载器
线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContext-ClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
3.5.5 自定义类加载器
什么时候需要自定义类加载器
1)想加载非 classpath 随意路径中的类文件
2)都是通过接口来使用实现,希望解耦时,常用在框架设计
3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器
步骤:
- 继承 ClassLoader 父类
- 要遵从双亲委派机制,重写 findClass 方法
注意不是重写 loadClass 方法,否则不会走双亲委派机制 - 读取类文件的字节码
- 调用父类的 defineClass 方法来加载类
- 使用者调用该类加载器的 loadClass 方法
package com.rainsun.d3_class_structure;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class d6_classLoader {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
MyClassLoader classLoader = new MyClassLoader();
Class<?> aClass = classLoader.loadClass("H");
Class<?> aClass1 = classLoader.loadClass("H");
System.out.println(aClass1 == aClass1); // true
aClass1.newInstance(); // 初始化了类,执行了static 代码块的语句:H init
}
}
class MyClassLoader extends ClassLoader{
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String path = "D:\\CodeProject\\Java\\"+ name + ".class";
try {
ByteArrayOutputStream os = new ByteArrayOutputStream();
Files.copy(Paths.get(path), os);
// 得到字节数组
byte[] bytes = os.toByteArray();
// 字节数据 -> *.class
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException("类文件未找到", e);
}
}
}