JVM类加载过程大致可分为三个阶段:
- 加载
- 连接(验证、准备、解析)
- 初始化
加载阶段(注意和JVM类加载过程中的加载区分开!):
怎么进行类的加载?
从各个地方获取JVM字节码二进制字节流,例如zip文件中、网络中(典型的应用Web Applet)、运行时计算生成(动态代理技术)。。。
通过类加载器进行加载。
- 通过类的全限定名获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构。
- 在内存(堆)中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
为什么要进行加载?
将JVM字节码放入到JVM内存当中,以便后续操作的进行。
加载阶段相较于其它阶段,是程序员可控性最强的阶段。对于非数组类型的引用类型,我们既可以使用JVM定义好的类加载器(引导类、扩展类、系统类),还可以使用自定义的类加载器。
对于数组类型,本身是由虚拟机直接在内存中动态构造出来的,如果它的组件类型(去掉一个维度的类型)是引用类型的话,书上说的是,递归采用上述加载过程进行加载。我的理解是:再进行去维度,直到变成它的元素类型(指数组去掉所有维度后的类型)。最后,如果它的元素类型或组件类型不是引用类型的话(int、char...),那么数组将被标记为与引导类加载器关联(被引导类加载器加载);如果是引用类型的话,则被标识在加载该数组的类加载器的命名空间上。
加载阶段和连接阶段的部分动作(如文件格式验证)是交叉进行的。加载阶段尚未完成,连接阶段可能已经开始。
连接阶段又分为三个子阶段:验证、准备、解析。
验证:网络世界纷杂多样,为了保证虚拟机自身的安全性,必须要保证Class文件的字节流的规范。这个阶段非常重要。直接决定了JVM承受恶意代码攻击的能力。因此十分严谨,耗时也在类加载阶段中占了很大的比重。
验证阶段要完成以下大致4个阶段:文件格式验证、元数据验证、字节码验证、符号引用验证。这里不细说。
准备:初始化类变量,为其赋上默认值(int --> 0、char --> \u0000、引用类型 --> null)。
准备阶段会被赋值为0;
编译时 javac 会为m生成 ConstantValue属性,准备阶段虚拟机会根据 ConstantValue 将m赋值为2。
解析:大体来说,是将符号引用转换为直接引用。
符号引用:在Class文件中它以 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info 等类型的常量出现。
用一组符号来描述所引用的目标,可以是任意形式的字面量,与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到虚拟机内存当中。
直接引用:直接执行目标的指针、相对偏移量或者是间接定位到目标的句柄。比较抽象,不好解释。只需要知道:被直接引用指向的目标,必定已经在虚拟机中存在。
虚拟机可以根据自身需求判断,是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到符号引用要被使用时进行解析(目的是什么?)
对同一个符号引用进行多次解析请求是很常见的时候,除了invokedynamic之外,虚拟机实现可以对第一次解析结果进行缓存,譬如可以在运行时直接引用常量池中记录,并把常量标识为已解析状态,避免重复解析。
对于 invokedynamic 指令,本身就被定义为动态的,要等代码执行到那一个位置才会对其符号引用进行解析。
初始化:收集静态变量的赋值操作和静态代码块的内容放到<clinit>()方法中,执行。
注:
- 虚拟机在执行子类的初始化时,会先初始化其父类。
- 一个接口,如果没有default方法,即使是被实现了,实现类进行了初始化,接口也不会初始化。
public class ClassLoaderTest {
public static void main(String[] args) {
System.out.println(A.i);
}
}
class A extends B {
static int i = 0;
static {
System.out.println("A开始初始化");
}
}
class B implements C {
static {
System.out.println("B开始初始化");
}
}
interface C {
// default void m() {}
Runnable r = new Thread() {
{
System.out.println("C开始初始化");
}
};
}
运行结果:
将接口 C 中的default打开: