文章目录
- 一、什么是类加载
- 二、类加载的过程
-
- [1. 加载](#1. 加载)
- [2. 连接](#2. 连接)
-
- [2.1 验证](#2.1 验证)
- [2.2 准备](#2.2 准备)
- [2.3 解析](#2.3 解析)
- [3. 初始化](#3. 初始化)
- [三、代码示例 - 经典面试题](#三、代码示例 - 经典面试题)
- 四、总结
一、什么是类加载
类加载是指虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型的过程。
二、类加载的过程
类加载的过程可以分为三个主要阶段:加载 、连接 和初始化。其中连接阶段又可以细分为验证、准备和解析三个子阶段。
1. 加载
加载是类加载过程的第一个阶段,在这个阶段,虚拟机需要完成以下三件事:
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
2. 连接
2.1 验证
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证阶段主要包括四个检验过程:
文件格式验证:
- 是否以魔数0xCAFEBABE开头
- 主、次版本号是否在当前虚拟机处理范围之内
元数据验证:
- 这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)
- 这个类的父类是否继承了不允许被继承的类(被final修饰的类)
字节码验证:
- 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作
- 检查字节码指令是否正确
符号引用验证:
- 符号引用中通过字符串描述的全限定名是否能找到对应的类
- 符号引用中的类、字段、方法的访问性是否可被当前类访问
2.2 准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。这些变量所使用的内存都将在方法区中进行分配。
普通静态变量的处理:
java
public static int value = 123;
在准备阶段,变量value被分配内存并设置初始值为0,而不是123。把value赋值为123的动作将在初始化阶段才会执行。
static final变量的特殊处理:
java
// String常量 - 在准备阶段直接赋值
public static final String STRING_VALUE = "Hello World";
- 对于基本类型的static final变量,如果类字段的字段属性表中存在常量属性,那在准备阶段变量就会被初始化为常量属性所指定的值
java
// 即使是包装类型,如果不是编译时常量,也是在初始化阶段赋值
public static final Integer INTEGER_VALUE = new Integer(100);
public static final Long LONG_WRAPPER = Long.valueOf(200L);
- 对于引用类型的static final变量,即使被final修饰,在准备阶段仍然只会被设置为null,真正的初始化要等到初始化阶段
2.3 解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用: 符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
直接引用: 直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
3. 初始化
初始化阶段是类加载过程的最后一步,到了初始化阶段,才真正开始执行类中定义的Java程序代码。
在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源。
初始化阶段就是执行类构造器<clinit>()
方法的过程:
<clinit>()
方法的特点:
<clinit>()
方法与类的构造函数不同,它不需要显式地调用父类构造器- 虚拟机会保证在子类的
<clinit>()
方法执行之前,父类的<clinit>()
方法已经执行完毕 - 子类引用父类中的静态变量时,子类不会被初始化
三、代码示例 - 经典面试题
下面通过一个涉及父子类继承、静态代码块、构造代码块的经典面试题来观察类加载的完整过程:
java
// 父类
class Parent {
// 父类静态变量
public static String parentStaticField = "父类静态变量";
// 父类实例变量
public String parentField = "父类实例变量";
// 父类静态代码块
static {
System.out.println("1. 父类静态代码块执行");
}
// 父类构造代码块
{
System.out.println("3. 父类构造代码块执行");
}
// 父类构造方法
public Parent() {
System.out.println("4. 父类构造方法执行");
}
}
// 子类
class Child extends Parent {
// 子类静态变量
public static String childStaticField = "子类静态变量";
// 子类实例变量
public String childField = "子类实例变量";
// 子类静态代码块
static {
System.out.println("2. 子类静态代码块执行");
}
// 子类构造代码块
{
System.out.println("5. 子类构造代码块执行");
}
// 子类构造方法
public Child() {
System.out.println("6. 子类构造方法执行");
}
}
// 测试类
public class ClassLoadingTest {
public static void main(String[] args) {
Child child1 = new Child();
System.out.println("-----------------");
Child child2 = new Child();
}
}
运行结果:
java
1. 父类静态代码块执行
2. 子类静态代码块执行
3. 父类构造代码块执行
4. 父类构造方法执行
5. 子类构造代码块执行
6. 子类构造方法执行
-----------------
8. 父类构造代码块执行
9. 父类构造方法执行
10. 子类构造代码块执行
11. 子类构造方法执行
执行顺序分析:
- 第一次创建子类对象时的完整过程:
- 首先触发子类的类加载,但由于继承关系,需要先加载父类
- 父类加载:执行父类的静态代码块和静态变量初始化
- 子类加载:执行子类的静态代码块和静态变量初始化
- 创建对象实例:先调用父类构造过程,再调用子类构造过程
- 第二次创建子类对象时:
由于类已经加载完成,静态代码块不会再次执行,只执行对象实例化过程。
四、总结
类加载过程包含三个主要阶段:加载、连接和初始化。连接阶段又细分为验证、准备、解析三个子阶段。每个阶段都有其特定的职责:
- 加载阶段:获取类的二进制字节流,转换为运行时数据结构,生成Class对象
- 验证阶段:确保Class文件的字节流符合虚拟机要求,保证安全性
- 准备阶段:为类变量分配内存并设置零值
- 解析阶段:将符号引用替换为直接引用
- 初始化阶段:执行类构造器,真正开始执行Java程序代码