简介

从编译后的 class 文件到内存中的类,需要经过加载链接以及初始化三个步骤。链接需要经过验证,而内存中的类没有初始化,也无法使用。

加载 —— 双亲委派模型

在说明加载前,需要理解双亲委派模型是个啥玩意儿。

子-类加载器 需要加载某个 class 文件时,会将这个任务委派给他的 父-类加载器,然后递归这个操作,如果 父-类加载器 没有加载,则 子-类加载器 才会去加载。

注: 上面的类加载器的父子关系并不是 java 语言的继承关系,只是一种组合关系,或者说层级关系)

类的唯一性

类加载器名称 + 类全限定名称

类加载器的类别

BootstrapClassLoader(启动类加载器)

启动类加载器是用 C++ 实现的,没有对应的 Java 对象,因此在 Java 中只能用 NULL 来指代。相当于这个加载器是最顶级的加载器,不能直接通过引用进行操作。

该加载器加载 Java 的核心库 java.*,构造 ExtClassLoaderAppClassLoader

ExtClassLoader(标准扩展类加载器)

ext:extension

Java 编写,用于加载扩展库,比如 classpath 中的 jrejavax.* 或者 java.ext.dirs 指定位置中的类,可以直接调用该类加载器。

AppClassLoader(引用类加载器)

同样是用 Java 编写,加载程序所在的目录。

CustomClassLoader(用户自定义类加载器)

也是用 Java 编写的,用于加载指定路径的 class 文件。

以上的类加载的层级是:CustomClassLoader < AppClassLoader < ExtClassLoader < BootstrapClassLoader

网络资料:Java 9 引入了模块系统,并且略微更改了上述的类加载器。扩展类加载器被改名为平台类加载器(platform class loader)。Java SE 中除了少数几个关键模块,比如说 java.base 是由启动类加载器加载之外,其他的模块均由平台类加载器所加载

ClassLoader 源码分析

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
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 首先检查这个类是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 如果有父类加载器则让父类加载器加载该类
if (parent != null) {
c = parent.loadClass(name, false);
} else {
/**
* 让最顶级的 BootstrapClassLoader 加载
* 这里最终会调用一个 private native Class<?> findBootstrapClass(String name); 方法
* 这个方法用 native 标注了,说明是调用了 C++ 编写的系统级别的本地方法接口
* 说明 BootstrapClassLoader 的确无法直接通过引用调用
*/
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 真找不到就捕获异常,还能咋办
}

if (c == null) {
// 如果 BootstrapClassLoader 没找到就自己去找,递归回来
long t1 = System.nanoTime();
c = findClass(name);

// 这是定义类加载器,记录数据
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

流程图

JVM-双亲委派模型

链接

链接阶段主要分为三个阶段:

  • 验证

    验证的目的是确保被加载的类满足 JVM 的约束条件。

  • 准备

    为被加载类的静态字段分配内存,对于静态字段的初始化,会在初始化阶段中进行。

    如果字段被 final 修饰,在编译的时候会给字段添加 ConstantValue 属性,在准备阶段完成赋值。

  • 解析

    class 加载到 JVM 前,类中对于其他类、方法或者字段是不知道具体地址的,甚至不知道自己的方法和地址;所以编译器会给这些成员生成一个符号引用,符号引用放在 class 文件的常量池中,而解析阶段就是将这些符号引用解析成实际引用。

初始化

初始化一个静态字段,可以在声明该静态字段时就直接复制,也可以在静态代码块中对其赋值。

final 修饰的静态代码块在 Java 中被定义为常量,常量直接由 JVM 进行初始化;而常量以外的静态字段全部被编译器放置到一个名为 clinit 的方法中。

真正进入初始化后,会对常量字段进行赋值,执行 clinit 方法对静态字段或者静态代码块进行初始化。

对于类初始化的条件,JVM 枚举了一下触发条件:

  1. 虚拟机启动时,初始化指定的类;
  2. new 一个对象时,初始化目标类;
  3. 静态方法调用或者静态字段访问时会初始化目标类;
  4. 子类初始化会先初始化父类;
  5. 如果接口实现了 default 方法,直接或间接实现该接口的类初始化,该接口也会初始化;
  6. 使用反射时也会初始化被反射的类;
  7. new、getstatic、putstatic、invokestatic

(补充:类的初始化是线程安全的)