it编程 > 编程语言 > Java

Java类加载器ClassLoader详解

9人参与 2025-06-11 Java

以下文章都是基于jdk1.8环境

一、类的加载过程

jdk8引用的都是jar包

jdk11引用的都是model

我们编写的".java"文件需要通过javac编译成".class"文件,而程序运行时,jvm会把".class"文件加载到内存中,并创建对应的class对象,这个过程被称为类的加载。

简单来说:将class文件读入内存,并为之创建一个class对象。

jvm运行的是class文件,不单单是java语言,无论哪种语言编写的,只要文件是.class类型的,就能够在jvm上运行。

学习类加载器的目的:使用类加载器可以让我们得代码7*24小时,不间断地运行。即修改代码后无需重启就能生效,类似于热部署。

二、默认的类加载器

jvm通过类加载器把“.class”文件加载到内存中,默认有 3 个类加载器,分别是:

三个类加载器各有不同的作用

(一)bootstrap classloader1.基本介绍

作用:加载jdk核心类库(string,integer,long,arraylist等等)

方式:加载某个类时,在指定的路径中(jar包或文件夹)搜索这个类,如果搜到就加载,如果没搜到,报:classnotfoundexception

搜索路径:由 sun.boot.class.path 所指定的,比如:%jre_home%\jre\lib下的rt.jar、resources.jar、charsets.jar等,也就是jdk核心类库,其中 rt.jar 里面就存放着常用的 java api

代码演示:

public static void main(string[] args) {
    // 输出string类是被哪个类加载器,加载到内存中
    system.out.println(string.class.getclassloader());
    // 获取系统配置
    // bootstrap生效的时候会去sun.boot.class.path路径下找对应的类
    string paths = system.getproperty("sun.boot.class.path");
    // windows下使用;分隔
    // linux使用:分隔
    string[] arr = paths.split(";");
    for (string s : arr) {
        system.out.println(s);
    }
}

返回结果:

为什么string.class.getclassloader() 返回null?

原因是:bootstrap classloader是由c/c++编写的,是虚拟机的一部分,并不是java中的类,所以无法在 java 代码中获取它的引用,因此返回 null。

因此:如果一个类(system.out.println(string.class.getclassloader());)输出为null,说明该类是被bootstrap classloader加载的。前提是基于jdk1.8环境。

bootstrap classloader加载string类会去下面这些类中寻找:

最后一行不是默认路径,是idea默认添加的,红框部分才是默认加载路径。

string类在rt包下,按照上图的顺序依次向下找,在rt包中找到后,就不会继续向下寻找了。

2.自定义路径

可通过 -xbootclasspath 参数修改 bootstrap classloader 的搜索路径

用法

含义

备注

-xbootclasspath:路径

指定的路径会完全取代jdk核心的搜索路径

坚决不要用

-xbootclasspath/a:路径

指定的路径会在jdk核心类后搜索

可用

-xbootclasspath/p:路径

指定的路径会在jdk核心类前搜索

可用,不建议使用

注意:如果配置多个路径,linux/unix下用“:”分割,windows下用“;”分割。

代码演示:在 pom.xml 中添加 commons-io,同时也要把这个jar包放到d:\test 中

<dependency>
    <groupid>commons-io</groupid>
    <artifactid>commons-io</artifactid>
    <version>2.8.0</version>
</dependency>

添加jvm参数,指定寻找的包路径在jdk核心类后搜索

-xbootclasspath/a:d:\test\commons-io-2.8.0.jar

去这个包下寻找byteordermark这个类

修改代码:

public static void main(string[] args) {
    // 输出string类是被哪个类加载器,加载到内存中
    // system.out.println(string.class.getclassloader());
    system.out.println(byteordermark.class.getclassloader());
    // 获取系统配置
    // bootstrap生效的时候会去sun.boot.class.path路径下找对应的类
    string paths = system.getproperty("sun.boot.class.path");
    // windows下使用;分隔
    // linux使用:分隔
    string[] arr = paths.split(";");
    for (string s : arr) {
        system.out.println(s);
    }
}

运行结果:

bootstrap classloader在jdk核心类中找不到就会去指定的路径下寻找。

(二)extclassloader

1.基本使用

扩展类加载器,加载扩展类库,搜索路径由-djava.ext.dirs指定,比如:%jre_home%\jre\lib\ext目录下的jar包和class文件。即extclassloader加载类的时候会去下面的路径寻找,找不到就会报错。

代码演示:

public static void main(string[] args) {
    // 输出string类是被哪个类加载器,加载到内存中
    // system.out.println(string.class.getclassloader());
    // system.out.println(byteordermark.class.getclassloader());
    system.out.println(dnsnameservice.class.getclassloader());
    // 获取系统配置
    // 输出 extclassloader 扫描的路径,注意:输出的是文件夹
    string paths = system.getproperty("java.ext.dirs");
    // windows下使用;分隔
    // linux使用:分隔
    string[] arr = paths.split(";");
    for (string s : arr) {
        system.out.println(s);
    }
}

运行结果:

sun.misc.launcher$extclassloader@383534aa

有$符号表示extclassloader是launcher这个类的内部类。只要出现$就说明后面是前面的内部类

搜索extclassloader后,发现extclassloader确实是个内部类,并且还是静态的。

2.配置指定路径

-djava.ext.dirs=d:\test

public static void main(string[] args) {
    // 输出string类是被哪个类加载器,加载到内存中
    // system.out.println(string.class.getclassloader());
    system.out.println(byteordermark.class.getclassloader());
    // system.out.println(dnsnameservice.class.getclassloader());
    // 获取系统配置
    // 输出 extclassloader 扫描的路径,注意:输出的是文件夹
    string paths = system.getproperty("java.ext.dirs");
    // windows下使用;分隔
    // linux使用:分隔
    string[] arr = paths.split(";");
    for (string s : arr) {
        system.out.println(s);
    }
}

代码中表示,让extclassloader去d:\test文件夹下寻找byteordermark这个类。

运行结果:

自定义路径会把默认路径覆盖掉,所以这种方法不建议使用,了解即可。

(三)appclassloader

appclassloader也叫systemclassloader(系统类加载器),搜索路径由java.class.path(classpath) 指定。加载项目中自己写的类 第三方依赖包。

public static void main(string[] args) {
    // 输出string类是被哪个类加载器,加载到内存中
    // system.out.println(string.class.getclassloader());
    // system.out.println(byteordermark.class.getclassloader());
    // system.out.println(dnsnameservice.class.getclassloader());
    system.out.println(main.class.getclassloader());
 
    // 获取系统配置
    // 输出 extclassloader 扫描的路径,注意:输出的是文件夹
    string paths = system.getproperty("java.class.path");
    // windows下使用;分隔
    // linux使用:分隔
    string[] arr = paths.split(";");
    for (string s : arr) {
        system.out.println(s);
    }
}

运行结果:

最下面的两行是idea自带的,无需理会

由此可以看出,idea在运行java代码时,主动给我们添加了很多路径(jdk核心类库、扩展类库、自己写的类和第三方依赖包到classpath中),所以才会打印如此多的路径。如下图。

(四)类加载器的初始化

1.源码跟踪

从上面的输出可以发现 extclassloader、appclassloader都是 java 对象,接下来看一下它们是如何创建的。

这两个对象的生成都是在 launcher 中完成的:launcher类是 java 程序的入口,在启动 java 应用的时候会首先创建launcher类的对象,创建launcher类的时候会创建extclassloader、appclassloader。

launcher类太过于底层,所以无法打断点,构造方法如下:

首先,创建 extclassloader

extclassloader对象创建成功后,将器传入appclassloader中

点进这个getappclassloader静态方法:

super一直点到最上层,在这里,parent参数就是传入的 extclassloader。

由此可以看出:extclassloader是appclassloader的父类加载器。

2.extclassloader和appclassloader的关系结论——父类加载器

在 java 中,appclassloader 和 extclassloader 都是由 sun.misc.launcher 类创建的。尽管它们的名字中包含“classloader”,但它们并不是通过继承关系来定义父子关系的,而是通过设置父加载器的方式来实现的。这意味着 appclassloader 实际上是将 extclassloader 作为其父类加载器,而不是通过类继承的方式。

只有类之间才会存在继承关系,我们这里说的是appclassloader 和 extclassloader对象。

在 java 类加载器的上下文中,“父类加载器”这个术语并不意味着类加载器之间存在继承关系(即它们不是通过 extends 关键字定义的父子类关系),而是指类加载器之间的委派关系。具体来说,appclassloader 将 extclassloader 作为其父类加载器,指的是当 appclassloader 需要加载某个类时,它首先会请求 extclassloader(它的父类加载器)尝试加载该类。这种机制是基于“双亲委派模型”的。

因此,appclassloader的父类是urlclassloader;父类加载器是extclassloader。

通过debug可以看出来,appclassloader的parent属性是extclassloader,extclassloader的parent属性是bootstrapclassloader,前文提过,显示null,就说明是bootstrapclassloader。

三、双亲委派模式

(一)概念介绍

双亲委派模式是java类加载机制的核心原理,用于规范类加载器之间的协作方式。其核心思想是:当一个类加载器收到类加载请求时,不会直接自己加载,而是将请求委派给父类加载器,只有当父类加载器无法加载时,才会由当前类加载器自己尝试加载

如果父加载器可以完成加载任务,就成功返回;倘若父加载器无法完成此加载任务,子加载器才会尝试自己去加载。

(二)源码解释

加载一个类,一定先从 launcher.appclassloader 的 loadclass 方法开始。

protected class<?> loadclass(string name, boolean resolve) throws classnotfoundexception {
    // 1. 检查是否已加载
    class<?> c = findloadedclass(name);
    if (c == null) {
        try {
            // 2. 委派给父类加载器(递归向上)
            if (parent != null) {
                c = parent.loadclass(name, false);
            } else {
                c = findbootstrapclassornull(name);
            }
        } catch (classnotfoundexception e) {
            // 父类加载器未找到,继续执行
        }
        if (c == null) {
            // 3. 自己尝试加载
            c = findclass(name);
        }
    }
    if (resolve) {
        resolveclass(c);
    }
    return c;
}

(三)双亲委派模式的核心优势

代码示例:

自定义一个integer类:

package java.lang;
 
public class integer {
    public static void main(string[] args) {
        system.out.println("运行自定义的integer类......");
    }
}

运行结果:

原因是bootstrap classloader已经在核心类库中找到java.lang.integer类了,所以不会再加载自定义的java.lang.integer类了,这充分体现了jdk核心类不会被覆盖的优势。

四、动态加载

(一)urlclassloader

urlclassloader是java中用于从指定的url(统一资源定位符)加载类和资源的类加载器,属于java.lang.classloader的子类,也是extclassloader和appclassloader的父类。它允许从网络或本地文件系统中的目录、jar 文件等位置动态加载类,广泛应用于插件化开发、热部署、动态模块加载等场景。

常用的构造方法:

(二)实战

新建一个普通的java项目parse-excel-demo,并打成jar包。

在另一个项目classloader-demo中编写下面的代码,即可使用上面的方法

public class rundemo {
    public static void main(string[] args) throws exception {
        file file = new file(
                "g:\\develop\\workspace\\four\\" +
                        "newsmprojects\\parse-excel-demo\\target\\" +
                        "parse-excel-demo-1.0-snapshot.jar");
        url[] urls = {file.touri().tourl()};
        urlclassloader myurlclassloader = new urlclassloader(urls);
 
        class<?> parseexcel = myurlclassloader.loadclass("com.test.excel.parseexcel");
        object obj = parseexcel.newinstance();
        method parse = parseexcel.getmethod("parse");
        parse.invoke(obj);
    }
}

运行结果:

即使被引用的jar包内容被修改,只要路径正确,rundemo所在的项目都不需要重启就能运行:

rundemo运行结果:

(三)依赖问题

有些时候,parse-excel-demo中可能引用一些第三方库,比如:jackson-core-2.11.0.jar

<dependency>
  <groupid>com.fasterxml.jackson.core</groupid>
  <artifactid>jackson-core</artifactid>
  <version>2.11.0</version>
</dependency>

并在代码中使用:

package com.test.excel;
 
import com.fasterxml.jackson.core.jsonfactory;
 
