一、引言
二、类加载流程
1. 加载
2. 连接
2.1 验证
2.2 准备
2.3 解析
3. 初始化
三、类加载器
类加载器的类型
双亲委派模型
打破双亲委派模型
双亲委派模型优点
一、引言
在 Java 的运行机制中,类加载是一个至关重要的环节。它不仅决定了 Java 程序的动态性和灵活性,还为 Java 的安全性和稳定性提供了基础保障。类加载机制的核心在于类加载器(ClassLoader),它负责将字节码文件加载到 Java 虚拟机(JVM)中,并将其转换为可执行的类对象。这一过程不仅涉及到类的加载、解析和初始化,还通过双亲委派模型确保了类加载的安全性和一致性。
本文将深入探讨 Java 类加载的完整流程。同时,我将介绍双亲委派模型的原理及其优势,以及如何通过自定义类加载器打破这一模型以满足特定需求。希望读者能掌握如何在实际开发中利用这一机制实现灵活的类加载策略。
二、类加载流程
首先我们要知道类加载的流程,
类加载的过程主要分三步:加载 ——连接——初始化
而连接这一步又分了三步:验证——准备——解析
这里我们绘制了一个图片描述了类加载的流程。
1. 加载
这是类加载器的第一步
首先我们通过全限定名获取到当前类的二进制流;
然后我们把字节流代表的静态存储结构转换为方法区的运行时数据结构(类的结构信息,常量池,字段信息,静态变量等等)Classloader通过defineClass将字节流数据转化class对象。defineClass只是个入口,该过程是在JVM底层实现。
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}
2. 连接
2.1 验证
-
主要是为了确保字节流信息符合.class规范,Java的.class文件开头魔数是0xCAFEBABE,通过他就可以校验是否是Java文件了,满足了魔数还要判断以下内容
- 元数据:检查元数据是否符合语义,是否有重复字段?不允许继承的类?等等……
- 字节码验证:检查字节码是否符合规范,类型转化是否正确
- 符号引用验证:是否引用了不能别的类而权限不正确
2.2 准备
主要是为静态变量分配内存,并设置初始值,如果使用了final修饰还会直接为他附上初值
2.3 解析
把符号引用转换成直接引用的过程
3. 初始化
初始化是类加载的最后一步,执行了类的初始化代码<clinit>()(编译后自动生成)
包括静态变量的赋值,静态代码块,很经典的一道题就是父子类的代码块顺序问题,答案如下
-
父类的静态变量赋值。
-
父类的静态代码块执行。
-
当前类的静态变量赋值。
-
当前类的静态代码块执行。
类加载触发初始化的条件:
初始化阶段的执行是类加载机制的一部分,但并不是所有类加载都会触发初始化。根据 Java 规范,以下情况会触发类的初始化:
-
创建类的实例:
-
通过
new
关键字创建类的实例。 -
通过反射创建类的实例。
-
通过克隆(
clone
)创建类的实例。
-
-
调用静态方法或访问静态字段:
-
调用类的静态方法。
-
访问类的静态字段(除了通过
Class.forName
加载类但不执行初始化的情况)。
-
-
子类初始化:
-
如果子类初始化,则父类也会被初始化。
-
-
JVM启动时加载的类:
-
JVM 启动时加载的主类(如
public static void main(String[] args)
所在的类)。
-
三、类加载器
类加载器的主要功能就是加载字节码到JVM中去,他赋予了Java类动态加载到JVM并执行的能力。类加载器是一个负责加载类的对象。ClassLoader
是一个抽象类。给定类的二进制名称,类加载器应尝试定位或生成构成类定义的数据。典型的策略是将名称转换为文件名,然后从文件系统中读取该名称的“类文件”。每个 Java 类都有一个引用指向加载它的 ClassLoader
。
类加载器的类型
- BootstrapClassLoader:启动类加载器,最顶级加载器,没有父类,获取父类得到他的时候会返回null,是由c++实现的。
- ExtensionClassloader:扩展类加载器,加载jar包以及系统变量指定路径下的类
- AppClassLoader:应用程序类加载器,面向用户加载器,加载classpath下的所有jar包和类
我们还可以加入自定义类加载器,暂且不提。
双亲委派模型
既然我们有了这么多类加载器,那么我们使用的时候会如何选择呢?
这就提到了我们的双亲委派模型:每次查找的时候,每一级ClassLoader 都会把搜索类或者资源的任务委托给父类加载器(也是防止重复加载),如果父类加载器找不到再给到下一级
执行流程:这里我们贴上源码
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
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.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
可以看到我们首先通过本地方法findLoadedClass查找是否被加载过
如果没有被加载过再进入下一步,如果父类加载器不为空,就调用父类加载器的loadClass
如果没有父类加载器(parent == null
),则尝试通过根加载器加载类(通过 findBootstrapClassOrNull(name)
方法)。
如果父类加载器(或引导类加载器)无法加载类,则调用当前类加载器的 findClass(String name)
方法来加载类。
基本流程也就结束了
打破双亲委派模型
自定义加载器的话,需要继承 ClassLoader
。如果我们不想打破双亲委派模型,就重写 ClassLoader
类中的 findClass()
方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass()
方法。
双亲委派模型优点
避免了类的重复加载,父加载器加载过了子类不会加载。
保证了Java核心的API不被篡改,不然我自己写一个Object类JVM直接使用了,但是我有双亲委派,他会一直找到根加载器找到核心API
写到这里我们就了解了JVM的类加载机制已经双亲委派模型,希望各位大佬多多指教!