类的加载过程

一.概述:
Java中数据类型分为基本数据类型和引用数据类型,基本数据类型由Java虚拟机预先定义,引用数据类型则需要类的加载(类,接口,枚举,注解)

二.类的生命周期主要包括如下7个阶段:
加载(Loading) –> [{验证(Verification) –> 准备(Preparation) –> 解析(Resolution)}
链接(Linking)] –> 初始化(Initialization)–> 使用(Using)–> 卸载(Unloading)
其中验证,准备,解析3各部分统称为链接

三.类的加载:
简而言之就是将Java类的字节码文件加载到机器内存中,并在内存中构建出Java类的原型—类模板对象。所谓的类模板对象,其实就是Java类在JVM内存中的一个快照,JVM将从字节码中解析出的常量池,类字段,类方法等信息存储到类模板中,这样在JVM运行期间能够通过类模板获取到java类的任何信息和进行方法调用等操作,反射的机制即基于这一基础。如果JVM没有将java类的声明信息存储起来,则JVM运行期间无法进行反射。
类加载结束后的效果:堆中创建并存储该类对象实例数据,方法区(Hostport虚拟机–jdk7以前永久代–jdk8
元空间)中创建类该Class对象的引用并指向堆中该类对象的实例数据

四.链接—验证阶段:
当类加载到系统后,就开始连接操作,验证是链接的第一步。它的目的是保证加载的字节码是合法的,合理并符合规范的。验证的步骤比较复杂,大体上java虚拟机需要做以下步骤的检查:
格式检查(该检查其实在加载阶段已经完成)
语义检查
字节码验证
符号引用验证

五.链接—准备阶段:
简而言之就是为类的静态变量分配内存,并将其初始化为默认值。
注意:非final修饰的变量,在准备环节进行默认初始化赋值。 final修饰以后,在编译阶段进行初始化赋值,在准备环节直接进行显示赋值。如果使用字面量的方式定义一个字符串的常量的话,也是在准备环节直接进行显示赋值。

1
2
public static final String constStr = "CONST";  // 准备阶段显示赋值
public static final String constStr1 = new String("CONST"); // 准备阶段不会显示赋值

六.链接—解析阶段:
简而言之就是将类,接口,字段和方法的符号引用转为直接引用
注意:链接阶段中解析操作往往伴随着JVM在执行初始化之后才再执行

七.初始化:
简而言之就是为类的静态变量赋予正确的初始值,初始化阶段的重要工作是执行类的初始化方法及静态代码块。

八.使用static + final修饰的字段的显式赋值的操作,到底是在哪个阶段进行的赋值?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
情况1:在链接阶段的准备环节赋值
/*
* 情况2:在初始化阶段<clinit>()中赋值
*
* 结论:
* 在链接阶段的准备环节赋值的情况:
* 1. 对于基本数据类型的字段来说,如果使用static final修饰,则显式赋值(直接赋值常量,而非调用方法)通常是在链接阶段的准备环节进行
* 2. 对于String来说,如果使用字面量的方式赋值,使用static final修饰的话,则显式赋值通常是在链接阶段的准备环节进行
*
* 在初始化阶段<clinit>()中赋值的情况:
* 排除上述的在准备环节赋值的情况之外的情况。
*
* 最终结论:使用static + final修饰,且显示赋值中不涉及到方法或构造器调用的基本数据类型或String类型的显式赋值,是在链接阶段的准备环节进行。
*/
public class InitializationTest2 {
public static int a = 1;//在初始化阶段<clinit>()中赋值
public static final int INT_CONSTANT = 10;//在链接阶段的准备环节赋值

public static final Integer INTEGER_CONSTANT1 = Integer.valueOf(100);//在初始化阶段<clinit>()中赋值
public static Integer INTEGER_CONSTANT2 = Integer.valueOf(1000);//在初始化阶段<clinit>()中赋值

public static final String s0 = "helloworld0";//在链接阶段的准备环节赋值
public static final String s1 = new String("helloworld1");//在初始化阶段<clinit>()中赋值

public static String s2 = "helloworld2";

public static final int NUM1 = new Random().nextInt(10);//在初始化阶段<clinit>()中赋值
}

九.()线程安全问题:
虚拟机会保证一个类的()方法在多线程环境中被正确地加锁,同步,如果多个线程同时去初始化一个类,那么只会有一个线程去
执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。
但是需要注意以下会发生死锁的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 初始化线程A需要初始化B
class StaticA {
static {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
try {
Class.forName("com.atguigu.java1.StaticB");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println("StaticA init OK");
}
}
// 初始化线程B需要初始化A
class StaticB {
static {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
try {
Class.forName("com.atguigu.java1.StaticA");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println("StaticB init OK");
}
}

public class StaticDeadLockMain extends Thread {
private char flag;

public StaticDeadLockMain(char flag) {
this.flag = flag;
this.setName("Thread" + flag);
}

@Override
public void run() {
try {
Class.forName("com.atguigu.java1.Static" + flag);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println(getName() + " over");
}

public static void main(String[] args) throws InterruptedException {
// 线程A初始化A,持有A的锁,但是想要完成A的<clinit>()就需要获取B的锁完成B的<clinit>()
StaticDeadLockMain loadA = new StaticDeadLockMain('A');
loadA.start();
// 线程A初始化B,持有B的锁,但是想要完成B的<clinit>()就需要获取A的锁完成A的<clinit>()
StaticDeadLockMain loadB = new StaticDeadLockMain('B');
loadB.start();
}
}

十.类的主动使用:意味着会调用类的(),即执行了类的初始化阶段
1. 当创建一个类的实例时,比如使用new关键字,或者通过反射、克隆、反序列化。
2. 当调用类的静态方法时,即当使用了字节码invokestatic指令。
3. 当使用类、接口的静态字段时(final修饰特殊考虑),比如,使用getstatic或者putstatic指令。(对应访问变量、赋值变量操作)
4. 当使用java.lang.reflect包中的方法反射类的方法时。比如:Class.forName(“com.atguigu.java.Test”)
5. 当初始化子类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
6. 如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,该接口要在其之前被初始化。
7. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
8. 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。
(涉及解析REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄对应的类)
补充说明:
当Java虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口。
>在初始化一个类时,并不会先初始化它所实现的接口
>在初始化一个接口时,并不会先初始化它的父接口
因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态字段时,才会导致该接口的初始化。

十一.类的被动使用:即不会进行类的初始化操作,即不会调用()
1. 当访问一个静态字段时,只有真正声明这个字段的类才会被初始化。
> 当通过子类引用父类的静态变量,不会导致子类初始化
2. 通过数组定义类引用,不会触发此类的初始化
3. 引用常量不会触发此类或接口的初始化。因为常量在链接阶段就已经被显式赋值了。
4. 调用ClassLoader类的loadClass()方法加载一个类,并不是对类的主动使用,不会导致类的初始化。
说明:没有初始化的类,不意味着没有加载!