类的加载过程主要分为三个阶段:加载、链接(验证,准备,解析)、初始化。
网上有很多关于这一块的介绍和概念,但是要么不准确,要么就不够具体。如果单从概念上看是很难理解的,本文更多的是解释每个步骤的相关概念以加深同学们的理解。
整体过程如下:
先说一个java的命令,方便下面反编译看字节码文件:javap -v XXX.class,不仅会输出行号、本地变量表信息、反编译汇编代码,还会输出当前类用到的常量池等信息,。(也可以通过IDEA装插件的形式看,搜jclasslib)
通过反编译看到的class文件中的常量池,加载到内存后叫运行时常量池。
一.加载
先看概念:
关于第一点需要补充一下:
关于第二点方法区具体的实现要看jdk版本(jdk7-永久代,jdk8-元空间)。
另外还需要知道生成大的class实例是在加载这个过程中出现的。
二.链接
链接又分为三个步骤:验证、准备、解析。还是先看概念,然后逐点解释:
2.1 验证
目的是防止恶意修改或攻击。
举个例子就是:能够被java虚拟机识别的字节码文件都有一个专有标示,它的十六进制格式开头都有“CAFEBABE”,被称为Java class文件的魔数。
额外说明:十六进制(简写为hex或下标16)在数学中是一种逢16进1的进位制。只能用数字0到9和字母A到F(或a~f)表示,其中:A~F表示10~15,这些称作十六进制数字。例如123123->1e0f3。
再额外说明:在计算机领域,魔数有两个含义,一指用来判断文件类型的魔数;二指程序代码中的魔数,也称魔法值。所谓魔法值,是指在代码中直接出现的数值,只有在这个数值记述的那部分代码中才能明确了解其含义。例如阿里巴巴代码规约就会提示。
为什么是CAFEBABE呢?
网上猜测是Java一直以咖啡为代言,CAFEBABE可以认为是 Cafe Babe,读音上和Cafe Baby很近。所以这个也许就是代表Cafe Baby的意思。
2.2 准备
主要是设置类变量的默认初始值,例如:1
2
3
4
5
6public class A {
private static int a = 1;//准备阶段:a=0;----->初始化阶段a=1。
public static void main(String[] args) {
System.out.println(a);
}
}
数据类型不同,初始值不同。
final修饰的static变量编译的时候就分配了,准备阶段会显示初始化。
不会为实例变量分配初始化。
2.3 解析
主要是符号引用转直接引用的过程,了解即可。
三.初始化
还是先看概念,然后逐点解释:
注:clinit=class init
clinit方法不是我们自己定义的,通过反编译可以看到:
如何理解“顺序执行”呢?先看代码:1
2
3
4
5
6
7
8
9
10
11
12public class A {
private static int a = 1;//准备阶段:a=0;----->初始化阶段a=1。
static {
a = 2;
b = 20;
}
private static int b = 10;//为什么可以先赋值再定义?链接之准备阶段:b=0--> 初始化:20-->10
public static void main(String[] args) {
System.out.println(a);//2
System.out.println(b);//10
}
}
下图为上面代码反编译后的字节码文件:
从字节码文件可以看出是顺序执行的。
如果代码里没有静态变量或者静态代码块,编译就不会有clinit方法。比如下图中的情况:
所以可以看出类构造器方法clinit就是针对类变量的赋值和静态代码块的,如果没有就不会生成clinit方法。
还有个init方法其实就是类的构造器。
另外,需要注意的是下面的写法:
前向引用可参考oracle官方文档:
前向引用官方解释
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 public class A {
public static void main(String[] args) {
Runnable runnable = () -> {
System.out.println(Thread.currentThread().getName() + "开始");
DeadThread deadThread = new DeadThread();
System.out.println(Thread.currentThread().getName() + "开始");
};
Thread t1 = new Thread(runnable, "线程1");
Thread t2 = new Thread(runnable, "线程2");
t1.start();
t2.start();
}
}
class DeadThread {
static {
if (true) {
System.out.println(Thread.currentThread().getName() + "初始化当前类");
while (true) {
}
}
}
}
输出如下:
1
2
3线程2开始
线程1开始
线程2初始化当前类
部分代码和截图参考自B站视频。