类加载机制

image-20220115135430504

类加载的时机

在对象的创建过程中,完成了检查阶段

image-20220115141354723
  • 加载 (Loading)
  • 验证(Verification)
  • 准备(Preparation)
  • 解析(Resolution)
  • 初始化 (Initialization)
  • 使用(Using)
  • 卸载(Unloading)

其中验证、准备、解析三个部分统称为连接(Linking)

其中加载、验证、准备、解析、初始化和卸载这五个阶段的顺序是确定的,类型的加载过程必须按照这种顺序按部就班地开始。

而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始, 这是为了支持Java语言的运行时绑定特性(也称为动态绑定或晚期绑定)。

1
2
3
请注意,这里笔者写的是 按部就班地“开始”,而不是按部就班地“进行”或按部就班地“完成”,强调这点是因为这些阶段通常都 是互相交叉地混合进行的,会在一个阶段执行的过程中调用、激活另一个阶段。

加载阶段与连接阶段的部分动作(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段 尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的一部 分,这两个阶段的开始时间仍然保持着固定的先后顺序。

类加载的过程

第一个阶段——加载

  • 1)通过一个类的全限定名来获取定义此类的二进制字节流
  • 2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

img

第二个阶段——验证

验证是连接阶段的第一步,这一阶段的目的是保证Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全

验证内容主要包含如下四点:

  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证

第三个阶段——准备

准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存设置类变量初始值(对象创建过程的隐式创建)的阶段

假设一个类变量的定义为:

1
public static int value = 123;

那变量value在准备阶段过后的初始值为0而不是123,因为这时尚未开始执行任何Java方法,而把 value赋值为123的 putstatic 指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值 为123的动作要到类的初始化阶段才会被执行

第四个阶段——解析

解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程

第五个阶段——初始化

类的初始化阶段是类加载过程的最后一个步骤,初始化阶段就是执行类构造器<clinit>()方法的过程

<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作静态语句块(static{}块)中的语句合并产生的

类加载器

在类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。

而实现这个动作的代码被称为“类加载器”(Class Loader)

类与类加载器

类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远超类加载阶段

对于 任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。

这句话表达地更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等

双亲委派模型

站在Java虚拟机的角度来看,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap Class Loader),另外一种就是其他所有

的类加载器。

启动类加载器

1
2
3
4
5
Bootstrap Class Loader

这个类加载器使用C++语言实现,是虚拟机的一部分。

负责加载存放在<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够识别的类库

其他所有的类加载器

1
这些类加载器都由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader

扩展类加载器

1
2
3
4
5
6
7
Extension Class Loader

这个类加载器是在类sun.misc.Launcher$ExtClassLoader中以Java代码的形式实现的。

主要负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。

顾名思义,它是一种Java系统类库的扩展机制。用户可以将具有通用性的类库存放至ext目录里来扩展Java SE的功能

应用程序类加载器

1
2
3
4
5
Application Class Loader

这个类加载器是在类sun.misc.Launcher$AppClassLoader中以Java代码的形式实现的。

主要负责加载用户类路径上所有的类库

互相配合

1
JDK 9之前的Java应用都是由这三种类加载器互相配合来完成加载的,而各种类加载器之间的层次关系被称之为类加载器的“双亲委派模型”
img

工作流程

1
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。

优点

1
2
3
4
5
一个显而易见的好处就是Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系。

例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都能够保证是同一个类。

如果没有使用双亲委派模型,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为java.lang.Object的类,并放在程序的ClassPath中,那系统中就会出现多个不同的Object类,Java类型体系中最基础的行为也就无从保证,应用程序将会变得一片混乱。

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
// 首先,检查请求的类是否已经被加载过了
Class c = findLoadedClass(name);
if (c == null){
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e){
// 如果父类加载器抛出ClassNotFoundException
// 说明父类加载器无法完成加载请求
}
if (c == null){
// 在父类加载器无法加载时
// 再调用本身的findClass方法来进行类加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
}

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!