类加载

  • 了解 java 类加载机制

  • 理解类加载器 ClassLoader

类加载时机

  • 隐式加载 new 创建类的实例,

  • 显式加载:loaderClass,forName 等

  • 访问类的静态变量,或者为静态变量赋值

  • 调用类的静态方法

  • 使用反射方式创建某个类或者接口对象的 Class 对象。

  • 初始化某个类的子类

  • 直接使用 java.exe 命令来运行某个主类

类加载过程

类加载有五步:
加载 -> 验证 -> 准备 -> 解析 -> 初始化

加载

类加载过程的一个阶段,ClassLoader 通过一个类的完全限定名查找此类字节码文件,并利用字节码文件创建一个 class 对象

验证

目的在于确保 class 文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身的安全,主要包括四种验证:文件格式的验证,元数据的验证,字节码验证,符号引用验证

准备

为类变量(static 修饰的字段变量)分配内存并且设置该类变量的初始值,(如 static int i = 5 这里只是将 i 赋值为 0,在初始化的阶段再把 i 赋值为 5),
这里不包含 final 修饰的 static ,因为 final 在编译的时候就已经分配了。
这里不会为实例变量分配初始化,类变量会分配在方法区中,实例变量会随着对象分配到 Java 堆中

解析

这里主要的任务是把常量池中的符号引用替换成直接引用

初始化

这里是类记载的最后阶段,如果该类具有父类就进行对父类进行初始化,执行其静态初始化器(静态代码块)和静态初始化成员变量。
(前面已经对 static 初始化了默认值,这里我们对它进行赋值,成员变量也将被初始化)

forName 和 loaderClass 区别

  • Class.forName()得到的 class 是已经初始化完成的

  • ClassLoader.loaderClass 得到的 class 是还没有链接(验证,准备,解析三个过程被称为链接)的。

双亲委派机制

双亲委派模式要求除了顶层的启动类加载器之外,其余的类加载器都应该有自己的父类加载器,
但是在双亲委派模式中父子关系采取的并不是继承的关系,而是采用组合关系来复用父类加载器的相关代码。

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
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}

工作原理

通过 loadClass 加载类时,会先通过 findLoadClass 查找该类是否已加载,
如果未加载,会先通过父加载期加载,依次向上递归,到最上级父加载器加载,
如果此时还未加载,再依次向下递归调用 findClass

就是每个儿子都很懒,遇到类加载的活都给它爸爸干,直到爸爸说我也做不来的时候,儿子才会想办法自己去加载。

优势

采用双亲委派模式的好处就是 Java 类随着它的类加载器一起具备一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载,当父亲已经加载了该类的时候,就没有必要子类加载器(ClassLoader)再加载一次。其次是考虑到安全因素,Java 核心 API 中定义类型不会被随意替换,假设通过网路传递一个名为 java.lang.Integer 的类,通过双亲委派的的模式传递到启动类加载器,而启动类加载器在核心 Java API 发现这个名字类,发现该类已经被加载,并不会重新加载网络传递过来的 java.lang.Integer.而之际返回已经加载过的 Integer.class,这样便可以防止核心 API 库被随意篡改。可能你会想,如果我们在 calsspath 路径下自定义一个名为 java.lang.SingInteger?该类并不存在 java.lang 中,经过双亲委托模式,传递到启动类加载器中,由于父类加载器路径下并没有该类,所以不会加载,将反向委托给子类加载器,最终会通过系统类加载器加载该类,但是这样做是不允许的,因为 java.lang 是核心的 API 包,需要访问权限,强制加载将会报出如下异常。

1
java.lang.SecurityException:Prohibited package name: java.lang

类与加载器

在 JVM 中标识两个 Class 对象,是否是同一个对象存在的两个必要条件:

  • 类的完整类名必须一致,包括包名。
  • 加载这个 ClassLoader(指 ClassLoader 实例对象)必须相同。