一个类的加载分为三个阶段
- 加载
- 连接
- 验证
- 准备
- 解析
- 初始化
加载
加载是类加载的第一个过程,请区分它们。
在加载阶段,主要将字节码加载到方法区中,并且在堆中生成一个.Class对象,这个.Class对象主要用来作为访问方法区数据的入口。
连接
验证
该阶段主要是验证我们生成字节码文件的正确性,防止生成的字节码不正确或者对JVM造成一定危害的字节码。
比如说,我们在程序中访问一个越界的数组、将一个对象转型为一个它并未实现的类或者我们恶意的去修改这个二进程字节码文件等,这些情况都会在验证阶段被发现。
准备
该过程主要是为类变量分配内存和初始值的过程。
- 为static变量设置初始值,注意,该阶段只是给静态变量一个默认值,比如下述的代码,在该阶段只会赋值一个默认值0给它,真正将10赋值是在初始化阶段。
public static int a = 10;
- static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 java_mirror(堆中.Class文件) 末尾。
在jdk1.7之前,静态变量都是存储到方法区的;在jdk7之后,静态变量被分配到堆中的.Class文件末尾。
- 如果该static对象是final类型的基本类型或者字符串常量,那么在该阶段就会进行赋值操作,比如以下两个对象,在准备阶段就会被赋值为10、abc。
public static final int a = 10; public static final String b = "abc";
- 如果该static对象是final类型的引用类型,那么它的赋值操作还是在初始化阶段完成的,比如下面的对象,在该阶段只会被分配一个默认值null,初始化阶段才会进行真的的赋值操作( new Persion() )。
public static final Person person = new Person();
解析
该阶段主要将常量池中的符号引用转换为直接引用的过程。
初始化
在之前的几个阶段中,几乎都是由虚拟机来完成的,而在初始化阶段,才真正去执行类中的程序代码(也就是字节码)。
在准备阶段,只是对一些static对象赋一个默认值,而在初始化阶段,才真正的对这些变量赋上其实际的值。
说白了,初始化阶段就是执行< cinit > ()V
的过程。
cinit () V
:
当类被初始化时,会执行这个方法。它会从上至下,将类中所有static变量和static代码块收集在一起,合并成一个< cinit > ()V
执行。
比如:
class Demo01 {
public static int a = 10;
static {
a = 20;
}
static {
a = 30;
}
}
反编译class,可以看到上述的static变量的赋值和静态代码块都会合并到一个方法中了。
从上往下的顺序。
触发初始化的时机
初始化是惰性的,类初始化发生的时机:
- main 方法所在的类,总会被首先初始化
- 首次访问这个类的静态变量或静态方法时
- 子类初始化,如果父类还没初始化,会引发
- 子类访问父类的静态变量,只会触发父类的初始化
- Class.forName
- new 会导致初始化
不会触发类初始化的情况:
- 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
- 类对象.class 不会触发初始化
- 创建该类的数组不会触发初始化,
Person[] person = new Person[1];
- 类加载器的 loadClass 方法
- Class.forName 的参数 2 为 false 时
< cinit >是线程安全的
类初始化的操作是线程安全的。
当有多个线程去同时初始化一个类的时候,会使用锁来确保初始化过程中的线程安全问题,当某一个线程初始化很慢的时候,后面的线程就会阻塞等待其初始化完毕。
例如:
public class Demo02 {
public static void main(String[] args) {
// 创建两个线程 同时初始化DeadLoopDemo类
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + ": start...");
DeadLoopDemo deadLoopDemo = new DeadLoopDemo();
}).start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + ": start...");
DeadLoopDemo deadLoopDemo = new DeadLoopDemo();
}).start();
}
}
class DeadLoopDemo {
static {
if (true) {
System.out.println("start to init DeadLoopDemo...");
while (true) {
// 使线程永远停在这执行 验证后面的线程初始化是否会阻塞
}
}
}
}
控制台输出,可以很清楚的看到,只有一个线程进行了初始化,还有一个线程处于阻塞状态,等待上一个线程初始化完毕。