开发者

Java类加载器ClassLoader详解

目录
  • 一、类的加载过程
  • 二、默认的类加载器
    • (一)Bootstrap ClassLoader1.基本介绍
    • (二)ExtClassLoader
    • (三)AppClassLoader
    • (四)类加载器的初始化
  • 三、双亲委派模式
    • (一)概念介绍
    • (二)源码解释
    • (三)双亲委派模式的核心优势
  • 四、动态加载
    • (一)URLClassLoader
    • (二)实战
    • (三)依赖问题
    • (四)版本冲突问题
  • 总结

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

    一、类的加载过程

    JDK8引用的都是jar包

    Java类加载器ClassLoader详解

    JDK11引用的都是model

    Java类加载器ClassLoader详解

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

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

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

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

    Java类加载器ClassLoader详解

    二、默认的类加载器

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

    • Bootstrap ClassLoader     启动类加载器
    • ExtClassLoader               扩展类加载器
    • AppClassLoader         &nbs编程客栈p;    系统类加载器(应用类加载器)

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

    (一)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

    Java类加载器ClassLoader详解

    代码演示:

    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);
        }
    }

    返回结果:

    Java类加载器ClassLoader详解

    为什么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类会去下面这些类中寻找:

    Java类加载器ClassLoader详解

    最后一行不是默认路径,是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>

    Java类加载器ClassLoader详解

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

    -Xbootclasspath/a:D:\test\commons-io-2.8.0.jar

    Java类加载器ClassLoader详解

    去这个包下寻找ByteOrderMark这个类

    Java类加载器ClassLoader详解

    修改代码:

    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);
        }
    }

    运行结果:

    Java类加载器ClassLoader详解

    Bootstrap ClassLoader在JDK核心类中找不到就会去指定的路径下寻找。

    (二)ExtClassLoader

    1.基本使用

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

    Java类加载器ClassLoader详解

    代码演示:

    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.getPrope编程rty("java.ext.dirs");
        // windows下使用;分隔
        // linux使用:分隔
        String[] arr = paths.split(";");
        for (String s : arr) {
            System.out.println(s);
        }
    }

    运行结果:

    Java类加载器ClassLoader详解

    sun.misc.Launcher$ExtClassLoader@383534aa

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

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

    Java类加载器ClassLoader详解

    Java类加载器ClassLoader详解

    2.配置指定路径

    -Djava.ext.dirs=D:\test

    Java类加载器ClassLoader详解

    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这个类。

    运行结果:

    Java类加载器ClassLoader详解

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

    (三)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");
        //javascript windows下使用;分隔
        // linux使用:分隔
        String[] arr = paths.split(";");
        for (String s : arr) {
            System.out.println(s);
        }
    }

    运行结果:

    Java类加载器ClassLoader详解

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

    Java类加载器ClassLoader详解

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

    Java类加载器ClassLoader详解

    (四)类加载器的初始化

    1.源码跟踪

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

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

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

    首先,创建 ExtClassLoader

    Java类加载器ClassLoader详解

    Java类加载器ClassLoader详解

    ExtClassLoader对象创建成功后,将器传入AppClassLoader中

    Java类加载器ClassLoader详解

    点进这个getAppClassLoader静态方法:

    Java类加载器ClassLoader详解

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

    由此可以看出:ExtClassLoader是AppClassLoader的父类加载器。

    Java类加载器ClassLoader详解

    2.ExtClassLoader和AppClassLoader的关系结论——父类加载器

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

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

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

    因此,AppClassLoader的父类是URLClassLoader;父类加载器是ExtClassLoader。

    Java类加载器ClassLoader详解

    Java类加载器ClassLoader详解

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

    Java类加载器ClassLoader详解

    三、双亲委派模式

    (一)概念介绍

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

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

    Java类加载器ClassLoader详解

    (二)源码解释

    加载一个类,一定先从 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;
    }

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

    • 安全性:确保核心类(如java.lang.Object)只能由启动类加载器加载,防止用户自定义类覆盖JDK核心类,避免恶意代码篡改。保证java核心api不会被随意替换,专业说法:沙箱安全机制。
    • 唯一性:同一名称的类只会被加载一次(由首个能加载它的类加载器完成),确保类实例的兼容性。
    • 层级隔离:不同类加载器加载的类空间隔离,例如Tomcat的多个Web应用可通过不同类加载器隔离类版本。

    代码示例:

    自定义一个Integer类:

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

    运行结果:

    Java类加载器ClassLoader详解

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

    四、动态加载

    (一)URLClassLoader

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

    常用的构造方法:

    • URLClassLoader(URL[] urls, ClassLoader parent):使用指定的父加载器创建对象,从指定的urls路径来查询、并加载类。
    • URLClassLoader(URL[] urls):使用默认的父加载器(AppClassLoader)创建一个ClassLoader对象,从指定的urls路径来查询、并加载类。

    (二)实战

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

    Java类加载器ClassLoader详解

    在另一个项目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);
        }
    }

    运行结果:

    Java类加载器ClassLoader详解

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

    Java类加载器ClassLoader详解

    RunDemo运行结果:

    Java类加载器ClassLoader详解

    (三)依赖问题

    有些时候,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中的代码,结果:

    Java类加载器ClassLoader详解

    问题分析:

    myUrlClassLoader的父类加载器是

    Java类加载器ClassLoader详解

    Java类加载器ClassLoader详解

    场景

    父类加载器

    原因

    默认构造URLClassLoader(urls)

    AppClassLoader

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

    显式指定父类加载器

    任意(如ExtClassLoader)

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

    继承关系

    URLClassLoader是父类

    AppClassLoader和ExtClassLoader是URLClassLoader的子类。

    根据双亲委派模式,myUrlClassLoader加载时parseExcel的时候会去加载JsonFactory,加载JsonFactory会交给父类加载器AppClassLoader,AppClassLoader无法加载就会交给其父类加载器ExtClassLoader,ExtClassLoader无法加载又交给自己python的父类加载器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);
        }
    }

    Java类加载器ClassLoader详解

    Java类加载器ClassLoader详解

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

    (四)版本冲突问题

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

    Java类加载器ClassLoader详解

    再次运行会报错:

    Java类加载器ClassLoader详解

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

    Java类加载器ClassLoader详解

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

    让程序同时运行两个版本的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\\python2.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);
        }
    }

    Java类加载器ClassLoader详解

    总结

    以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(www.devze.com)。

    0

    上一篇:

    下一篇:

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    最新开发

    开发排行榜