public class parseexcel {
    public void parse() {
        system.out.println("开始解析excel......");
        jsonfactory jsonfactory = new jsonfactory();
        system.out.println("运行其getformatgeneratorfeatures方法:" +
                jsonfactory.getformatgeneratorfeatures());
    }
}

修改后重新对 parse 打包,然后运行clasterloader-demo中的代码,结果:

问题分析:

myurlclassloader的父类加载器是

场景

父类加载器

原因

默认构造urlclassloader(urls)

appclassloader

默认使用系统类加载器作为父类加载器。

显式指定父类加载器

任意(如extclassloader)

可通过构造方法urlclassloader(urls,parent)自定义。

继承关系

urlclassloader是父类

appclassloader和extclassloader是urlclassloader的子类。

根据双亲委派模式,myurlclassloader加载时parseexcel的时候会去加载jsonfactory,加载jsonfactory会交给父类加载器appclassloader,appclassloader无法加载就会交给其父类加载器extclassloader,extclassloader无法加载又交给自己的父类加载器bootstrap classloader,都找不到才会报错。

因此,当我们开发中碰到classnotfoundexception的排错方法要考虑类的加载过程。

解决方案:完善代码,将jsonfactory所属的包放在myurlclassloader的扫描路径下:

public class rundemo {
    public static void main(string[] args) throws exception {
        file file = new file(
                "g:\\develop\\workspace\\four\\" +
                        "newsmprojects\\parse-excel-demo\\target\\" +
                        "parse-excel-demo-1.0-snapshot.jar");
        file file2 = new file(
                "d:\\apache-maven-3.8.1\\repository\\" +
                        "com\\fasterxml\\jackson\\core\\jackson-core\\2.11.0" +
                        "\\jackson-core-2.11.0.jar");
        url[] urls = {file.touri().tourl(), file2.touri().tourl()};
        urlclassloader myurlclassloader = new urlclassloader(urls);
 
        class<?> parseexcel = myurlclassloader.loadclass("com.test.excel.parseexcel");
        object obj = parseexcel.newinstance();
        method parse = parseexcel.getmethod("parse");
        parse.invoke(obj);
    }
}

也可以将依赖放入classloader-demo中。appclassloader可以成功加载第三方依赖包。

(四)版本冲突问题

我们降低classloader-demo中的依赖版本:

再次运行会报错:

即使将2.11.0版本放到搜索路径中还是会报错,因为appclassloader已经加载完毕了,myurlclassloader就不会再加载了。

而项目中已有的依赖也不能更改,那么如何解决呢?

让程序同时运行两个版本的jsonfactory即可。

public class rundemo {
    public static void main(string[] args) throws exception {
        file file = new file(
                "g:\\develop\\workspace\\four\\" +
                        "newsmprojects\\parse-excel-demo\\target\\" +
                        "parse-excel-demo-1.0-snapshot.jar");
        file file2 = new file(
                "d:\\apache-maven-3.8.1\\repository\\" +
                        "com\\fasterxml\\jackson\\core\\jackson-core\\2.11.0" +
                        "\\jackson-core-2.11.0.jar");
        url[] urls = {file.touri().tourl(),file2.touri().tourl()};
        // 创建自定义类加载器
        // 这时候myurlclassloader的parent是extclassloader        
        urlclassloader myurlclassloader = new urlclassloader(urls,rundemo.class.getclassloader().getparent());
 
        class<?> parseexcel = myurlclassloader.loadclass("com.test.excel.parseexcel");
        object obj = parseexcel.newinstance();
        method parse = parseexcel.getmethod("parse");
        parse.invoke(obj);
    }
}

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。

(0)

您想发表意见!!点此发布评论

推荐阅读

Java中获取实时气象信息的3种方法

06-11

java的Stream流处理示例小结

06-11

java导出Echarts图表的示例代码(柱状图/饼形图/折线图)

06-11

java获取压缩文件中的XML并解析保存到数据库

06-11

Springboot项目由JDK8升级至JDK17详细教程

06-11

SpringBoot Java通过API的方式调用腾讯智能体(腾讯元宝)代码示例

06-11

猜你喜欢

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论