注册
登录
类加载系统加载类时分为三个步骤,加载、链接、初始化,下面展开介绍。
类加载子系统结构图:

JVM 使用类加载器加载 class 文件,类加载器可分为引导类加载器和自定义类加载器两种。
引导类加载器(Bootstrap ClassLoader),有时也被称作启动类加载器或者零类加载器(Null ClassLoader),是 Java 虚拟机中最基础的类加载器之一。它的主要职责是加载 Java 核心类库。
自定义类加载器需要继承自 ClassLoader 类,JDK 默认提供了一些。比较重要的有两个,拓展类加载器(ExtClassLoader) 和应用类加载器(AppClassLoader) 。
下面展开说说这三个加载器的作用、区别以及联系。先看一张图:

特点:
$JAVA_HOME/jre/lib/ 或类似的位置加载 Java 核心类。ExtClassLoader)特点:
ExtClassLoader是在sun.misc.Launcher类里的静态内部类,继承自 ClassLoader 类,重写 loadClass() 方法。ExtClassLoader 主要负责加载位于 $JAVA_HOME/jre/lib/ext 目录下的扩展类库。ExtClassLoader 遵循 Java 类加载器的委托模型。当它收到一个类加载请求时,它首先会尝试使用其父类加载器(即 Bootstrap ClassLoader)来加载这个类。如果父类加载器无法加载,则 ExtClassLoader 会尝试自己加载。ExtClassLoader 位于 BootstrapClassLoader 之后,但在 AppClassLoader之前。这意味着它继承了 Bootstrap ClassLoader 的特性,并且它加载的类对 Application ClassLoader 可见。特点:
AppClassLoader也是在sun.misc.Launcher类里的静态内部类,继承自 ClassLoader 类,重写 loadClass() 方法。AppClassLoader主要负责的目录是当前应用程序的 classpath 所指定的路径,也就是说我们自己写的类默认都是通过AppClassLoader加载的。我们在IDEA里运行代码时,仔细观察控制台可以发现第一行通过-classpath指定了当前应用程序的class文件的目录AppClassLoader 遵循 Java 类加载器的委托模型。当它收到一个类加载请求时,它首先会尝试使用其父类加载器 ExtClassLoader来加载这个类。如果父类加载器无法加载,则 AppClassLoader 会尝试自己加载。AppClassLoader位于ExtClassLoader 之后。除了这些特点外,AppClassLoader还有一些别的用途:
原因一:前面我们介绍了引导类加载器、拓展类加载器、应用类加载器分别负责不同的路径下的class文件,但是并不是完全不相交的,比如-classpath除了指定当前应用程序的class文件目录外,也会指定$JAVA_HOME/jre/lib/目录下的某些 jar 包,所以要避免重复加载某些类。
原因二:如果我们的程序被黑客攻击了,比如黑客自己创建了一个java.lang的包,里面创建了一个名为String的类,把这个包和类植入我们正在运行的项目里,如果他的这个类被加载了,那我们项目里的String就会被篡改。
为了避免以上两种原因,我们要保证类只加载一次,并且保证越靠近 JVM 的类加载器优先级越高。这就是双亲委派干的事情!!!
原理:
引导类加载器、拓展类加载器、应用类加载器这三者之前有个关系,但又不是父子类关系,而是应用类加载器有个parent属性是拓展类加载器的对象。拓展类加载器的parent为空,所以会调用引导类加载器。我们观察ClassLoader的loaderClass()方法可以得出类的加载过程:


简单来说就是,
通过AppClassLoader加载class时会先用ExtClassLoader去加载这个类;
通过ExtClassLoader加载class时会先用BootStrapClassLoader去加载这个类;
好处:
class 加载完后会进行链接,分为三步:验证、准备、解析。
第一步是验证 class 文件是否正确,比如验证格式。
对 static 修饰的属性赋予一个默认值,但这一步不会赋初始值。
举个例子,class 里定义了一个static int a = 1,准备阶段会把 a 赋值为 0,在初始化阶段 a 才会 = 1。
将符号引用解析为直接引用。什么意思呢?
首先我们需要知道类被加载后是放到方法区的,每个类都是一个 Klass 对象(也可称为 Klass 结构)。
一般情况下我们都会在一个A类里使用到别的B类,使用方式是B类的全限定名,就只是一个字符串。但是JVM 实际在执行的时候需要从方法区中找到B类的 Klass 对象,解析的作用就是把这个名称字符串替换为实际的 Klass 对象内存地址。“符号引用”就是名称字符串、“直接引用”就是 Klass 对象内存地址。
初始化是类加载子系统的最后一个阶段,也是最为关键的阶段之一。下面详细介绍初始化阶段的内容及其重要性。
初始化阶段是类加载过程中的最后一个阶段,它负责执行类构造器 <clinit> 方法,并初始化类的静态变量。
初始化阶段的主要任务包括:
<clinit> 方法:<clinit> 方法是一个特殊的静态构造器,它负责对类进行初始化。每个类都有一个 <clinit> 方法,该方法在类第一次被初始化时由 JVM 自动生成并执行。<clinit> 方法中被赋值。<clinit> 方法的特点<clinit> 方法中的语句。<clinit> 方法是线程安全的,这意味着即使有多个线程同时初始化同一个类,也不会发生冲突。下面是一个简单的示例,展示类的初始化过程:
public class InitializationExample {
static {
System.out.println("执行静态初始化块。");
}
static int staticVar = initializeStaticVar();
private static int initializeStaticVar() {
System.out.println("初始化静态变量。");
return 10;
}
public static void main(String[] args) {
System.out.println("静态变量初始化为: " + staticVar);
}
}
输出如下:
执行静态初始化块。
初始化静态变量。
静态变量初始化为: 10
类的初始化通常在以下几种情况下触发:
java.lang.Class 或 java.lang.reflect 包中的方法来引用类时,如果这些方法会导致类的初始化,那么 JVM 会初始化该类。类的初始化顺序遵循一定的规则: