当前位置: 首页 > article >正文

Java中的类加载器

一、概述

类加载器.png
以 JDK 8 为例:

名称加载哪的类说明
BootstrapClassLoaderJAVA_HOME/jre/lib无法直接访问
Extension ClassLoaderJAVA_HOME/jre/lib/ext上级为 Bootstrap,显示为 null
Application ClassLoaderclasspath上级为 Extension
自定义类加载器自定义上级为 App
  • 启动类加载器(bootstrap class loader):由 C++ 实现的,没有对应的 Java 对象,因此在 Java 中只能用 null 来指代。负责加载最为基础、最为重要的类,比如存放在 JRE 的 lib 目录下 jar 包中的类(以及由虚拟机参数 -Xbootclasspath 指定的类)
  • 扩展类加载器(extension class loader):Java 核心类库提供。负责加载相对次要、但又通用的类,比如存放在 JRE 的 lib/ext 目录下 jar 包中的类(以及由系统变量 java.ext.dirs 指定的类)
  • 应用类加载器(application class loader):Java 核心类库提供。负责加载应用程序路径下的类。(这里的应用程序路径,是指虚拟机参数 -cp/-classpath、系统变量 java.class.path 或环境变量 CLASSPATH 所指定的路径。)默认情况下,应用程序中包含的类便是由应用类加载器加载的。
  • 自定义类加载器:除了由 Java 核心类库提供的类加载器外,可以加入自定义的类加载器,来实现特殊的加载方式。比如可以对 class 文件进行加密,加载时再利用自定义的类加载器对其解密。

除了加载功能之外,类加载器还提供了命名空间的作用。

在 Java 虚拟机中,类的唯一性是由类加载器实例以及类的全名一同确定的。即便是同一串字节流,经由不同的类加载器加载,也会得到两个不同的类。在大型应用中,往往借助这一特性,来运行同一个类的不同版本。

启动类加载器之外,其他的类加载器都是 java.lang.ClassLoader 的子类,因此有对应的 Java 对象。

二、启动类加载器

public class F {
    static {
        System.out.println("bootstrap F init");
    }
}
public class Load {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("com.hcx.jvm.load.F");
        System.out.println(aClass.getClassLoader()); // AppClassLoader  ExtClassLoader
    }
}
E:\git\jvm\out\production\jvm>java -Xbootclasspath/a:. com.hcx.jvm.load.Load
bootstrap F init
null
  • -Xbootclasspath 表示设置 bootclasspath
  • 其中 /a:. 表示将当前目录追加至 bootclasspath 之后
  • 可以用这个办法替换核心类
    • java -Xbootclasspath: (完全替换)
    • java -Xbootclasspath/a:<追加路径>(后追加)
    • java -Xbootclasspath/p:<追加路径>(前追加 可以替换)

三、扩展类加载器

public class G {
    static {
        System.out.println("classpath G init");
    }
}
/**
 * 演示 扩展类加载器
 * 在 C:\Program Files\Java\jdk1.8.0_91 下有一个 my.jar
 * 里面也有一个 G 的类,观察到底是哪个类被加载了
 */
public class Load {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("com.hcx.jvm.load.G");
        System.out.println(aClass.getClassLoader()); //sun.misc.Launcher$AppClassLoader@18b4aac2
    }
}

写一个同名的类

public class G {
    static {
       System.out.println("ext G init");
    }
}

打个 jar 包,将 jar 包拷贝到 JAVA_HOME/jre/lib/ext,重新执行 Load

ext G init
sun.misc.Launcher$ExtClassLoader@29453f44

四、双亲委派模型

