类的加载过程及注意点

  类的加载过程主要分为三个阶段:加载、链接(验证,准备,解析)、初始化。
  网上有很多关于这一块的介绍和概念,但是要么不准确,要么就不够具体。如果单从概念上看是很难理解的,本文更多的是解释每个步骤的相关概念以加深同学们的理解。
  整体过程如下:
alt
  先说一个java的命令,方便下面反编译看字节码文件:javap -v XXX.class,不仅会输出行号、本地变量表信息、反编译汇编代码,还会输出当前类用到的常量池等信息,。(也可以通过IDEA装插件的形式看,搜jclasslib)
  通过反编译看到的class文件中的常量池,加载到内存后叫运行时常量池。

一.加载

先看概念:
alt
关于第一点需要补充一下:
alt
关于第二点方法区具体的实现要看jdk版本(jdk7-永久代,jdk8-元空间)。
另外还需要知道生成大的class实例是在加载这个过程中出现的。

二.链接

链接又分为三个步骤:验证、准备、解析。还是先看概念,然后逐点解释:
alt

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
6
public 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 解析

主要是符号引用转直接引用的过程,了解即可。

三.初始化

还是先看概念,然后逐点解释:
alt
注:clinit=class init
clinit方法不是我们自己定义的,通过反编译可以看到:
alt

如何理解“顺序执行”呢?先看代码:

1
2
3
4
5
6
7
8
9
10
11
12
public 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
}
}

下图为上面代码反编译后的字节码文件:
alt
从字节码文件可以看出是顺序执行的。
如果代码里没有静态变量或者静态代码块,编译就不会有clinit方法。比如下图中的情况:
alt
所以可以看出类构造器方法clinit就是针对类变量的赋值和静态代码块的,如果没有就不会生成clinit方法。
还有个init方法其实就是类的构造器。
另外,需要注意的是下面的写法:
alt
前向引用可参考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站视频。

--------------本文结束,感谢您的阅读--------------
Brayden Wong wechat

如有任何问题欢迎加微信与我联系
坚持原创技术分享,您的支持将鼓励我继续创作!