类加载子系统

发布时间 2023-12-13 18:59:59作者: 变体精灵

类加载子系统负责从文件系统或者网络中加载字节码文件

类加载子系统整体架构图

可以看出,整个类加载子系统分为三个部分,加载、链接、初始化

一、加载

JVM 支持两种类型的类加载器,分别是引导类加载器(BootStrapClassLoader)和自定义加载器

从概念上来讲,自定义加载器一般指的是程序中由开发人员自定义的一类加载器,但是 JVM 规范中却不是这么定义的,而是将所有派生于抽象类 ClassLoader 的类加载器都划分为自定义类加载器

无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有 3 个

BootStrapClassLoader、ExtentionClassLoader、ApplicationClassLoader

public class ClassLoaderDemo {
    public static void main(String[] args) {
        // 获取用户自定义类加载器
        ClassLoader applicationClassLoader = ClassLoaderDemo.class.getClassLoader();
        System.out.println(applicationClassLoader);

        // 获取系统类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);

        // 获取系统类加载器的上层加载器(扩展类加载器)
        ClassLoader extentionClassLoader = systemClassLoader.getParent();
        System.out.println(extentionClassLoader);

        // 获取扩展类加载器的上层加载器(引导类加载器)
        ClassLoader bootStrapClassLoader = extentionClassLoader.getParent();
        System.out.println(bootStrapClassLoader);
    }
}

从打印结果可知,用户自定义类所使用的加载器就是系统类加载器 AppClassLoader

系统类加载器的上层加载器是 ExtClassLoader

由于 BootStrapClassLoader 不是 Java 语言实现的,所以这里输出为 null

BootStrapClassLoader
BootStrapClassLoader 称为启动类加载器或者引导类加载器,这个类加载器使用 C/C++ 实现,嵌套在 JVM 内部,它用来加载 Java 的核心类库
${JAVA_HOME/jre/lib/rt.jar
${JAVA_HOME/jre/lib/resources.jar
sun.boot.class.path 路径下的 jar 包
出于安全考虑,启动类加载器只加载包名为 java、javax、sun 等开头的类

ExtentionClassLoader
该加载器由 Java 语言编写,从 java.ext.dirs 系统属性所指定的目录中加载类,或从 ${JAVA_HOME}/jre/lib/ext 加载类,如果用户创建的 jar 存放在此目录下,也会自动由扩展类加载器进行加载

ApplicationClassLoader
该加载器由 Java 语言编写,它负责加载 classpath 下的类库,它是程序中默认的类加载器,一般来说,我们编写的类都是由它来完成加载的

public class ClassLoaderDemo {
    public static void main(String[] args) {
        System.out.println("系统类加载器加载路径");
        URL[] bootStrapUrls = Launcher.getBootstrapClassPath().getURLs();
        for (URL urL : bootStrapUrls) {
            System.out.println(urL.toExternalForm());
        }
        System.out.println("扩展类加载器加载路径");
        String extDirs = System.getProperty("java.ext.dirs");
        for (String extPath : extDirs.split(",")) {
            System.out.println(extPath);
        }
    }
}

双亲委派机制

如果一个类加载器收到类加载请求,它并不会自己先去加载,而是把这个请求委托给上级的加载器去执行加载任务,如果它的上级加载器还有上级,那么进一步向上委托,直至达到最顶层的 BootStrapClassLoader,如果上级类加载器可以完成加载任务就直接成功返回,如果上级类加载器无法完成加载任务,下级加载器才会尝试自己去加载,这就是双亲委派模式

例如: 自定义一个类 ClassLoaderDemo,我要去使用这个类就必然要先去加载它,通过上帝视角我们知道这个类是通过 AppClassLoader 去加载的,但是实际上 AppClassLoader 并不会先去加载 ClassLoaderDemo 这个类,而是委托给上级的 ExtClassLoader 去加载,而 ExtClassLoader 还有上级 BootStrapClassLoader,那么继续向上委托直到 BootStrapClassLoader 位置,由于 BootStrapClassLoader  加载不到 ClassLoaderDemo 这个类,那么 EExtClassLoader 就尝试去加载,而 ExtClassLoader 也加载不要,AppClassLoader 才尝试去加载

双亲委派机制的引入是为了 避免类的重复加载,防止恶意篡改核心 API,保护程序安全

二、链接

链接阶段又分为三个小阶段
验证阶段
确保字节码文件的字节流符合当前 JVM 规范,保证被加载的类的正确性,不会危害虚拟机自身安全
主要包括四种验证:文件格式验证、元数据验证、字节码验证、符号引用验证

准备阶段
为类变量分配内存空间并设置默认初始值
注意这里不包含 final 修饰的类变量,因为 final 修饰的类变量在编译期就确定了

解析阶段
将常量池内的符号引用转换为直接引用

三、初始化

初始化阶段就是执行 <clinit>() 方法的过程,此方法不需要定义,是 javac 编译器自动收集类中的所有类变量的赋值动作和静态代码块的语句合并而来的

<clinit()>() 不同于类的构造器,类构造器在 JVM 视角下对应的是 <init>() 方法

若该类已经具有父类,JVM 会保证子类的 <clinit>() 执行前,父类的 <clinit>() 已经执行完毕

虚拟机必须保证一个类的 <clinit>() 方法在多线程下被同步加锁

示例代码

public class ClassLoaderDemo {
    static {
        int a = 10;
    }

    public static int b = 20;

    public static int c = 30;

    static {
        int d = 40;
    }
}

根据上图可知,一个 ClassLoaderDemo 中的所有静态变量和静态代码块都被 java 编译器收集起来了,收集到了一个 <clinit>() 方法中,代码中静态变量和静态代码块的书写顺序就是实际的收集顺序

ps: <init>() 方法对应的是类的构造器、构造代码块...

上面代码中的 public static int b = 20 整个初始化过程如下

首先在链接的准备阶段进行默认初始化 (b = 0),然后在初始化阶段才完成显示初始化 (b = 20)