指调用类加载器的 loadClass 方法时,查找类的规则

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // 1. 检查该类是否已经加载
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    // 2. 有上级的话,委派上级 loadClass
                    c = parent.loadClass(name, false);
                } else {
                    // 3. 如果没有上级了(ExtClassLoader),则委派 BootstrapClassLoader
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
            }
            if(c == null) {
                long t1 = System.nanoTime();
                // 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载
                c = findClass(name);
                // 5. 记录耗时
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if(resolve) {
            resolveClass(c);
        }
        return c;
    }
}
public class Load {
    public static void main(String[] args) throws ClassNotFoundException {
        System.out.println(Load.class.getClassLoader());
        Class<?> aClass = Load.class.getClassLoader().loadClass("com.hcx.jvm.load.H");
        System.out.println(aClass.getClassLoader());
    }
}

执行流程为:

  1. sun.misc.Launcher$AppClassLoader //1 处, 开始查看已加载的类,结果没有
  2. sun.misc.Launcher A p p C l a s s L o a d e r / / 2 处,委派上级 s u n . m i s c . L a u n c h e r AppClassLoader // 2 处,委派上级 sun.misc.Launcher AppClassLoader//2处,委派上级sun.misc.LauncherExtClassLoader.loadClass()
  3. sun.misc.Launcher$ExtClassLoader // 1 处,查看已加载的类,结果没有
  4. sun.misc.Launcher$ExtClassLoader // 3 处,没有上级了,则委派BootstrapClassLoader查找
  5. BootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 H 这个类,显然没有
  6. sun.misc.Launcher$ExtClassLoader // 4 处,调用自己的 findClass 方法,是在JAVA_HOME/jre/lib/ext 下找 H 这个类,显然没有,回到sun.misc.Launcher$AppClassLoader的 // 2 处
  7. 继续执行到 sun.misc.Launcher$AppClassLoader // 4 处,调用它自己的 findClass 方法,在classpath 下查找,找到了

一个类加载器在加载类时,先把这个请求委托给自己的父类加载器去执行,如果父类加载器还存在父类加载器,就继续向上委托,直到顶层的启动类加载器。如果父类加载器能够完成类加载,就成功返回,如果父类加载器无法完成加载,那么子加载器才会尝试自己去加载。

image.png

好处:双亲委派模可以避免类的重复加载,另外也避免了 Java 的核心 API 被篡改。

打破双亲委派:重写loadClass方法

    public static void main(String[] args) throws Exception {
        //类加载器加载的目录
        System.out.println("启动类加载器:"+System.getProperty("sun.boot.class.path")); // Bootstrap class loader:加载 %JAVA_HOME%/lib路径下的jar包
        System.out.println("扩展类加载器:"+System.getProperty("java.ext.dirs")); // Extension class loader:加载 %JAVA_HOME%/jre/lib/ext路径下的jar包
        System.out.println("应用类加载器:"+System.getProperty("java.class.path")); // Application class loader:加载CLASSPATH路径下指定
}

打印结果:

启动类加载器:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/sunrsasign.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/classes
扩展类加载器:/Users/hongcaixia/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java
应用类加载器:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/deploy.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/cldrdata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/dnsns.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/jaccess.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/jfxrt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/localedata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/nashorn.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/sunec.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/zipfs.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/javaws.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/jce.jar

自定义加载器:

public class MyCustomClassLoader extends ClassLoader{

    private String path;
    private String clName;

    public MyCustomClassLoader(String path, String clName) {
        this.path = path;
        this.clName = clName;
    }

    @Override
    public Class findClass(String name) {
        byte[] b = loadClassData(name);
        return defineClass(name, b, 0, b.length);
    }

    private byte[] loadClassData(String name) {
        name = path+name.replace(".","\\")+".class";
        InputStream in = null;
        ByteArrayOutputStream out = null;
        try {
            in = new FileInputStream(name);
            out = new ByteArrayOutputStream();
            int i = 0;
            while ((i=in.read())!=-1){
                out.write(i);
            }
        }catch (Exception e){

        }finally {
            //关闭流
        }
        return out.toByteArray();
    }
}

使用自定义加载器加载Person类

