目录
[2.3 图解方法执行流程](#2.3 图解方法执行流程)
[1)原始 java 代码](#1)原始 java 代码)
[5)main 线程开始运行,分配栈帧内存](#5)main 线程开始运行,分配栈帧内存)
[2.4 练习 - 分析 i++](#2.4 练习 - 分析 i++)
[2.5 条件判断指令](#2.5 条件判断指令)
[2.6 循环控制指令](#2.6 循环控制指令)
[2.7 练习 - 判断结果](#2.7 练习 - 判断结果)
[2.8 构造方法★★★](#2.8 构造方法★★★)
[1) ()V 类构造方法](#1) ()V 类构造方法)
[2) ()V 实例构造方法](#2) ()V 实例构造方法)
[2.9 方法调用](#2.9 方法调用)
[2.10 多态的原理](#2.10 多态的原理)
[2.11 异常处理](#2.11 异常处理)
[3)multi-catch 的情况](#3)multi-catch 的情况)
[5)finally 面试题一](#5)finally 面试题一)
[5)finally 面试题二](#5)finally 面试题二)
[2.12 synchronized](#2.12 synchronized)
[1. 自动生成无参构造方法](#1. 自动生成无参构造方法)
[2. 自动拆装箱的转换](#2. 自动拆装箱的转换)
[3. 泛型集合取值](#3. 泛型集合取值)
[4. 可变参数](#4. 可变参数)
[5. foreach 循环](#5. foreach 循环)
[6. switch 字符串](#6. switch 字符串)
[7. switch 枚举](#7. switch 枚举)
[8. 枚举](#8. 枚举)
[9. try-with-resources](#9. try-with-resources)
[10. 方法重写时的桥接方法](#10. 方法重写时的桥接方法)
[11. 匿名内部类](#11. 匿名内部类)
[4.1 加载](#4.1 加载)
[4.2 链接](#4.2 链接)
[4.3 初始化](#4.3 初始化)
[练习二:典型应用 - 完成懒惰初始化单例模式](#练习二:典型应用 - 完成懒惰初始化单例模式)
[5.1 双亲委派机制](#5.1 双亲委派机制)
[5.2 线程上下文类加载器](#5.2 线程上下文类加载器)
[5.3 自定义类加载器](#5.3 自定义类加载器)
[6.1 即时编译](#6.1 即时编译)
[6.2 反射优化](#6.2 反射优化)
这篇文章很不错,尤其是讲到类加载阶段那一块的时候:(没有本人这篇博文好,哈哈)
一、类文件结构
根据 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];
}
二、字节码指令
反编译命令:javap -v Xxx.class
2.3 图解方法执行流程
1)原始 java 代码
java
package org.wuya.test;
/**
* 演示 字节码指令 和 操作数栈、常量池的关系
*/
public class Demo3_1 {
public static void main(String[] args) {
int a = 10;
int b = Short.MAX_VALUE + 1;
int c = a + b;
System.out.println(c);
}
}
2)编译后的字节码文件
Classfile /D:/JavaTools/springbootRedisDemo/springbootRedisDemo/target/classes/org/wuya/test/Demo3_1.class
Last modified 2024-4-21; size 641 bytes
MD5 checksum 87a2a6a3e3f7d22289041a5d50f4c0dd
Compiled from "Demo3_1.java"
public class org.wuya.test.Demo3_1
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#26 // java/lang/Object."<init>":()V
#2 = Class #27 // java/lang/Short
#3 = Integer 32768
#4 = Fieldref #28.#29 // java/lang/System.out:Ljava/io/PrintStream;
#5 = Methodref #30.#31 // java/io/PrintStream.println:(I)V
#6 = Class #32 // org/wuya/test/Demo3_1
#7 = Class #33 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 Lorg/wuya/test/Demo3_1;
#15 = Utf8 main
#16 = Utf8 ([Ljava/lang/String;)V
#17 = Utf8 args
#18 = Utf8 [Ljava/lang/String;
#19 = Utf8 a
#20 = Utf8 I
#21 = Utf8 b
#22 = Utf8 c
#23 = Utf8 MethodParameters
#24 = Utf8 SourceFile
#25 = Utf8 Demo3_1.java
#26 = NameAndType #8:#9 // "<init>":()V
#27 = Utf8 java/lang/Short
#28 = Class #34 // java/lang/System
#29 = NameAndType #35:#36 // out:Ljava/io/PrintStream;
#30 = Class #37 // java/io/PrintStream
#31 = NameAndType #38:#39 // println:(I)V
#32 = Utf8 org/wuya/test/Demo3_1
#33 = Utf8 java/lang/Object
#34 = Utf8 java/lang/System
#35 = Utf8 out
#36 = Utf8 Ljava/io/PrintStream;
#37 = Utf8 java/io/PrintStream
#38 = Utf8 println
#39 = Utf8 (I)V
{
public org.wuya.test.Demo3_1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lorg/wuya/test/Demo3_1;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: bipush 10
2: istore_1
3: ldc #3 // int 32768
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
13: iload_3
14: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
17: return
LineNumberTable:
line 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
MethodParameters:
Name Flags
args
}
SourceFile: "Demo3_1.java"
3)常量池载入运行时常量池
4)方法字节码载入方法区
5)main 线程开始运行,分配栈帧内存
(stack=2,locals=4)
6)执行引擎开始执行字节码★★★
bipush 10
istore_1
ldc #3 // int 32768
istore_2
iload_1
iload_2
iadd
istore_3
getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
iload_3
invokevirtual #5 // Method java/io/PrintStream.println:(I)V
return
说明:具体每一步的图片看讲义。非常明晰。
bipush 10
将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有
- sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)
- ldc 将一个 int 压入操作数栈
- ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节)
这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池。
- bipush 10 将一个 byte 压入操作数栈(其长度会补齐 4 个字节)
- istore_1 将操作数栈顶数据弹出,存入局部变量表的 slot 1,结果就是a被赋值为10 【可见,bipush 10和istore_1就对应java源代码中的int a = 10;操作】
(接下来就是对b变量赋值:由于b变量对应的值是32768,已经超过了short的范围了,所以它存储的位置是运行时常量池中)
- ldc 从常量池加载#3数据到操作数栈(注意 Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算好的)
- istore_2 将操作数栈顶数据弹出,存入局部变量表的slot 2,结果就是b被赋值为32767
(接下来该执行a+b赋值给c了。局部变量表中不能执行a+b这个操作,它一定是在操作数栈中完成,所以执行引擎要对a、b这两个变量进行读取)
- iload_1 把局部变量1槽位的值读入操作数栈上
- iload_2 把局部变量2槽位的值读入操作数栈上
- iadd 相加,把结果存入操作数栈
- istore_3 将操作数栈顶数据弹出,存入局部变量表的slot 3
- getstatic 从常量池#4找成员变量(System类的out成员变量)的引用,把堆中System.out 对象的引用值放入操作数栈
(接下来该进行打印,打印的话,需要一些参数)
- iload_3 把局部变量2槽位的值32768读入操作数栈上
- invokevirtual 分析见下面
- return 完成main方法调用,弹出main栈帧,程序结束
invokevirtual #5
- 找到常量池 #5 项
- 定位到方法区 java/io/PrintStream.println:(I)V 方法
- 生成新的栈帧(分配 locals、stack等)
- 传递参数,执行新栈帧中的字节码
- 执行完毕,弹出栈帧
- 清除 main 操作数栈内容
2.4 练习 - 分析 i++
1)源码如下:
java
package org.wuya.test;
/**
* 从字节码角度分析 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);
}
}
2)编译后的字节码文件:
重点看下面这段
{
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: bipush 10
2: istore_1
3: iload_1
4: iinc 1, 1 表示对1号槽位自增,自增1
7: iinc 1, 1
10: iload_1
11: iadd 表示将操作数栈中的两个数(10+12)进行了相加,把结果22保存在了操作数栈
12: iload_1 执行a--操作,将局部变量表中slot 1的值12先读入操作数栈,再自减
13: iinc 1, -1
16: iadd 将22+12的结果34保存在操作数栈
17: istore_2 将操作数栈顶数据弹出,存入局部变量表的slot 2,即将34赋值给了b
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
MethodParameters:
Name Flags
args
}
分析:
- 注意 iinc 指令是直接在局部变量 slot 上进行运算
- a++ 和 ++a 的区别是先执行 iload 还是 先执行 iinc
a++ 是先iload再自增(iinc),++a 是先自增(iinc)再iload。
具体分析见上面。图解见讲义。
2.5 条件判断指令
讲义有一张指令代表的含义,略。
几点说明:
- byte,short,char 都会按 int 比较,因为操作数栈都是 4 字节
- goto 用来进行跳转到指定行号的字节码
- 比较小的整数(从-1到5)是用iconst来表示的。
源码:
java
package org.wuya.test;
public class Demo3_3 {
public static void main(String[] args) {
int a = 0;
if(a == 0) {
a = 10;
} else {
a = 20;
}
}
}
关键的字节码:
0: iconst_0
1: istore_1
2: iload_1
3: ifne 12 ifne是判断是否 != 0,如果不等于0的话就跳到12行,否则顺序执行
6: bipush 10
8: istore_1
9: goto 15 goto表示跳到15行执行
12: bipush 20
14: istore_1
15: return
2.6 循环控制指令
其实循环控制还是前面介绍的那些指令,例如 while 循环,do while 循环,字节码略。
java
public class Demo3_4 {
public static void main(String[] args) {
int a = 0;
while (a < 10) {
a++;
}
}
}
public class Demo3_5 {
public static void main(String[] args) {
int a = 0;
do {
a++;
} while (a < 10);
}
}
再看看 for 循环,比较 while 和 for 的字节码,你发现它们是一模一样的。
2.7 练习 - 判断结果
请从字节码角度分析,下列代码运行的结果:
java
package org.wuya.test;
public class Demo3_6_1 {
public static void main(String[] args) {
int i = 0;
int x = 0;
while (i < 10) {
x = x++;
i++;
}
System.out.println(x);// 结果是 0
}
}
关键字节码:
0: iconst_0
1: istore_1
2: iconst_0
3: istore_2
4: iload_1
5: bipush 10
7: if_icmpge 21
10: iload_2
11: iinc 2, 1
14: istore_2 关键是这个赋值操作,将操作数栈中的0又赋值给了x
15: iinc 1, 1
18: goto 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
2.8 构造方法★★★
1) <cinit>()V 类构造方法
源码:
java
package org.wuya.test;
public class Demo3_8_1 {
static {
i = 20;
}
static {
i = 30;
}
static int i = 10;
}
反编译后的字节码:
Classfile /D:/JavaTools/.../src/main/java/org/wuya/test/Demo3_8_1.class
Last modified 2024-4-21; size 305 bytes
MD5 checksum 88505d650a05ea6c2869fd5eae46f394
Compiled from "Demo3_8_1.java"
public class org.wuya.test.Demo3_8_1
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#14 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#15 // org/wuya/test/Demo3_8_1.i:I
#3 = Class #16 // org/wuya/test/Demo3_8_1
#4 = Class #17 // java/lang/Object
#5 = Utf8 i
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 <clinit>
#12 = Utf8 SourceFile
#13 = Utf8 Demo3_8_1.java
#14 = NameAndType #7:#8 // "<init>":()V
#15 = NameAndType #5:#6 // i:I
#16 = Utf8 org/wuya/test/Demo3_8_1
#17 = Utf8 java/lang/Object
{
static int i;
2: putstatic #2 // Field i:I
5: bipush 30
7: putstatic #2 // Field i:I
10: bipush 10
12: putstatic #2 // Field i:I
15: return
LineNumberTable:
line 6: 0
line 11: 5
line 14: 10
}
SourceFile: "Demo3_8_1.java"
编译器会按(源码中)从上至下的顺序,收集所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方法 <cinit>()V :
static int i;
2: putstatic #2 // Field i:I
5: bipush 30
7: putstatic #2 // Field i:I
10: bipush 10
12: putstatic #2 // Field i:I
15: return
<cinit>()V 方法会在类加载的初始化阶段被调用。
2) <init>()V 实例构造方法
源码:
java
package org.wuya.test;
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);
System.out.println(d.b);
}
}
下面是反编译后的字节码:
java
PS D:\JavaTools\...\target\classes\org\wuya\test> javap -v .\Demo3_8_2.class
Classfile /D:/JavaTools/.../target/classes/org/wuya/test/Demo3_8_2.class
Last modified 2024-4-21; size 859 bytes
MD5 checksum 987873efec13bf5b024a1af1a2d71201
Compiled from "Demo3_8_2.java"
public class org.wuya.test.Demo3_8_2
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #12.#32 // java/lang/Object."<init>":()V
#2 = String #33 // s1
#3 = Fieldref #6.#34 // org/wuya/test/Demo3_8_2.a:Ljava/lang/String;
#4 = Fieldref #6.#35 // org/wuya/test/Demo3_8_2.b:I
#5 = String #36 // s2
#6 = Class #37 // org/wuya/test/Demo3_8_2
#7 = String #38 // s3
#8 = Methodref #6.#39 // org/wuya/test/Demo3_8_2."<init>":(Ljava/lang/String;I)V
#9 = Fieldref #40.#41 // java/lang/System.out:Ljava/io/PrintStream;
#10 = Methodref #42.#43 // java/io/PrintStream.println:(Ljava/lang/String;)V
#11 = Methodref #42.#44 // java/io/PrintStream.println:(I)V
#12 = Class #45 // java/lang/Object
#13 = Utf8 a
#14 = Utf8 Ljava/lang/String;
#15 = Utf8 b
#16 = Utf8 I
#17 = Utf8 <init>
#18 = Utf8 (Ljava/lang/String;I)V
#19 = Utf8 Code
#20 = Utf8 LineNumberTable
#21 = Utf8 LocalVariableTable
#22 = Utf8 this
#23 = Utf8 Lorg/wuya/test/Demo3_8_2;
#24 = Utf8 MethodParameters
#25 = Utf8 main
#26 = Utf8 ([Ljava/lang/String;)V
#27 = Utf8 args
#28 = Utf8 [Ljava/lang/String;
#29 = Utf8 d
#30 = Utf8 SourceFile
#31 = Utf8 Demo3_8_2.java
#32 = NameAndType #17:#46 // "<init>":()V
#33 = Utf8 s1
#34 = NameAndType #13:#14 // a:Ljava/lang/String;
#35 = NameAndType #15:#16 // b:I
#36 = Utf8 s2
#37 = Utf8 org/wuya/test/Demo3_8_2
#38 = Utf8 s3
#39 = NameAndType #17:#18 // "<init>":(Ljava/lang/String;I)V
#40 = Class #47 // java/lang/System
#41 = NameAndType #48:#49 // out:Ljava/io/PrintStream;
#42 = Class #50 // java/io/PrintStream
#43 = NameAndType #51:#52 // println:(Ljava/lang/String;)V
#44 = NameAndType #51:#53 // println:(I)V
#45 = Utf8 java/lang/Object
#46 = Utf8 ()V
#47 = Utf8 java/lang/System
#48 = Utf8 out
#49 = Utf8 Ljava/io/PrintStream;
#50 = Utf8 java/io/PrintStream
#51 = Utf8 println
#52 = Utf8 (Ljava/lang/String;)V
#53 = Utf8 (I)V
{
public org.wuya.test.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 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2 // String s1
7: putfield #3 // Field a:Ljava/lang/String;
10: aload_0
11: bipush 20
13: putfield #4 // Field b:I
16: aload_0
17: bipush 10
19: putfield #4 // Field b:I
22: aload_0
23: ldc #5 // String s2
25: putfield #3 // Field a:Ljava/lang/String;
28: aload_0
29: aload_1
30: putfield #3 // Field a:Ljava/lang/String;
33: aload_0
34: iload_2
35: putfield #4 // Field b:I
38: return
LineNumberTable:
line 18: 0
line 6: 4
line 9: 10
line 12: 16
line 15: 22
line 19: 28
line 20: 33
line 21: 38
LocalVariableTable:
Start Length Slot Name Signature
0 39 0 this Lorg/wuya/test/Demo3_8_2;
0 39 1 a Ljava/lang/String;
0 39 2 b I
MethodParameters:
Name Flags
a
b
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=2, args_size=1
0: new #6 // class org/wuya/test/Demo3_8_2
3: dup
4: ldc #7 // String s3
6: bipush 30
8: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
11: astore_1
12: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
15: aload_1
16: getfield #3 // Field a:Ljava/lang/String;
19: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
22: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
25: aload_1
26: getfield #4 // Field b:I
29: invokevirtual #11 // Method java/io/PrintStream.println:(I)V
32: return
LineNumberTable:
line 24: 0
line 25: 12
line 26: 22
line 27: 32
LocalVariableTable:
Start Length Slot Name Signature
0 33 0 args [Ljava/lang/String;
12 21 1 d Lorg/wuya/test/Demo3_8_2;
MethodParameters:
Name Flags
args
}
SourceFile: "Demo3_8_2.java"
编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在最后。
【从字节码可以看出来,新的构造方法是Demo3_8_2(java.lang.String, int)】
在这个构造方法中,aload_0 表示把this加载到操作数栈;
2.9 方法调用
看一下几种不同的方法调用对应的字节码指令
1)如果源代码是这样的:
java
package org.wuya.test;
public class Demo3_9 {
public Demo3_9() { }
private void test1() { }
private final void test2() { }
public void test3() { }
public static void test4() { }
}
则javac命令编译后,再执行javap反编译后,方法表集合的字节码是这样的:
java
{
public org.wuya.test.Demo3_9();
descriptor: ()V
flags: ACC_PUBLIC
public static void test4();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=0, args_size=0
0: return
LineNumberTable:
line 12: 0
}
可以看到在类加载阶段,只加载了构造方法和static方法。
2)如果源代码是这样的:
java
package org.wuya.test;
public class Demo3_9 {
public Demo3_9() { } //构造方法
private void test1() { } //私有方法
private final void test2() { } //final方法
public void test3() { } //public方法
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();
}
}
则javac命令编译后,再执行javap反编译后,方法表集合的字节码是这样的:
java
{
public org.wuya.test.Demo3_9();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 4: 0
public void test3();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 10: 0
public static void test4();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=0, args_size=0
0: return
LineNumberTable:
line 12: 0
public java.lang.String toString();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #2 // Method java/lang/Object.toString:()Ljava/lang/String;
4: areturn
LineNumberTable:
line 16: 0
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 org/wuya/test/Demo3_9
3: dup
4: invokespecial #4 // Method "<init>":()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
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
LineNumberTable:
line 20: 0
line 21: 8
line 22: 12
line 23: 16
line 24: 20
line 25: 25
line 26: 28
line 27: 33
}
final的方法,不管修饰符是private,还是public,还是什么,对应的字节码指令是一样的。
可以看到:
- 构造方法、final方法、private方法都是调用的invokespecial;
- 普通public方法调用的是invokevirtual;
- 静态方法调用的是invokestatic;
其中,invokestatic和invokespecial都属于静态绑定,也就是在.class字节码指令生成的时候,就知道如何找到哪个类的哪个方法了。构造方法、私有方法、静态方法都是能唯一确定的;只有普通的public方法在编译期间并不能确定是调用哪个类的哪个方法,也许是子类的也许是父类的,所以invokevirtual叫动态绑定,在运行的时候确定。
- new 是创建【对象】,给对象分配堆内存,执行成功会将【对象引用】压入操作数栈;
- dup 是复制操作数栈栈顶的内容,本例即为【对象引用】,为什么需要两份引用呢,一个是要配合 invokespecial 调用该对象的构造方法 "<init>":()V (**会消耗掉栈顶一个引用,就是说调用完构造方法后,复制的这个引用就清除了。**延伸:这里就是我们所说的new对象的时候会调用构造方法),另一个要配合 astore_1 赋值给局部变量;(注意:赋值完成后,操作数栈就没有数据了哦)
- 最终方法(final),私有方法(private),构造方法都是由 invokespecial 指令来调用,属于静态绑定;
- 普通成员方法是由 invokevirtual 调用,属于动态绑定,即支持多态;
- 成员方法与静态方法调用的另一个区别是,执行方法前是否需要【对象引用】;
- 比较有意思的是 d.test4(); 是通过【对象引用】调用一个静态方法,可以看到在调用invokestatic 之前执行了 pop 指令,把【对象引用】从操作数栈弹掉了;(因为调用静态方法不需要对象调用)
- 还有一个执行 invokespecial 的情况是通过 super 调用父类方法 。
2.10****多态的原理
前面介绍了虚拟机指令中的方法调用的多种方式,其中有一种方式是invokevirtual ,它是实现一个方法的多态调用,它的工作方法更为复杂,这里研究invokevirtual指令的执行流程,也就是多态的原理。
java
package org.wuya.test;
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("吃鱼");
}
}
1)运行代码
停在System.in.read()方法上,这时运行jps获取进程id
2)运行 HSDB 工具
进入JDK安装目录,执行相关命令。
3)查找某个对象
4)查看对象内存结构
点击超链接可以看到对象的内存结构,此对象没有任何属性,因此只有对象头的16字节,前8字节是MarkWord,后8字节就是对象的Class指针,但目前看不到它的实际地址。
5)查看对象Class的内存地址
6)查看类的vtable
找到Dog Class的vtable长度为6,意思就是Dog类有6个虚方法(多态相关的,final,static不会列入)。 那么这 6 个方法都是谁呢?
7 )验证方法地址
通过 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() 方法是继承Animal 类的
- finalize() ,equals(),hashCode(),clone() 都是继承 Object 类的
多态原理小结★★★
当执行 invokevirtual 指令时,
- 先通过栈帧中的对象引用找到对象
- 分析对象头,找到对象的实际 Class
- Class 结构中有 vtable ,它在类加载的链接阶段就已经根据方法的重写规则生成好了
- 查表得到方法的具体地址
- 执行方法的字节码
2.11 异常处理
1) try-catch
源码:
java
package org.wuya.test;
public class Demo3_11_1 {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
}
}
}
运行上面的代码,生成.class文件,再执行反编译命令后的关键的字节码摘录:
(不运行代码直接javac编译,与运行代码编译,反编译后的字节码格式上有一点差异)
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 这两行执行try中的代码块,如果发生异常,怎么进入catch的呢?
5: goto 12
8: astore_2 把那些异常对象的引用地址存入到局部变量表2号槽位上,槽位名字是e
9: bipush 20
11: istore_1
12: return
Exception table: Exception table对上面[2,5)行代码进行监控,一旦有异常发生,会进行匹配,如果与声明的java/lang/Exception类型一致或者是它的子类,则进入到target指示的第8行执行。
from to target type
2 5 8 Class java/lang/ExceptionLineNumberTable:
line 6: 0
line 8: 2
line 11: 5
line 9: 8
line 10: 9
line 12: 12
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: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 8
locals = [ class "[Ljava/lang/String;", int ]
stack = [ class java/lang/Exception ]
frame_type = 3 /* same */
MethodParameters:
Name Flags
args
2)多个single-catch块的情况
3)multi-catch****的情况
这是JDK1.7后的新语法
源码:
java
package org.wuya.test;
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");
}
}
4)finally
java
package org.wuya.test;
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;
}
}
}
分析字节码知道:finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程。
5)finally****面试题一
finally 出现了 return
问:下面程序输出结果是什么?
- 由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯定以 finally 的为准
- 至于字节码中第 2 行,似乎没啥用,且留个伏笔,看下个例子
- 跟上例中的 finally 相比,发现没有 athrow 了,这告诉我们:如果在 finally 中出现了 return,会吞掉异常,可以试一下下面的代码
finally 中出现了 return,会吞掉异常,所以日常编码一定不要在finally中写return代码!
java
package org.wuya.test;
public class Demo3_12_1 {
public static void main(String[] args) {
int result = test();
System.out.println(result);
}
public static int test() {
try {
//int i = 1/0;//虽然有异常,但该程序执行时不会发生任何异常,因为吞掉了。
return 10;
} finally {
return 20;
}
}
}
5)finally****面试题二
finally 对返回值影响
java
package org.wuya.test;
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;
}
}
}
分析字节码知道:如果在try中进行了return,之后又在finally中进行了修改,是不会影响返回结果的。因为它在return之前先做了暂存,然后执行了finally中的代码,然后再把暂存的值恢复到栈顶,返回的还是暂存的这个值。所以结果已经在return时确定了,再在finally中修改已经无效了。
延伸:
2.12 synchronized
从字节码角度分析:为什么synchronized能够保证加锁/释放锁的成对出现呢?
java
package org.wuya.test;
public class Demo3_13 {
public static void main(String[] args) {
Object lock = new Object();
synchronized (lock) {
System.out.println("ok");
}
}
}
java
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: new #2 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: astore_1
8: aload_1
9: dup
10: astore_2
11: monitorenter
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
20: aload_2
21: monitorexit
22: goto 30
25: astore_3
26: aload_2
27: monitorexit
28: aload_3
29: athrow
30: return
Exception table:
from to target type
12 22 25 any
25 28 25 any
LineNumberTable:
line 5: 0
line 6: 8
line 7: 12
line 8: 20
line 9: 30
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 args [Ljava/lang/String;
8 23 1 lock Ljava/lang/Object;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 25
locals = [ class "[Ljava/lang/String;", class java/lang/Object, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
MethodParameters:
Name Flags
args
注意:方法级别的 synchronized 不会在字节码指令中有所体现。
**三、**编译期处理
- 所谓的 语法糖 ,其实就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利(给糖吃嘛)
- 注意,以下代码的分析,借助了 javap 工具,idea 的反编译功能,idea 插件 jclasslib 等工具。另外, 编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了 几乎等价 的 java 源码方式,并不是编译器还会转换出中间的 java 源码,切记。
1. 自动生成无参构造方法
2. 自动拆装箱的转换
java
package org.wuya.test;
public class Candy2 {
public static void main(String[] args) {
Integer x = 1;
int y = x;
}
}
这个特性是 JDK 5 开始加入的。由于是编译期完成的,所以代码怎么写都行哦,不存在效率高低之分。即使下面这样写也不代表效率高哦。
java
public static void main(String[] args) {
Integer x = Integer.valueOf(1);
int y = x.intValue();
}
**3.**泛型集合取值
- 泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行 泛型擦除 的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理,所以在取值时,编译器真正生成的字节码中,还要额外做一个类型转换(checkcast)的操作。
- 擦除的是字节码上的泛型信息,可以看到 LocalVariableTypeTable 仍然保留了方法参数泛型的信息。但是有限制:虽然没有被擦除,但局部变量上的泛型信息没有办法通过反射机制拿到;只有在方法参数和返回值上带有泛型信息,才能用反射的方法获取到。
4. 可变参数
可变参数也是 JDK 5 开始加入的新特性。
java
package org.wuya.test;
public class Candy4 {
public static void foo(String... args) {
String[] array = args; // 直接赋值
System.out.println(array);
}
public static void main(String[] args) {
foo("hello", "world");
}
}
可变参数 String... args 其实是一个 String[] args ,从代码中的赋值语句中就可以看出来。
同样 java 编译器会在编译期间将上述代码变换为:
java
public class Candy4 {
public static void foo(String[] args) {
String[] array = args; // 直接赋值
System.out.println(array);
}
public static void main(String[] args) {
foo(new String[]{"hello", "world"});
}
}
注意:如果调用了 foo() 则等价代码为 foo(new String[]{}) ,创建了一个空的数组,而不会传递 null 进去。
5. foreach****循环
也是 JDK 5 开始引入的语法糖。
- 数组的循环会被编译器转换为:带下标的fori循环;
- 集合的循环实际被编译器转换为对迭代器的调用
注意
foreach 循环写法,能够配合数组,以及所有实现了 Iterable 接口的集合类一起使用,其中
Iterable 用来获取集合的迭代器( Iterator )
6. switch****字符串
从 JDK 7 开始, switch 可以作用于字符串和枚举类,这个功能其实也是语法糖。
字符串的switch,实际被拆分成两个整数的switch分支。
7. switch****枚举
java
package org.wuya.test;
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;
}
}
}
转换后代码:
java
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;
}
}
}
8. 枚举
JDK 7 新增了枚举类,以前面的性别枚举为例:
java
enum Sex {
MALE, FEMALE
}
转换后代码:
java
public final class Sex extends Enum<Sex> {
public static final Sex MALE;
public static final Sex FEMALE;
private static final Sex[] $VALUES;
static {
MALE = new Sex("MALE", 0);
FEMALE = new Sex("FEMALE", 1);
$VALUES = new Sex[]{MALE, FEMALE};
}
/**
* 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);
}
}
枚举类的本质也是一个Class,它里面的 MALE, FEMALE 实际上就是Class的两个实例对象。枚举类和普通类的最大区别就是,普通类的实例对象可以是无穷多个,而枚举类的实例对象是有限的,比如该例中实例对象只有MALE, FEMALE两个。通过上面的字节码可以分析出来。
9. try-with-resources
jdk1.7新增,用于简化资源的关闭。语法:
java
try (资源变量 =创建资源对象) {
} catch () {
}
其中资源对象需要实现 AutoCloseable 接口,例如 InputStream 、 OutputStream 、
Connection 、 Statement 、 ResultSet 等接口都实现了 AutoCloseable ,使用 try - with
resources 可以不用写 finally 语句块,编译器会帮助生成关闭资源代码,例如:
java
public class Candy9 {
public static void main(String[] args) {
try(InputStream is = new FileInputStream("d:\\1.txt")) {
System.out.println(is);
} catch (IOException e) {
e.printStackTrace();
}
}
}
会被转换为:
java
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();
}
}
}
10. 方法重写时的桥接方法
11. 匿名内部类
引用局部变量的匿名内部类,源代码:
java
public class Candy11 {
public static void test(final int x) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("ok:" + x);
}
};
}
}
转换后代码:
java
// 额外生成的类
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);
}
}
注意:
这同时解释了为什么匿名内部类引用局部变量时,局部变量必须是final的:因为在创建Candy11$1对象时,将x的值赋值给了Candy111对象的valx属性,所以x不应该再发生变化了,如果变化,那么val$x属性没有机会再跟着一起变化。
四、类加载阶段
三大步:加载;链接(验证、准备、解析);初始化。
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 工具查看
4.2 链接
1)验证
验证类是否符合 JVM规范,安全性检查。
2)准备★★★
为 static 变量分配空间,设置默认值;
- static 变量在JDK7之前存储于instanceKlass末尾,从JDK7开始,存储于_java_mirror末尾;
- static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段(第三大步)完成;
- 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了 ,赋值在准备阶段完成;
- 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成;
3)解析
将常量池中的符号引用解析为直接引用。(举例说明的)
4.3 初始化
<cinit>()V 方法
初始化即调用 <cinit>()V ,虚拟机会保证这个类的『构造方法』的线程安全。
发生初始化的时机
概括得说,类初始化是【懒惰的】
- main 方法所在的类,总会被首先初始化
- 首次访问这个类的静态变量或静态方法时
- 子类初始化,如果父类还没初始化,会引发
- 子类访问父类的静态变量,只会触发父类的初始化
- Class.forName
- new 会导致初始化
不会导致类初始化的情况
- 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
- 类对象.class 不会触发初始化
- 创建该类的数组不会触发初始化
- 类加载器的 loadClass 方法
- Class.forName 的参数 2 为 false 时
练习一:
从字节码分析,使用 a,b,c 这三个常量是否会导致 E 初始化
java
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;
}
练习二:典型应用 - 完成懒惰初始化单例模式
java
public final class Singleton {
private Singleton() { }
// 内部类中保存单例
private static class LazyHolder {
static final Singleton INSTANCE = new Singleton();
}
// 第一次调用 getInstance 方法,才会导致内部类加载和初始化其静态成员
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
静态内部类的好处:它可以访问外部类的资源。
以上的实现特点是:
- 懒惰实例化
- 初始化时的线程安全是有保障的
五、类加载器
以 JDK 8 为例:
java
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("java.lang.String");
ClassLoader classLoader = aClass.getClassLoader();
System.out.println(classLoader);//null
}
代码如上,如果获取某个类的类加载器是null时,表示该类是由启动类加载器Bootstrap加载的。因为Bootstrap加载器的代码是c语言写的,java直接获取不到。
5.1 双亲委派机制
所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则。
- Bootstrap 启动类加载器
- ExtClassLoader 扩展类加载器
- AppClassLoader 应用程序类加载器
1)启动类加载器优先级最高。
2)如我们自己写的类(classpath路径下),首先经由AppClassLoader时,必须经过【上级】加载器的允许,即会逐层向上询问,上级的两个加载器没有加载时,才能轮到AppClassLoader加载。(确保了类的唯一性、安全性)
3)首先检查本类加载器有没有加载过,如果还没有,就看它有没有上级,有上级就调用递归方法委派给上级,源码如下:(老师用的断点跟踪,非常清晰)
java
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) {
}
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;
}
}
5.2 线程上下文类加载器
我们在使用JDBC时,都需要加载Driver驱动,不知道你注意到没有,不写Class.forName("com.mysql.jdbc.Driver")也是可以让 com.mysql.jdbc.Driver 正确加载的,怎么做的呢?
大名鼎鼎的 Service Provider Interface (SPI)
SPI约定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名为文件,文件内容是实现类名称
这样就可以使用
java
ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class);
Iterator<接口类型> iter = allImpls.iterator();
while(iter.hasNext()) {
iter.next();
}
来得到实现类,体现的是【面向接口编程 + 解耦】的思想,在下面一些框架中都运用了此思想:
- JDBC
- Servlet 初始化器
- Spring 容器
- Dubbo(对 SPI 进行了扩展)
接着看 ServiceLoader.load 方法:
java
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取线程上下文类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器AppClassLoader,它内部又是由 Class.forName 调用了线程上下文类加载器完成类加载,具体代码略。
5.3 自定义类加载器
什么时候需要自定义类加载器?
1)想加载非 classpath 随意路径中的类文件
2)都是通过接口来使用实现,希望解耦时,常用在框架设计
3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器
步骤:
- 继承 ClassLoader 父类;
- 要遵从双亲委派机制,重写 findClass 方法;
注意不是重写 loadClass 方法,否则不会走双亲委派机制 - 读取类文件的字节码;
- 调用父类的 defineClass 方法来加载类;
- 使用者调用该类加载器的 loadClass 方法;
六、运行期优化
6.1 即时编译
JVM 将执行状态分成了 5 个层次:
-
0 层,解释执行(Interpreter)
-
1 层,使用 C1 即时编译器编译执行(不带 profiling)
-
2 层,使用 C1 即时编译器编译执行(带基本的 profiling)
-
3 层,使用 C1 即时编译器编译执行(带完全的 profiling)
-
4 层,使用 C2 即时编译器编译执行
profiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的回边次数】等。
即时编译器(JIT)与解释器的区别: -
解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释;
-
JIT 是将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需再编译;
-
解释器是将字节码解释为针对所有平台都通用的机器码;
-
JIT 会根据平台类型,生成平台特定的机器码。
对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。 执行效率上简单比较一下 Interpreter < C1 < C2,总的目标是发现热点代码(hotspot名称的由来),优化之。
即时编译的三种体现形式:
- 逃逸分析:刚才的一种优化手段称之为【逃逸分析】,发现新建的对象是否逃逸。可以使用 -XX:-DoEscapeAnalysis 关闭逃逸分析,再运行刚才的示例观察结果。
- 方法内联
- 字段优化
6.2 反射优化
java
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 Exception {
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 实现
java
class NativeMethodAccessorImpl extends MethodAccessorImpl {
private final Method method;
private DelegatingMethodAccessorImpl parent;
private int numInvocations;
NativeMethodAccessorImpl(Method var1) {
this.method = var1;
}
public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
// inflationThreshold 膨胀阈值,默认 15
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);
}
// 调用本地native实现,效率低下
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 回车表示分析该进程
(此处省略......)
注意
通过查看 ReflectionFactory 源码可知,sun.reflect.noInflation 可以用来禁用膨胀(直接生成 GeneratedMethodAccessor1,但首次生成比较耗时,如果仅反射调用一次,不划算)
sun.reflect.inflationThreshold 可以修改膨胀阈值。