Jvm学习笔记(二)
类加载
类文件结构
ClassFile{
u4 magic; //表示是不是class类型的文件 cafe babe
u2 minor_version; //十六进制表示 换算出来的数组与jdk版本有对应关系
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;
attrbute_info attrbutes[attrbutes_count];
}
常量池
- constant_pool 是一种表结构,以1 ~ constant_pool_count - 1为索引。表明了后面有多少个常量项。
- 常量池主要存放两大类常量:
字面量(Literal)
和符号引用(Symbolic Refrences)
- 它包含了class文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其他常量。常量池中的每一项都具备相同的特征。第1个字节作为类型标记,用于确定改项的格式,这个字节称为 tag byte(标记字节、标签字节)。
类型 | 标志(或标识) | 描述 |
---|---|---|
CONSTANT_utf8_info | 1 | UTF-8编码的字符串 |
CONSTANT_Integer_info | 3 | 整型字面量 |
CONSTANT_Float_info | 4 | 浮点型字面量 |
CONSTANT_Long_info | 5 | 长整型字面量 |
CONSTANT_Double_info | 6 | 双精度浮点型字面量 |
CONSTANT_Class_info | 7 | 类或接口的符号引用 |
CONSTANT_String_info | 8 | 字符串类型字面量 |
CONSTANT_Fieldref_info | 9 | 字段的符号引用 |
CONSTANT_Methodref_info | 10 | 类方法中的符号引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的符号引用 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_MethodType_info | 16 | 标志方法类型 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
字面量和符号引用
在对这些常量解读前,我们要搞清楚几个概念。
常量池主要存放两大类常量:字面量(Literal)
和符号引用(Symbolic Refrences)
。如下表:
常量 | 具体的常量 |
---|---|
字面量 | 文本字符串 |
声明为final的常量值 | |
符号引用 | 类和接口的全限定名 |
字段的名称和描述符 | |
方法的名称和描述符 |
字节码指令
javap工具
Oracle提供了javap工具来反编译class文件
反编译命令
javap -v HelloWorld.class
显示结果
Last modified 2022-7-8; size 559 bytes
MD5 checksum 5053c7c4ad0a79418343e5c4e0e97476
Compiled from "HelloWorld.java"
public class com.gao.demo.HelloWorld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()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 // com/gao/demo/HelloWorld
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/gao/demo/HelloWorld;
#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 // "<init>":()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 com/gao/demo/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 com.gao.demo.HelloWorld();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/gao/demo/HelloWorld;
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 6: 0
line 7: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"
图解运行流程
测试代码
public class Demo {
public static void main(String[] args) {
int a = 10;
int b = Short.MAX_VALUE + 1;
int c = a + b;
}
}
使用javap查看字节码文件
Last modified 2022-7-8; size 475 bytes
MD5 checksum 19b70530830a66824666ba8fd66fdc33
Compiled from "Demo.java"
public class com.gao.demo.Demo
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#23 // java/lang/Object."<init>":()V
#2 = Class #24 // java/lang/Short
#3 = Integer 32768
#4 = Class #25 // com/gao/demo/Demo
#5 = Class #26 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 LocalVariableTable
#11 = Utf8 this
#12 = Utf8 Lcom/gao/demo/Demo;
#13 = Utf8 main
#14 = Utf8 ([Ljava/lang/String;)V
#15 = Utf8 args
#16 = Utf8 [Ljava/lang/String;
#17 = Utf8 a
#18 = Utf8 I
#19 = Utf8 b
#20 = Utf8 c
#21 = Utf8 SourceFile
#22 = Utf8 Demo.java
#23 = NameAndType #6:#7 // "<init>":()V
#24 = Utf8 java/lang/Short
#25 = Utf8 com/gao/demo/Demo
#26 = Utf8 java/lang/Object
{
public com.gao.demo.Demo();
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 5: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/gao/demo/Demo;
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: return
LineNumberTable:
line 8: 0
line 9: 3
line 10: 6
line 11: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 args [Ljava/lang/String;
3 8 1 a I
6 5 2 b I
10 1 3 c I
}
SourceFile: "Demo.java"
类加载
类加载器会将main方法所在类进行类加载的操作,将字节数据读取到内存里来。
常量池放入运行时常量池
运行时常量池是方法区的一部分
比较小的数字不存放在常量池中,比如int a = 10
,如果数字的范围超过了整数的最大值,就会存放在常量池中。
方法字节码载入方法区
将方法区字节码放入方法区后,main线程开始运行,分配栈帧。
浅绿色代表局部变量表,蓝色代表操作数栈。
操作流程解析
(stack = 2,locals = 4) 对应操作数栈有2个空间(每个空间4个字节),局部变量表中有4个槽位
bipush 10
- 将一个byte压入操作数栈(其长度会补齐四个字节),类似的命令还有sipush(将short压入操作数栈),ldc(将一个int压入操作数栈),ldc2_w,(将一个long压入操作数栈,分两次压入,因为long是8字节)
istore 1 将操作数栈的数弹出,存入局部变量表中,1代表槽位。此时a就被赋予了10。
ldc #3
- 从常量池加载#3数据到操作数栈
- short.max_value是32767,所以32768 = short.Max + 1,这是在编译过程中就准备好的
istore2(局部变量表中的二号槽位)
给b赋值
iload1,load2
读取局部变量表1和2的数到操作数栈
iadd
弹出操作数栈的两个数,将结果存入操作数栈。
istore3
给c赋值(局部变量表中的三号槽位)
getstatic #4
invokevirtual #5
- 找到常量池 #5项
- 定位到方法区java/io/PrintStream.println(I) v方法
- 生成新的栈帧(分配locals,stack等)
- 传递参数,执行新栈帧中的字节码
控制指令
条件控制指令:
几点说明:
- byte,short,char 都会按int比较,因为操作数栈都是4字节
- goto 用来进行跳转到指定行号的字节码
java 条件判断源码:
// 从字节码角度来分析:条件判断指令
public class T04_ByteAnalyseIf {
public static void main(String[] args) {
int a = 0;
if (a == 0) {
a = 10;
} else {
a = 20;
}
}
}
字节码:使用javap -v T04_ByteAnalyseIf.class,将java程序对应的字节码如下,并做了执行的注释。
0: iconst_0 // int型常量值0进操作数栈
1: istore_1 // 从操作数栈弹出数据存储局部变量表1号槽位
2: iload_1 // 从局部变量表1号槽位中加载数据到操作数栈中
3: ifne 12 // 当栈顶int型数值不等于0时跳转到12行
6: bipush 10 // 将一个byte型常量值10 推送至栈顶
8: istore_1 // 将栈顶int型数值存入第二个局部变量,从0开始计数
9: goto 15 // 跳转到15行
12: bipush 20 // 将一个byte型常量值20 推送至栈顶
14: istore_1 // 将栈顶int型数值存入第二个局部变量,从0开始计数
15: return // 当前方法返回void
思考:以上比较指令中没有long, float, double 的比较,那么它们要比较怎么办?
循环控制指令:
其实循环控制还是前面介绍的那些指令,例如while循环:
// 从字节码角度来分析:循环控制指令
public class T05_ByteAnalyseWhile {
public static void main(String[] args) {
int a = 0;
while (a < 10) {
a++;
}
}
}
T05_ByteAnalyseWhile 字节码:使用javap -v T05_ByteAnalyseWhile.class,将java程序对应的字节码如下,并做了执行的注释。
0: iconst_0 // int型常量值0进栈
1: istore_1 // 将栈顶int型数值存入第二个局部变量,从0开始计数
2: iload_1 // 第二个int型局部变量进栈,从0开始计数
3: bipush 10 // 将一个byte型常量值推送至栈顶
5: if_icmpge 14 // 比较栈顶两int型数值大小,当结果大于等于0时跳转到14行
8: iinc 1, 1 // 指定int型变量增加指定值,即自增1
11: goto 2 // 无条件跳转
14: return // 当前方法返回void
上述是从字节码角度分析while,下面是从字节码角度分析do while:
// 从字节码角度来分析:循环控制do while指令
public class T06_ByteAnalyseDoWhile {
public static void main(String[] args) {
int a = 0;
do {
a++;
} while (a < 10);
}
}
T06_ByteAnalyseDoWhile 字节码:使用javap -v T06_ByteAnalyseDoWhile.class,将java程序对应的字节码如下,并做了执行的注释。
0: iconst_0 // int型常量值0进栈
1: istore_1 // 将栈顶int型数值存入第二个局部变量,从0开始计数
2: iinc 1, 1 // 指定int型变量增加指定值,即自增1
5: iload_1 // 第二个int型局部变量进栈,从0开始计数
6: bipush 10 // 将一个byte型常量值推送至栈顶
8: if_icmplt 2 // 比较栈顶两int型数值大小,当结果小于0时跳转
11: return // 当前方法返回void
上述是从字节码角度分析do while,下面是从字节码角度分析 for 循环:
// 从字节码角度来分析:循环控制 for 指令
public class T07_ByteAnalyseFor {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
}
}
}
T07_ByteAnalyseFor 字节码:使用javap -v T07_ByteAnalyseFor.class,将java程序对应的字节码如下,并做了执行的注释。
0: iconst_0 // int型常量值0进栈
1: istore_1 // 将栈顶int型数值存入第二个局部变量,从0开始计数
2: iload_1 // 第二个int型局部变量进栈,从0开始计数
3: bipush 10 // 将一个byte型常量值推送至栈顶
5: if_icmpge 14 // 比较栈顶两int型数值大小,当结果大于等于0时跳转到14行
8: iinc 1, 1 // 指定int型变量增加指定值,即自增1
11: goto 2 // 无条件跳转
14: return // 当前方法返回void
注意:比较while 和 for 的字节码,会发现它们是一模一样的,殊途也能同归。所以我们编写的while 循环 与 for循环在底层是一样的执行。
构造方法
字节码指令 cinit:
public class demo{
static int i = 10;
static{
i = 20;
}
static{
i = 30;
}
}
编译器会按从上至下的顺序,收集所有static 静态代码块和静态成员赋值的代码,合并为一个特殊的方法()V:
stack=1, locals=0, args_size=0
0: bipush 10
2: putstatic #3 // Field i:I
5: bipush 20
7: putstatic #3 // Field i:I
10: bipush 30
12: putstatic #3 // Field i:I
15: return
字节码指令init
public class Demo4 {
private String a = "s1";
{
b = 20;
}
private int b = 10;
{
a = "s2";
}
public Demo4(String a, int b) {
this.a = a;
this.b = b;
}
public static void main(String[] args) {
Demo4 d = new Demo4("s3", 30);
System.out.println(d.a);
System.out.println(d.b);
}
}
编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在后
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
方法调用
public class Demo5 {
public Demo5() {
}
private void test1() {
}
private final void test2() {
}
public void test3() {
}
public static void test4() {
}
public static void main(String[] args) {
Demo5 demo5 = new Demo5();
demo5.test1();
demo5.test2();
demo5.test3();
Demo5.test4();
}
}
不同方法在调用时,对应的虚拟机指令有所区别
- 私有、构造、被final修饰的方法,在调用时都使用invokespecial指令
- 普通成员方法在调用时,使用invokevirtual指令。因为编译期间无法确定该方法的内容,只有在运行期间才能确定
- 静态方法在调用时使用invokestatic指令
字节码
Code:
stack=2, locals=2, args_size=1
0: new #2 // class com/nyima/JVM/day5/Demo5
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: invokestatic #7 // Method test4:()V
23: return
- new 是创建【对象】,给对象分配堆内存,执行成功会将【对象引用】压入操作数栈
- dup 是赋值操作数栈栈顶的内容,本例即为【对象引用】,为什么需要两份引用呢,一个是要配合 invokespecial 调用该对象的构造方法 “init”😦)V (会消耗掉栈顶一个引用),另一个要 配合 astore_1 赋值给局部变量
多态原理
public class Demo2 {
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();
}
static abstract class Animal{
public abstract void eat();
@Override
public String toString() {
return "我是" + this.getClass().getSimpleName();
}
}
static class Dog extends Animal{
@Override
public void eat() {
System.out.println("啃骨头");
}
}
static class Cat extends Animal{
@Override
public void eat() {
System.out.println("吃鱼");
}
}
}
调出HSDB工具
在jdk目录下输入下列命令
java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB
使用jps指令查看进程数
根据进程号在HSDB中连接
因为普通成员方法需要在运行时才能确定具体的内容,所以虚拟机需要调用invokevirtual指令
在执行invokevirtual指令时,经历了以下几个步骤
- 先通过栈帧中对象的引用找到对象
- 分析对象头,找到对象实际的Class
- Class结构中有vtable
- 查询vtable找到方法的具体地址
- 执行方法的字节码
异常捕获
try-catch
public class Demo1 {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
}catch (Exception e) {
i = 20;
}
}
}
对应字节码指令
Code:
stack=1, locals=3, args_size=1
0: iconst_0
1: istore_1
2: bipush 10
4: istore_1
5: goto 12
8: astore_2
9: bipush 20
11: istore_1
12: return
//多出来一个异常表
Exception table:
from to target type
2 5 8 Class java/lang/Exception
- 可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开(也就是检测2~4行)的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号
- 8行的字节码指令 astore_2 是将异常对象引用存入局部变量表的2号位置(为e)
多个single-catch
public class Demo1 {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
}catch (ArithmeticException e) {
i = 20;
}catch (Exception e) {
i = 30;
}
}
}
对应的字节码
Code:
stack=1, locals=3, args_size=1
0: iconst_0
1: istore_1
2: bipush 10
4: istore_1
5: goto 19
8: astore_2
9: bipush 20
11: istore_1
12: goto 19
15: astore_2
16: bipush 30
18: istore_1
19: return
Exception table:
from to target type
2 5 8 Class java/lang/ArithmeticException
2 5 15 Class java/lang/Exception
- 因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用
finally
public class Demo2 {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
} finally {
i = 30;
}
}
}
对应字节码
Code:
stack=1, locals=4, args_size=1
0: iconst_0
1: istore_1
//try块
2: bipush 10
4: istore_1
//try块执行完后,会执行finally
5: bipush 30
7: istore_1
8: goto 27
//catch块
11: astore_2 //异常信息放入局部变量表的2号槽位
12: bipush 20
14: istore_1
//catch块执行完后,会执行finally
15: bipush 30
17: istore_1
18: goto 27
//出现异常,但未被Exception捕获,会抛出其他异常,这时也需要执行finally块中的代码
21: astore_3
22: bipush 30
24: istore_1
25: aload_3
26: athrow //抛出异常
27: return
Exception table:
from to target type
2 5 11 Class java/lang/Exception
2 5 21 any
11 15 21 any
可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch剩余的异常类型流程
注意:虽然从字节码指令看来,每个块中都有finally块,但是finally块中的代码只会被执行一次
finally中的return
public class Demo3 {
public static void main(String[] args) {
int i = Demo3.test();
//结果为20
System.out.println(i);
}
public static int test() {
int i;
try {
i = 10;
return i;
} finally {
i = 20;
return i;
}
}
}
对应字节码
Code:
stack=1, locals=3, args_size=0
0: bipush 10
2: istore_0
3: iload_0
4: istore_1 //暂存返回值
5: bipush 20
7: istore_0
8: iload_0
9: ireturn //ireturn会返回操作数栈顶的整型值20
//如果出现异常,还是会执行finally块中的内容,没有抛出异常
10: astore_2
11: bipush 20
13: istore_0
14: iload_0
15: ireturn //这里没有athrow了,也就是如果在finally块中如果有返回操作的话,且try块中出现异常,会吞掉异常!
Exception table:
from to target type
0 5 10 anyCopy
- 由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯定以finally的为准
- 至于字节码中第 2 行,似乎没啥用,且留个伏笔,看下个例子
- 跟上例中的 finally 相比,发现没有 athrow 了,这告诉我们:如果在 finally 中出现了 return,会吞掉异常
- 所以不要在finally中进行返回操作
被吞掉的异常
public class Demo3 {
public static void main(String[] args) {
int i = Demo3.test();
//最终结果为20
System.out.println(i);
}
public static int test() {
int i;
try {
i = 10;
//这里应该会抛出异常
i = i/0;
return i;
} finally {
i = 20;
return i;
}
}
}
会发现打印结果为20,并未抛出异常
finally不带return
public class Demo4 {
public static void main(String[] args) {
int i = Demo4.test();
System.out.println(i);
}
public static int test() {
int i = 10;
try {
return i;
} finally {
i = 20;
}
}
}
对应字节码
Code:
stack=1, locals=3, args_size=0
0: bipush 10
2: istore_0 //赋值给i 10
3: iload_0 //加载到操作数栈顶
4: istore_1 //加载到局部变量表的1号位置
5: bipush 20
7: istore_0 //赋值给i 20
8: iload_1 //加载局部变量表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
Synchronized
public class Demo5 {
public static void main(String[] args) {
int i = 10;
Lock lock = new Lock();
synchronized (lock) {
System.out.println(i);
}
}
}
class Lock{}
对应字节码
Code:
stack=2, locals=5, args_size=1
0: bipush 10
2: istore_1
3: new #2 // class com/nyima/JVM/day06/Lock
6: dup //复制一份,放到操作数栈顶,用于构造函数消耗
7: invokespecial #3 // Method com/nyima/JVM/day06/Lock."<init>":()V
10: astore_2 //剩下的一份放到局部变量表的2号位置
11: aload_2 //加载到操作数栈
12: dup //复制一份,放到操作数栈,用于加锁时消耗
13: astore_3 //将操作数栈顶元素弹出,暂存到局部变量表的三号槽位。这时操作数栈中有一份对象的引用
14: monitorenter //加锁
//锁住后代码块中的操作
15: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
18: iload_1
19: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
//加载局部变量表中三号槽位对象的引用,用于解锁
22: aload_3
23: monitorexit //解锁
24: goto 34
//异常操作
27: astore 4
29: aload_3
30: monitorexit //解锁
31: aload 4
33: athrow
34: return
//可以看出,无论何时出现异常,都会跳转到27行,将异常放入局部变量中,并进行解锁操作,然后加载异常并抛出异常。
Exception table:
from to target type
15 24 27 any
27 31 27 any
编译期处理
所谓的 语法糖 ,其实就是指 java 编译器把 *.java 源码编译为 .class 字节码的过程中,自动生成和转换*的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利
注意,以下代码的分析,借助了 javap 工具,idea 的反编译功能,idea 插件 jclasslib 等工具。另外, 编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了 几乎等价 的 java 源码方式,并不是编译器还会转换出中间的 java 源码,切记。
默认构造器
public class Candy1{
}
编译成class后的代码
public class Candy1{
//这个无参构造器是编译器帮助我们加上的
public Candy1(){
super(); //即调用父类Object的无参构造方法(java/lang/Object."<init>":()V
}
}
自动拆装箱
基本类型和其包装类型的相互转换过程,称为拆装箱
在JDK 5以后,它们的转换可以在编译期自动完成
public class Demo2 {
public static void main(String[] args) {
Integer x = 1;
int y = x;
}
}Copy
转换过程如下
public class Demo2 {
public static void main(String[] args) {
//基本类型赋值给包装类型,称为装箱
Integer x = Integer.valueOf(1);
//包装类型赋值给基本类型,称谓拆箱
int y = x.intValue();
}
}
泛型集合取值
泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行 泛型擦除 的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理:
public class Demo3 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(10);
Integer x = list.get(0);
}
}Copy
对应字节码
Code:
stack=2, locals=3, args_size=1
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: bipush 10
11: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
//这里进行了泛型擦除,实际调用的是add(Objcet o)
14: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
19: pop
20: aload_1
21: iconst_0
//这里也进行了泛型擦除,实际调用的是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: return
所以调用get函数取值时,有一个类型转换的操作
Integer x = (Integer) list.get(0);
如果要将返回结果赋值给一个int类型的变量,则还有自动拆箱的操作
int x = (Integer) list.get(0).intValue();
擦除的是字节码上的泛型信息,可以看到LocalVaribaleTypeTable仍然保留了方法参数类型泛型的信息
通过反射仍然可以获得这些信息。
public Set<Integer> test(List<String> list, Map<Integer,Object> map){
return null;
}
public static void main(String[] args) throws NoSuchMethodException {
Method test = Demo3.class.getMethod("test", List.class, Map.class);
Type[] types = test.getGenericExceptionTypes();
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])/;
}
}
}
}
可变参数
public class Demo4 {
public static void foo(String... args) {
//将args赋值给arr,可以看出String...实际就是String[]
String[] arr = args;
System.out.println(arr.length);
}
public static void main(String[] args) {
foo("hello", "world");
}
}
可变参数 String… args 其实是一个 String[] args ,从代码中的赋值语句中就可以看出来。 同 样 java 编译器会在编译期间将上述代码变换为:
public class Demo4 {
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[]{}),创建了一个空数组,而不是直接传递的null
foreach
public class Demo5 {
public static void main(String[] args) {
//数组赋初值的简化写法也是一种语法糖。
int[] arr = {1, 2, 3, 4, 5};
for(int x : arr) {
System.out.println(x);
}
}
}
编译器会帮我们转换为
public class Demo5 {
public Demo5 {}
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);
}
}
}
如果是集合使用foreach
public class Demo5 {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
for (Integer x : list) {
System.out.println(x);
}
}
}
集合要使用foreach,需要该集合类实现了Iterable接口,因为集合的遍历需要用到迭代器Iterator
public class Demo5 {
public Demo5 {}
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);
}
}
}
switch字符串
public class Demo6 {
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;
}
}
}
在编译器中执行的操作
public class Demo6 {
public Demo6() {
}
public static void main(String[] args) {
String str = "hello";
int x = -1;
//通过字符串的hashCode+value来判断是否匹配
switch (str.hashCode()) {
//hello的hashCode
case 99162322 :
//再次比较,因为字符串的hashCode有可能相等
if(str.equals("hello")) {
x = 0;
}
break;
//world的hashCode
case 11331880 :
if(str.equals("world")) {
x = 1;
}
break;
default:
break;
}
//用第二个switch在进行输出判断
switch (x) {
case 0:
System.out.println("h");
break;
case 1:
System.out.println("w");
break;
default:
break;
}
}
}
编译期间,单个的switch被拆分为两个
- 在编译期间,单个的switch被分为了两个。
- 第一个判断字符串是否相等并给x赋值
- 判断的过程同时用了hashcode和equals
- 使用hashcode判断是为了提高效率,如果只用equals效率会下降
- 使用equals是防止有hashCode冲突。比如BM和C
- 第二个根据x的值来决定判断输出语句。
switch枚举
public class Demo7 {
public static void main(String[] args) {
SEX sex = SEX.MALE;
switch (sex) {
case MALE:
System.out.println("man");
break;
case FEMALE:
System.out.println("woman");
break;
default:
break;
}
}
}
enum SEX {
MALE, FEMALE;
}
编译器中执行的代码如下
public class Demo7 {
/**
* 定义一个合成类(仅 jvm 使用,对我们不可见)
* 用来映射枚举的 ordinal 与数组元素的关系
* 枚举的 ordinal 表示枚举对象的序号,从 0 开始
* 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1
*/
static class $MAP {
//数组大小即为枚举元素个数,里面存放了case用于比较的数字
static int[] map = new int[2];
static {
//ordinal即枚举元素对应所在的位置,MALE为0,FEMALE为1
map[SEX.MALE.ordinal()] = 1;
map[SEX.FEMALE.ordinal()] = 2;
}
}
public static void main(String[] args) {
SEX sex = SEX.MALE;
//将对应位置枚举元素的值赋给x,用于case操作
int x = $MAP.map[sex.ordinal()];
switch (x) {
case 1:
System.out.println("man");
break;
case 2:
System.out.println("woman");
break;
default:
break;
}
}
}
enum SEX {
MALE, FEMALE;
}
枚举类
enum SEX {
MALE, FEMALE;
}
转换后的代码
public final class Sex extends Enum<Sex> {
//对应枚举类中的元素
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};
}
//调用父类中的方法
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);
}
}
try-with_resources
jdk7 开始新增了对需要关闭的资源处理的特殊语法“try-with-resources":
try(资源变量 = 创建资源对象){
}catch(){
}
}
其中资源对象需要实现AutoCloseable接口,例如InputStream、OutputStream、Connection、Statment、ResultSet等接口都实现了AutoCloseable,使用try-with-resources可以不用写finally语句块,编译器会帮助生成关闭资源代码,例如:
public class Candy{
public static void main(String[] args){
try (InputStream is = new FileInputStream("d://1.txt")){
System.out.println(is);
}catch (IOException e){
e.printStackTrace();
}
}
}
会转换为
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方法呢?
是为了防止异常信息的丢失。
方法重写时的桥接方法
我们都知道,方法重写时对返回值分两种情况:
- 父子类的返回值完全一致
- 子类返回值可以是父类返回值的子类
class A{
public Number m(){
return 1;
}
}
class B extends A {
@Override
//子类m方法的返回值是Interger是父类m方法返回值Number的子类
public Integer m(){
return 2;
}
}
对于子类编译器会做如下处理
class B extends A{
public Interger m(){
return 2;
}
//此方法才是真正重写了父类public Number m()方法
public synthetic bridge Number m(){
//调用public Interger m()
return m();
}
}
其中桥接方法比较特殊,仅对java虚拟机可见,并且与原来的public integer m()没有命名冲突,可以用下面的反射代码来验证:
for(Method m : B.class.getDeclaredMethods()){
System.out.println(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 Demo6 {
public static void test(final int x){
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("ok:" + x);
}
};
}
}
转换后的代码:
final class Candy$1 implements Runnable{
int val$x;
Candy$1(int x){
this.val$x = x;
}
@Override
public void run() {
System.out.println("ok:" + this.val$x);
}
}
public class Candy11{
public static void test(final int x){
Runnable runable = new Candy11$1(x);
}
}
注意
同时这也解释了为什么匿名内部类引用局部变量时,局部变量必须时final的:因为在创建Candy11$1对象时,将x的值赋值给了Candy11$1对象的valx属性,所以x不应该再发生变化了,如果发生变化,那么valx属性没有机会再跟着一起变化。
类加载阶段
类加载子系统
内存结构概述
注意:方法区只有HotSpot虚拟机有,J9,JRockit都没有
如果自己想手写一个Java虚拟机的话,主要考虑哪些结构呢?
- 类加载器
- 执行引擎
类加载器子系统
类加载器子系统作用:
-
类加载器子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识。
-
ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。
-
加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
类加载器ClassLoader角色
- class file(在下图中就是Car.class文件)存在于本地硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载到JVM当中来根据这个文件实例化出n个一模一样的实例。
- class file加载到JVM中,被称为DNA元数据模板(在下图中就是内存中的Car Class),放在方法区。
- 在.class文件–>JVM–>最终成为元数据模板,此过程就要一个运输工具(类装载器Class Loader),扮演一个快递员的角色。
类加载过程
概述
public class HelloLoader {
public static void main(String[] args) {
System.out.println("谢谢ClassLoader加载我....");
System.out.println("你的大恩大德,我下辈子再报!");
}
}
它的加载过程是怎么样的呢?
- 执行 main() 方法(静态方法)就需要先加载main方法所在类 HelloLoader
- 加载成功,则进行链接、初始化等操作。完成后调用 HelloLoader 类中的静态方法 main
- 加载失败则抛出异常
完整的流程图如下所示:
加载阶段
加载:
-
通过一个类的全限定名获取定义此类的二进制字节流
-
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
-
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
加载class文件的方式:
- 从本地系统中直接加载
- 通过网络获取,典型场景:Web Applet
- 从zip压缩包中读取,成为日后jar、war格式的基础
- 运行时计算生成,使用最多的是:动态代理技术
- 由其他文件生成,典型场景:JSP应用从专有数据库中提取.class文件,比较少见
- 从加密文件中获取,典型的防Class文件被反编译的保护措施
链接阶段
链接分为三个子阶段:验证 -> 准备 -> 解析
验证(Verify)
- 目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全
- 主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
举例
使用 BinaryViewer软件查看字节码文件,其开头均为 CAFE BABE ,如果出现不合法的字节码文件,那么将会验证不通过。
准备(Prepare)
- 为类变量(static变量)分配内存并且设置该类变量的默认初始值,即零值
- 这里不包含用final修饰的static,因为final在编译的时候就会分配好了默认值,准备阶段会显式初始化
- 注意:这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中
举例
代码:变量a在准备阶段会赋初始值,但不是1,而是0,在初始化阶段会被赋值为 1
public class HelloApp {
private static int a = 1;//prepare:a = 0 ---> initial : a = 1
public static void main(String[] args) {
System.out.println(a);
}
}
解析(Resolve)
-
将常量池内的符号引用转换为直接引用的过程
-
事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行
-
符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
-
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等
符号引用
- 反编译 class 文件后可以查看符号引用,下面带# 的就是符号引用
初始化阶段
类的初始化时机
- 创建类的实例
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(比如:Class.forName(“com.atguigu.Test”))
- 初始化一个类的子类
- Java虚拟机启动时被标明为启动类的类
- JDK7开始提供的动态语言支持:java.lang.invoke.MethodHandle实例的解析结果REF_getStatic、REF putStatic、REF_invokeStatic句柄对应的类没有初始化,则初始化
除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化,即不会执行初始化阶段(不会调用 clinit() 方法和 init() 方法)
clinit()
-
初始化阶段就是执行类构造器方法
<clinit>()
的过程 -
此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。也就是说,当我们代码中包含static变量的时候,就会有clinit方法
-
<clinit>()
方法中的指令按语句在源文件中出现的顺序执行 -
<clinit>()
不同于类的构造器。(关联:构造器是虚拟机视角下的<init>()
) -
若该类具有父类,JVM会保证子类的
<clinit>()
执行前,父类的<clinit>()
已经执行完毕 -
虚拟机必须保证一个类的
<clinit>()
方法在多线程下被同步加锁
IDEA 中安装 JClassLib Bytecode viewer 插件,可以很方便的看字节码。安装过程可以自行百度
1,2,3说明
举例1:有static变量
查看下面这个代码的字节码,可以发现有一个<clinit>()
方法。
public class ClassInitTest {
private static int num = 1;
static{
num = 2;
number = 20;
System.out.println(num);
//System.out.println(number);//报错:非法的前向引用。
}
/**
* 1、linking之prepare: number = 0 --> initial: 20 --> 10
* 2、这里因为静态代码块出现在声明变量语句前面,所以之前被准备阶段为0的number变量会
* 首先被初始化为20,再接着被初始化成10(这也是面试时常考的问题哦)
*
*/
private static int number = 10;
public static void main(String[] args) {
System.out.println(ClassInitTest.num);//2
System.out.println(ClassInitTest.number);//10
}
}
<clint字节码>:
0 iconst_1
1 putstatic #3 <com/atguigu/java/ClassInitTest.num>
4 iconst_2
5 putstatic #3 <com/atguigu/java/ClassInitTest.num>
8 bipush 20 //先赋20
10 putstatic #5 <com/atguigu/java/ClassInitTest.number>
13 getstatic #2 <java/lang/System.out>
16 getstatic #3 <com/atguigu/java/ClassInitTest.num>
19 invokevirtual #4 <java/io/PrintStream.println>
22 bipush 10 //再赋10
24 putstatic #5 <com/atguigu/java/ClassInitTest.number>
27 return
当我们代码中包含static变量的时候,就会有clinit方法
举例2:无 static 变量
加上之后就有了
4说明
在构造器中:
- 先将类变量 a 赋值为 10
- 再将局部变量赋值为 20
5说明
若该类具有父类,JVM会保证子类的<clinit>()
执行前,父类的<clinit>()
已经执行完毕
如上代码,加载流程如下:
- 首先,执行 main() 方法需要加载 ClinitTest1 类
- 获取 Son.B 静态变量,需要加载 Son 类
- Son 类的父类是 Father 类,所以需要先执行 Father 类的加载,再执行 Son 类的加载
6说明
虚拟机必须保证一个类的<clinit>()
方法在多线程下被同步加锁
public class DeadThreadTest {
public static void main(String[] args) {
Runnable r = () -> {
System.out.println(Thread.currentThread().getName() + "开始");
DeadThread dead = new DeadThread();
System.out.println(Thread.currentThread().getName() + "结束");
};
Thread t1 = new Thread(r,"线程1");
Thread t2 = new Thread(r,"线程2");
t1.start();
t2.start();
}
}
class DeadThread{
static{
if(true){
System.out.println(Thread.currentThread().getName() + "初始化当前类");
while(true){
}
}
}
}
输出结果:
线程2开始
线程1开始
线程2初始化当前类
/然后程序卡死了
程序卡死,分析原因:
- 两个线程同时去加载 DeadThread 类,而 DeadThread 类中静态代码块中有一处死循环
- 先加载 DeadThread 类的线程抢到了同步锁,然后在类的静态代码块中执行死循环,而另一个线程在等待同步锁的释放
- 所以无论哪个线程先执行 DeadThread 类的加载,另外一个类也不会继续执行。(一个类只会被加载一次)
类加载器的分类
概述
-
JVM严格来讲支持两种类型的类加载器 。分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)
-
从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器
-
无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有3个,如下所示
ExtClassLoader
AppClassLoader
public class ClassLoaderTest {
public static void main(String[] args) {
//获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
//获取其上层:扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@1540e19d
//获取其上层:获取不到引导类加载器
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader);//null
//对于用户自定义类来说:默认使用系统类加载器进行加载
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
//String类使用引导类加载器进行加载的。---> Java的核心类库都是使用引导类加载器进行加载的。
ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println(classLoader1);//null
}
}
- 我们尝试获取引导类加载器,获取到的值为 null ,这并不代表引导类加载器不存在,因为引导类加载器右 C/C++ 语言,我们获取不到
- 两次获取系统类加载器的值都相同:sun.misc.Launcher$AppClassLoader@18b4aac2 ,这说明系统类加载器是全局唯一的
虚拟机自带的加载器
启动类加载器
启动类加载器(引导类加载器,Bootstrap ClassLoader)
- 这个类加载使用C/C++语言实现的,嵌套在JVM内部
- 它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
- 并不继承自java.lang.ClassLoader,没有父加载器
- 加载扩展类和应用程序类加载器,并作为他们的父类加载器
- 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
扩展类加载器
扩展类加载器(Extension ClassLoader)
- Java语言编写,由sun.misc.Launcher$ExtClassLoader实现
- 派生于ClassLoader类
- 父类加载器为启动类加载器
- 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载
系统类加载器
应用程序类加载器(也称为系统类加载器,AppClassLoader)
- Java语言编写,由sun.misc.LaunchersAppClassLoader实现
- 派生于ClassLoader类
- 父类加载器为扩展类加载器
- 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
- 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
- 通过classLoader.getSystemclassLoader()方法可以获取到该类加载器
public class ClassLoaderTest1 {
public static void main(String[] args) {
System.out.println("**********启动类加载器**************");
//获取BootstrapClassLoader能够加载的api的路径
URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL element : urLs) {
System.out.println(element.toExternalForm());
}
//从上面的路径中随意选择一个类,来看看他的类加载器是什么:引导类加载器
ClassLoader classLoader = Provider.class.getClassLoader();
System.out.println(classLoader);
System.out.println("***********扩展类加载器*************");
String extDirs = System.getProperty("java.ext.dirs");
for (String path : extDirs.split(";")) {
System.out.println(path);
}
//从上面的路径中随意选择一个类,来看看他的类加载器是什么:扩展类加载器
ClassLoader classLoader1 = CurveDB.class.getClassLoader();
System.out.println(classLoader1);//sun.misc.Launcher$ExtClassLoader@1540e19d
}
}
输出结果
**********启动类加载器**************
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/rt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/jfr.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/classes
null
***********扩展类加载器*************
C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext
C:\Windows\Sun\Java\lib\ext
sun.misc.Launcher$ExtClassLoader@29453f44
用户自定义类加载器
什么时候需要自定义类加载器?
在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。那为什么还需要自定义类加载器?
- 隔离加载类(比如说我假设现在Spring框架,和RocketMQ有包名路径完全一样的类,类名也一样,这个时候类就冲突了。不过一般的主流框架和中间件都会自定义类加载器,实现不同的框架,中间价之间是隔离的)
- 修改类加载的方式
- 扩展加载源(还可以考虑从数据库中加载类,路由器等等不同的地方)
- 防止源码泄漏(对字节码文件进行解密,自己用的时候通过自定义类加载器来对其进行解密)
如何自定义类加载器?
- 开发人员可以通过继承抽象类java.lang.ClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求
- 在JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadClass()方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findclass()方法中
- 在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URIClassLoader类,这样就可以避免自己去编写findclass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。
代码示例
public class CustomClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] result = getClassFromCustomPath(name);
if (result == null) {
throw new FileNotFoundException();
} else {
//defineClass和findClass搭配使用
return defineClass(name, result, 0, result.length);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
throw new ClassNotFoundException(name);
}
//自定义流的获取方式
private byte[] getClassFromCustomPath(String name) {
//从自定义路径中加载指定类:细节略
//如果指定路径的字节码文件进行了加密,则需要在此方法中进行解密操作。
return null;
}
public static void main(String[] args) {
CustomClassLoader customClassLoader = new CustomClassLoader();
try {
Class<?> clazz = Class.forName("One", true, customClassLoader);
Object obj = clazz.newInstance();
System.out.println(obj.getClass().getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
}
}
关于ClassLoader
ClassLoader 类介绍
ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器)
sun.misc.Launcher 它是一个java虚拟机的入口应用
获取ClassLoader途径
public class ClassLoaderTest2 {
public static void main(String[] args) {
try {
//1.
ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
System.out.println(classLoader);
//2.
ClassLoader classLoader1 = Thread.currentThread().getContextClassLoader();
System.out.println(classLoader1);
//3.
ClassLoader classLoader2 = ClassLoader.getSystemClassLoader().getParent();
System.out.println(classLoader2);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
输出结果:
null
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1540e19d
Process finished with exit code 0
双亲委派机制
双亲委派机制原理
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
- 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
- 父类加载器一层一层往下分配任务,如果子类加载器能加载,则加载此类,如果将加载任务分配至系统类加载器也无法加载此类,则抛出异常
双亲委派机制代码演示
举例1
1、我们自己建立一个 java.lang.String 类,写上 static 代码块
public class String {
//
static{
System.out.println("我是自定义的String类的静态代码块");
}
}
2、在另外的程序中加载 String 类,看看加载的 String 类是 JDK 自带的 String 类,还是我们自己编写的 String 类
public class StringTest {
public static void main(String[] args) {
java.lang.String str = new java.lang.String();
System.out.println("hello,atguigu.com");
StringTest test = new StringTest();
System.out.println(test.getClass().getClassLoader());
}
}
输出结果:
hello,atguigu.com
sun.misc.Launcher$AppClassLoader@18b4aac2
程序并没有输出我们静态代码块中的内容,可见仍然加载的是 JDK 自带的 String 类。
把刚刚的类改一下
package java.lang;
public class String {
//
static{
System.out.println("我是自定义的String类的静态代码块");
}
//错误: 在类 java.lang.String 中找不到 main 方法
public static void main(String[] args) {
System.out.println("hello,String");
}
}
由于双亲委派机制一直找父类,所以最后找到了Bootstrap ClassLoader,Bootstrap ClassLoader找到的是 JDK 自带的 String 类,在那个String类中并没有 main() 方法,所以就报了上面的错误。
举例2
package java.lang;
public class ShkStart {
public static void main(String[] args) {
System.out.println("hello!");
}
}
输出结果:
java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)
at java.lang.ClassLoader.defineClass(ClassLoader.java:761)
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:335)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main"
Process finished with exit code 1
即使类名没有重复,也禁止使用java.lang这种包名。这是一种保护机制
举例3
当我们加载jdbc.jar 用于实现数据库连接的时候
- 我们现在程序中需要用到SPI接口,而SPI接口属于rt.jar包中Java核心api
- 然后使用双清委派机制,引导类加载器把rt.jar包加载进来,而rt.jar包中的SPI存在一些接口,接口我们就需要具体的实现类了
- 具体的实现类就涉及到了某些第三方的jar包了,比如我们加载SPI的实现类jdbc.jar包【首先我们需要知道的是 jdbc.jar是基于SPI接口进行实现的】
- 第三方的jar包中的类属于系统类加载器来加载
- 从这里面就可以看到SPI核心接口由引导类加载器来加载,SPI具体实现类由系统类加载器来加载
双亲委派机制优势
通过上面的例子,我们可以知道,双亲机制可以
-
避免类的重复加载
-
保护程序安全,防止核心API被随意篡改
- 自定义类:自定义java.lang.String 没有被加载。
- 自定义类:java.lang.ShkStart(报错:阻止创建 java.lang开头的类)
沙箱安全机制
- 自定义String类时:在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java.lang.String.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的String类。
- 这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
其他
如何判断两个class对象是否相同?
在JVM中表示两个class对象是否为同一个类存在两个必要条件:
- 类的完整类名必须一致,包括包名
- 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同
- 换句话说,在JVM中,即使这两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的
对类加载器的引用
- JVM必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的
- 如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中
- 当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的(后面讲)
运行期优化
分层编译
JVM 将执行状态分成了 5 个层次:
- 0层:解释执行,用解释器将字节码翻译为机器码
- 1层:使用 C1 即时编译器编译执行(不带 profiling)
- 2层:使用 C1 即时编译器编译执行(带基本的profiling)
- 3层:使用 C1 即时编译器编译执行(带完全的profiling)
- 4层:使用 C2 即时编译器编译执行
profiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的 回边次数】等
即时编译器(JIT)与解释器的区别
- 解释器
- 将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
- 是将字节码解释为针对所有平台都通用的机器码
- 即时编译器
- 将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需再编译
- 根据平台类型,生成平台特定的机器码
对于大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。 执行效率上简单比较一下 Interpreter < C1 < C2,总的目标是发现热点代码(hotspot名称的由 来),并优化这些热点代码
逃逸分析
逃逸分析(Escape Analysis)简单来讲就是,Java Hotspot 虚拟机可以分析新创建对象的使用范围,并决定是否在 Java 堆上分配内存的一项技术
逃逸分析的 JVM 参数如下:
- 开启逃逸分析:-XX:+DoEscapeAnalysis
- 关闭逃逸分析:-XX:-DoEscapeAnalysis
- 显示分析结果:-XX:+PrintEscapeAnalysis
逃逸分析技术在 Java SE 6u23+ 开始支持,并默认设置为启用状态,可以不用额外加这个参数
方法内联
C++是否为内联函数由自己决定,Java由编译器决定。Java不支持直接声明为内联函数的,如果想让他内联,你只能够向编译器提出请求: 关键字final修饰 用来指明那个函数是希望被JVM内联的,如
public final void doSomething() {
// to do something
}Copy
总的来说,一般的函数都不会被当做内联函数,只有声明了final后,编译器才会考虑是不是要把你的函数变成内联函数
JVM内建有许多运行时优化。首先短方法更利于JVM推断。流程更明显,作用域更短,副作用也更明显。
private static int square(final int i){
return i * i;
}
System.out.println(square(9));
如果发现sqaure是热点方法,并且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝、粘贴到调用者的位置。
System.out.println(9 * 9);
还能够进行常量折叠的优化
System.out.println(81);
字段优化
JMH 基准测试请参考: http://openjdk.java.net/projects/code-tools/jmh/
创建 maven 工程,添加依赖如下
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.21</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.21</version>
</dependency>
字段优化代码示例如下:
// 运行期优化 —— 字段优化
// 热身,先热身再优化
@Warmup(iterations = 5, time = 1)
// 5轮测试
@Measurement(iterations = 5, time = 1)
@State(Scope.Benchmark)
public class T03_RunTime_FieldOptimize {
int[] elements = randomInts(1_000);
private static int[] randomInts(int size) {
ThreadLocalRandom 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 < elements.length; i++) {
doSum(elements[i]);
}
}
@Benchmark
public void test3() {
for (int element : elements) {
doSum(element);
}
}
static int sum = 0;
@CompilerControl(CompilerControl.Mode.INLINE) // 控制调用方法时是不是要进行方法内联;允许内联
static void doSum(int x) { sum += x;}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(T03_RunTime_FieldOptimize.class.getSimpleName())
.forks(1)
.build();
new Runner(opt).run();
}
}
开启方法内联:CompilerControl.Mode.INLINE,每个方法2轮热身,5轮测试。结果如下(每秒吞吐量,分数越高的更好):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pOVJvQAe-1658117731575)(C:\Users\lenovo\Desktop\java学习笔记\jvm学习笔记二\字段优化零.png)]
接下来禁用 doSum 方法内联:CompilerControl.Mode.DONT_INLINE
@CompilerControl(CompilerControl.Mode.DONT_INLINE) // 控制调用方法时是不是要进行方法内联;
static void doSum(int x) { sum += x;}
关闭方法内联,每个方法2轮热身,5轮测试。如果如下:吞吐量都有一定程度的下降
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mPc2HvRt-1658117731576)(C:\Users\lenovo\Desktop\java学习笔记\jvm学习笔记二\字段优化一.png)]
分析:
在上述的示例中,doSum() 方法是否内联会影响 elements 成员变量读取的优化:
如果 doSum() 方法内联了,test1 方法会被优化成下面的样子(伪代码)
@Benchmark
public void test1() {
// elements.length 首次读取会缓存起来 -> int[] local
for (int i = 0; i < elements.length; i++) { // 后续 999 次 求长度 <- local
// doSum(elements[i]);
doSum(elements[i]); // 1000 次取下标 i 的元素 <- local
}
}
可以节省 1999 次 Field 字段读取操作
但如果 doSum() 方法没有内联,则不会进行上面的优化
本地变量访问长度、数据时,不需要去 class 元数据那里找,在本地变量就可以找到了,相当于手动优化。但是方法内联是由虚拟机来优化的。所以,test3 方法与test2 方法是等价的,test1 方法是运行期间优化了,test2 方法是手动优化了, test3 方法的 foreach 是 编译期间优化了。
反射优化
public class Demo7 {
public static void foo(){
System.out.println("foo...");
}
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {
Method foo = Demo7.class.getMethod("foo");
for(int i = 0 ;i <= 16;i++){
System.out.printf("%d\t",i);
foo.invoke(null);
}
System.in.read();
}
}
查看invoke源码
@CallerSensitive
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, obj, modifiers);
}
}
//MethodAccessor是一个接口,有3个实现类,其中有一个是抽象类
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rox67uHW-1658117731576)(C:\Users\lenovo\Desktop\java学习笔记\jvm学习笔记二\反射优化.png)]
会由第一个方法去调用NativeMethodAccessorImpl
NativeMethodAccessorImpl源码
class NativeMethodAccessorImpl extends MethodAccessorImpl {
private final Method method;
private DelegatingMethodAccessorImpl parent;
private int numInvocations;
NativeMethodAccessorImpl(Method var1) {
this.method = var1;
}
//每次进行反射调用,会让numInvocation与ReflectionFactory.inflationThreshold的值(15)进行比较,并使使得numInvocation的值加一
//如果numInvocation>ReflectionFactory.inflationThreshold,则会调用本地方法invoke0方法
public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
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);
}
默认阈值是15
- 一开始if条件不满足,就会调用本地方法invoke();
- 随着numInvocation的增大,当它大于ReflectionFactory.inflationThrehold的值16时,就会本地方法访问器替换为一个运行时动态生成的访问器,来提高效率。
这时会从反射调用变成正常调用。