public static void main(String[] args) throws Exception {
        //自定义加载器
        MyCustomClassLoader myCustomClassLoader = new MyCustomClassLoader("/Users/hongcaixia/Documents/study/myjvm/src/main/java/com/hcx/jvm/cloader/", "MyClass");
        Class<?> personClazz = myCustomClassLoader.loadClass("com.hcx.jvm.cloader.Person");
        Object person = personClazz.newInstance();
        System.out.println(person.getClass().getClassLoader());//MyCustomClassLoader
        System.out.println(person.getClass().getClassLoader().getParent());//appClassLoader
        System.out.println(person.getClass().getClassLoader().getParent().getParent());//extClassLoader
        System.out.println(person.getClass().getClassLoader().getParent().getParent().getParent());//null 为null时自动调用bootstrap

    }
当把person.class放在classpath下时,父类加载器变成了appClassLoader;
当把person.class放在/jre/lib/ext下时,父类还是appClassLoader;因为ext下只能加载jar;打包成jar时才会直接是extClassLoader。

双亲委派加载:

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) {
                        // 父类不为空,就一直调用父类的loadClass方法
                        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
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

image.png

五、线程上下文类加载器

我们在使用 JDBC 时,都需要加载 Driver 驱动。
如果不写Class.forName("com.mysql.jdbc.Driver"),也是可以让 com.mysql.jdbc.Driver 正确加载的:

public class DriverManager {
    // 注册驱动的集合
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers
            = new CopyOnWriteArrayList<>();
    // 初始化驱动
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

DriverManager 的类加载器:在启动类路径下

System.out.println(DriverManager.class.getClassLoader()); //null

打印 null,表示它的类加载器是 Bootstrap ClassLoader,会到JAVA_HOME/jre/lib 下搜索类,但JAVA_HOME/jre/lib 下显然没有 mysql-connector-java-5.1.47.jar 包,这样问题来了,在DriverManager 的静态代码块中,怎么能正确加载 com.mysql.jdbc.Driver 呢?

loadInitialDrivers() 方法:

private static void loadInitialDrivers() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
        // If the driver is packaged as a Service Provider, load it.
        // Get all the drivers through the classloader
        // exposed as a java.sql.Driver.class service.
        // ServiceLoader.load() replaces the sun.misc.Providers()

        // 1.使用 ServiceLoader 机制加载驱动,即 SPI
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

                /* Load these drivers, so that they can be instantiated.
                 * It may be the case that the driver class may not be there
                 * i.e. there may be a packaged driver with the service class
                 * as implementation of java.sql.Driver but the actual class
                 * may be missing. In that case a java.util.ServiceConfigurationError
                 * will be thrown at runtime by the VM trying to locate
                 * and load the service.
                 *
                 * Adding a try catch block to catch those runtime errors
                 * if driver not available in classpath but it's
                 * packaged as service and that service is there in classpath.
                 */
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });

        println("DriverManager.initialize: jdbc.drivers = " + drivers);
        // 2.使用 jdbc.drivers 定义的驱动名加载驱动
        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }

2步骤发现它最后是使用 Class.forName 完成类的加载和初始化,关联的是应用程序类加载器,因此可以顺利完成类加载
1步骤它就是大名鼎鼎的 Service Provider Interface (SPI)
约定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名为文件,文件内容是实现类名称:
image.png
这样就可以使用:

ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class);
Iterator<接口类型> iter = allImpls.iterator();
while(iter.hasNext()) {
    iter.next();
}

来得到实现类,体现的是【面向接口编程+解耦】的思想,在下面一些框架中都运用了此思想:

  • JDBC
  • Servlet 初始化器
  • Spring 容器
  • Dubbo(对 SPI 进行了扩展)

ServiceLoader.load 方法:

