类的加载过程
加载
- 通过一个类的全限定名获取此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口
加载.class文件的方式
- 从本地系统中直接加载
- 通过网络获取
- 从zip压缩包中读取,成为日后jar,war格式的基础
- 运行时计算生成,使用最多的是:动态代理
- 有其他文件生成(JSP)
- 从数据库中提取
- 从加密文件中获取(防止反编译)
链接
验证
- 目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全
- 主要包括四种验证:文件格式验证,元数据验证,字节流验证,符号引用验证
准备
- 为类变量分配内存并且设置该类变量的默认初始值,即零值
- 这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化
- 这里不会为实例变量分配初始化,类变量会分配在方法区,而实例变量是会随着对象一起分配到Java堆中去
解析
- 将常量池内的符号引用转换为直接引用的过程
- 事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行
- 解析动作主要针对类或接口,字段,类方法,接口方法,方法类型等。对应常量池中的CONSTANT_Class_info等
初始化
- 初始化阶段就是执行类构造器方法<clinit>()的过程
- 此方法不需定义,就是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来
- 构造器方法中指令按语句在源文件中出现的顺序执行。
public class ClassInitTest {
private static int num = 1;
static {
num = 2;
number = 20;
System.out.println(number);// 会报错:非法的前向引用
}
private static int number = 10; // 链接的准备阶段:number=0 -->初始化<clinit>()按顺序依次:20 -->10
public static void main(String[] args) {
System.out.println(ClassInitTest.num);// 2
System.out.println(ClassInitTest.number); // 0
}
}
- <clinit>()不同于类的构造器(构造器是虚拟机反编译后的<init>()方法),当没有静态变量时或静态代码块,就不会存在
- 若该类具有父类,JVM会保证父类的<clinit>()执行完成后,再执行子类的<clinit>()
public class ClassInitTest {
static class Father {
public static int A = 1;
static {
A = 2;
}
}
static class Son extends Father {
public static int B = A
}
public static void main(String[] args) {
System.out.println(Son.B);// 2
}
}
- 虚拟机必须保证一个类的<clinit>()方法在多线程下被同步执行
public class ClassInitTest {
public static void main(String[] args) {
Runnable r = () -> {
System.out.println(Thread.currentThread().getName() + "开始");
DeadThread deadThread = 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() + "初始化DeadThread");
while (true) {
}
}
}
}
}
结果是只能输出一次 ‘初始化DeadThread’,如果初始化阻塞,会影响其他类无法使用该类