public static <S> ServiceLoader<S> load(Class<S> service) {
    // 获取线程上下文类加载器
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器,它内部又是由Class.forName 调用了线程上下文类加载器完成类加载,具体代码在 ServiceLoader 的内部类LazyIterator 中:

private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
        c = Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
        fail(service,
                "Provider " + cn + " not found");
    }
    if (!service.isAssignableFrom(c)) {
        fail(service,
                "Provider " + cn  + " not a subtype");
    }
    try {
        S p = service.cast(c.newInstance());
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
        fail(service,
                "Provider " + cn + " could not be instantiated",
                x);
    }
    throw new Error();          // This cannot happen
}

六、自定义类加载器

什么时候需要自定义类加载器:

  • 想加载非 classpath 随意路径中的类文件
  • 都是通过接口来使用实现,希望解耦时,常用在框架设计
  • 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器

步骤:

  1. 继承 ClassLoader 父类
  2. 要遵从双亲委派机制,重写 findClass 方法
    注意不是重写 loadClass 方法,否则不会走双亲委派机制
  3. 读取类文件的字节码
  4. 调用父类的 defineClass 方法来加载类
  5. 使用者调用该类加载器的 loadClass 方法

准备好两个类文件放入 E:\myclasspath,它实现了 java.util.Map 接口,可以先反编译看一下

public class Load {
    public static void main(String[] args) throws Exception {
        MyClassLoader classLoader = new MyClassLoader();
        Class<?> c1 = classLoader.loadClass("MapImpl1");
        Class<?> c2 = classLoader.loadClass("MapImpl1");
        System.out.println(c1 == c2); //true

        MyClassLoader classLoader2 = new MyClassLoader();
        Class<?> c3 = classLoader2.loadClass("MapImpl1");
        System.out.println(c1 == c3); //false

        c1.newInstance();
    }
}

class MyClassLoader extends ClassLoader {

    @Override // name 就是类名称
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String path = "e:\\myclasspath\\" + name + ".class";

        try {
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            Files.copy(Paths.get(path), os);

            // 得到字节数组
            byte[] bytes = os.toByteArray();

            // byte[] -> *.class
            return defineClass(name, bytes, 0, bytes.length);

        } catch (IOException e) {
            e.printStackTrace();
            throw new ClassNotFoundException("类文件未找到", e);
        }
    }
}

http://www.kler.cn/a/407065.html

相关文章:

  • 原生JS和CSS,HTML实现开屏弹窗
  • Jenkins + gitee 自动触发项目拉取部署(Webhook配置)
  • Spring Security @PreAuthorize注解
  • 计算机网络:运输层 —— TCP 的超时重传机制
  • RT_Thread内核源码分析(三)——线程
  • Maven maven项目构建的生命周期 Maven安装配置 IDEA 配置 Maven
  • 【Linux课程学习】:命令行参数,环境变量
  • 【Python系列】 Base64 编码:使用`base64`模块
  • 爬虫实战:从HTTP请求获取数据解析社区
  • Vscode进行Java开发环境搭建
  • win10 禁止更新
  • 【大语言模型】ACL2024论文-17 VIDEO-CSR:面向视觉-语言模型的复杂视频摘要创建
  • React第七节 组件三大属性之 refs 的用法注意事项
  • java-排序算法汇总
  • 归并排序与逆序对问题(C语言版)
  • Spark RDD Checkpoint 数据的保存机制
  • VSCode打开c#项目报错:DotnetAcquisitionTimeoutError
  • CSS浮动:概念、特性与应用
  • Sonar Qube介绍和安装(三)
  • uni-app 认识条件编译,了解多端部署
  • 雷电模拟器charles代理抓包
  • 分层架构 IM 系统之 Entry 部署模式
  • 【线程】Java线程操作
  • 【论文笔记】LLaVA-KD: A Framework of Distilling Multimodal Large Language Models
  • 自动化测试用例编写详解
  • 机器学习杂笔记1:类型-数据集-效果评估-sklearn-机器学习算法分类