Java基础之类加载机制

105 阅读22分钟

类的加载概述

当我们进行Java开发时,其实整个流程可以分为以下几个步骤

  1. 编写Java源代码:

    • 我们使用文本编辑器或集成开发环境(IDE)编写Java源代码文件,其扩展名为.java。在这个阶段,你描述类、方法、变量等高级语言的结构。
  2. 编译Java源代码:

    • 然后我们使用Java编译器(javac命令)将.java源代码文件编译成字节码文件,其扩展名为.class。这个过程将高级语言翻译成与平台无关的中间形式,即字节码。
  3. 类加载:

    • 运行Java程序时,Java虚拟机负责将编译后的字节码文件加载到内存中。类加载器将.class文件加载到JVM,并完成连接和初始化的阶段。这时,类的结构信息被加载到虚拟机中。
  4. 执行程序:

    • 一旦类被加载到虚拟机中,程序就可以在Java虚拟机上执行。虚拟机通过解释执行字节码指令,或者通过即时编译等技术将字节码转换成本地机器码执行。
  5. 运行时操作:

    • Java程序在运行时与虚拟机进行交互,包括对象的创建、方法的调用、内存的管理等。程序的执行结果将反映在运行时的操作上。

整个流程中,其实我们最主要是关注前两个步骤,即编写Java源代码和编译Java源代码。其余步骤是由Java虚拟机负责的运行时过程。

简图如下:

1573872911665.png

类的加载、连接和初始化

类的加载、连接和初始化是Java虚拟机在运行时负责将类从字节码文件加载到内存并准备执行的过程的三个阶段:

  1. 加载(Loading):

    • 定义: 类加载的第一阶段是加载,它负责在运行时查找并加载类的字节码文件。
    • 执行: 加载阶段由类加载器完成,类加载器按照一定的委托关系逐级加载类。类加载器将字节码文件加载到内存,并为每个类创建一个java.lang.Class对象来表示该类。这个过程并不执行类的初始化。
  2. 连接(Linking):

    • 连接阶段包括三个子阶段:验证、准备和解析。
      • 验证(Verification): 确保加载的类符合Java语言规范,不会危害虚拟机的安全。验证阶段包括对字节码的各种静态检查。
      • 准备(Preparation): 为类的静态变量分配内存,并将其初始化为默认值。这个阶段不涉及到真正的初始化,只是为静态变量分配空间。
      • 解析(Resolution): 将类、接口、字段和方法的符号引用解析为直接引用。解析阶段将符号引用转换成可以直接定位内存地址的直接引用。
  3. 初始化(Initialization):

    • 初始化阶段是类加载的最后一个阶段,它负责执行类的初始化代码,包括执行静态变量的赋值和静态代码块的执行。初始化是类加载过程中真正开始执行字节码的阶段。

类的加载

  1. 通过类的完全限定名查找字节码文件:

    • 当需要使用某个类时,Java虚拟机的类加载器首先根据类的完全限定名(包括包名和类名)来查找对应的字节码文件。这个过程可以包括从本地文件系统、网络或其他来源加载类文件。
  2. 读入字节码文件到内存中:

    • 一旦找到了类的字节码文件,类加载器将这个文件的二进制数据读入到内存中。这将在运行时数据区的方法区内进行,方法区是用于存储类的结构信息、静态变量、常量池等的内存区域。
  3. 创建Class对象:

    • 类加载器在读入字节码文件后,会使用这些二进制数据创建一个java.lang.Class对象。这个Class对象是Java虚拟机中表示这个类的数据结构,它包含了类的结构信息,如类名、父类、接口、字段、方法等。
  4. 存放在堆区内:

    • Class对象被存放在堆区内。堆区是Java虚拟机用于存储对象实例和一些其他动态分配的内存的区域。Class对象封装了类在方法区内的数据结构,而方法区是堆的一部分,用于存储类的元信息。

总体而言,这个过程是由Java虚拟机的类加载器完成的,通过将类的字节码文件加载到内存中,并创建相应的Class对象,实现了在运行时获取类的信息和结构。这样的动态加载机制使得Java具有更强的灵活性和动态性。

连接

  1. 验证(Verification):

    • 定义: 在这个阶段,虚拟机对加载的字节码进行验证,以确保其符合Java语言规范,不会危害虚拟机自身的安全。验证过程包括对字节码的各种静态检查,如类型检查、访问权限等。
    • 目的: 验证阶段的目的是确保被加载的类是合法的,不包含不安全或不符合规范的代码。这有助于防止在运行时出现潜在的安全问题。
  2. 准备(Preparation):

    • 定义: 在准备阶段,虚拟机为类的静态变量分配内存,并将其初始化为默认值。这个阶段不涉及到真正的初始化,只是为静态变量分配空间。
    • 限制: 对于实例变量而言,此阶段不会分配内存。而对于final static修饰的变量,编译期间就会分配并设置初值。
  3. 解析(Resolution):

    • 定义: 解析阶段将类、接口、字段和方法的符号引用解析为直接引用。符号引用是一组符号来描述目标,而直接引用是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
    • 目的: 解析的目的是将类加载过程中产生的符号引用替换为可以直接定位到内存地址的直接引用。这样可以提高程序的执行效率,同时在运行期能够准确访问到目标。

总体而言,连接阶段的这三个子阶段确保了在类加载过程中对字节码进行验证、为静态变量分配空间并初始化、以及将符号引用解析为直接引用。这有助于确保被加载的类的正确性、安全性,并为后续的初始化阶段做好准备。

初始化

初始化阶段包括执行类的静态变量赋值和静态代码块,以及对成员变量的初始化

  1. 对父类进行初始化:
    • 如果一个类在其声明中有明确的父类(使用extends关键字),那么在初始化该类之前,必须先对其父类进行初始化。这确保了父类的静态代码块和静态变量赋值会在子类之前执行。
  2. 执行静态变量赋值和静态代码块:
    • 类的静态变量在这个阶段被赋予初值,静态代码块中的代码也被执行。这些代码只在类被加载时执行一次。
  3. 成员变量的初始化:
    • 静态成员变量和静态代码块执行完成后,接着进行实例成员变量和实例代码块的初始化。这些代码在每次创建类的实例时都会执行一次。

总体而言,初始化阶段确保了类的静态成员变量和静态代码块在类被使用之前得到正确的初始化。这也符合Java中继承关系的特性,保证了父类在子类之前得到初始化。

类加载器

  1. 类的加载由类加载器完成:

    • 类加载是指将类的字节码文件加载到内存中并创建对应的Class对象的过程。这个过程由类加载器负责执行。类加载器是Java虚拟机的一部分,负责从不同的来源加载类的字节码。
  2. Java虚拟机自带的类加载器:

    • 启动类加载器(Bootstrap Class Loader): 负责加载Java核心类库
    • 扩展类加载器(Extension Class Loader): 负责加载Java的扩展库
    • 系统类加载器(System Class Loader): 负责加载应用程序的类,是默认的类加载器,也叫应用类加载器。
  3. 用户自定义的类加载器:

    • 用户可以通过继承java.lang.ClassLoader类创建自定义的类加载器。自定义类加载器可以实现特定的加载策略,从不同的来源加载类,如网络、数据库等。这种灵活性允许开发者根据需要实现定制的类加载行为。

总体而言,Java虚拟机的类加载器体系包括自带的启动类加载器、扩展类加载器和系统类加载器,以及用户自定义的类加载器。这种分层结构允许不同的类加载器负责加载不同来源的类,使得Java应用程序能够灵活地处理不同的加载需求。

虚拟机内置加载器

根类加载器(Bootstrap)

  1. 根类加载器是最底层的类加载器:
    • 根类加载器是Java虚拟机的最底层的类加载器。它负责加载Java核心类库,即Java运行时环境的一部分,这些类库通常位于Java的安装目录下的jre/lib/rt.jar,我们日常常用的Java类库比如String,ArrayList等都位于该包内。
  2. 由C++语言实现:
    • 根类加载器的实现是用C++语言完成的,而不是Java。这使得根类加载器能够在虚拟机启动时就被初始化,因为它不依赖于Java类加载器的体系。
  3. 没有父加载器:
    • 根类加载器是类加载器体系中的起点,它没有父加载器。在Java类加载器的层次结构中,根加载器是顶层加载器,不具备父加载器,因为它在虚拟机启动时就存在了。
  4. 没有继承ClassLoader类:
    • 根类加载器并没有继承自java.lang.ClassLoader类。这是因为根类加载器是由C++语言实现的,而Java的类加载器通常继承自ClassLoader类。

总体而言,根类加载器是Java虚拟机中的最底层加载器,负责加载核心类库,它是由C++语言实现的,没有父加载器,也没有继承ClassLoader类。根类加载器在Java虚拟机启动时就被初始化,是整个类加载器体系的起点。

public static void main(String[] args) {
    ClassLoader cl = Object.class.getClassLoader(); //获取根类加载器
    System.out.println(cl);//根类加载器打印出来的结果是null,这是因为根类加载器是由虚拟机底层的C++代码实现的
}

扩展类加载器(Extension)

  1. 扩展类加载器的实现:

    • 扩展类加载器是由原SUN公司实现的,具体的类名为sun.misc.Launcher$ExtClassLoader(在JDK9之后是jdk.internal.loader.ClassLoaders$PlatformClassLoader)。这个类是由Java语言编写的,与根类加载器不同,它属于Java的类加载器体系。
  2. 父加载器是根类加载器:

    • 扩展类加载器的父加载器是根类加载器。这符合Java类加载器的双亲委派模型,其中扩展类加载器作为系统类加载器的父加载器,而系统类加载器的父加载器是根类加载器。
  3. 负责加载扩展目录下的类库:

    • 扩展类加载器的主要责任是加载扩展目录下的类库。这些类库通常位于<JAVA_HOME>\jre\lib\ext目录

总体而言,扩展类加载器是Java类加载器体系中的一个重要组成部分,负责加载扩展目录下的类库。它是由Java语言编写的,父加载器是根类加载器,采用双亲委派模型确保类加载的一致性和安全性。

public static void main(String[] args) {
    //DNSNameService类位于dnsns.jar包中,它存在于jre/lib/ext目录下
    ClassLoader cl = DNSNameService.class.getClassLoader();
    System.out.println(cl);//打印结果sun.misc.Launcher$ExtClassLoader
}

系统类加载器(System)

  1. 系统类加载器的实现:

    • 系统类加载器是由原SUN公司实现的,具体的类名为sun.misc.Launcher$AppClassLoader(在JDK9之后是jdk.internal.loader.ClassLoaders$AppClassLoader)。这个类是纯Java实现的,属于Java类加载器体系。
  2. 父加载器是扩展类加载器:

    • 系统类加载器的父加载器是扩展类加载器。
  3. 负责加载classpath中的类:

    • 系统类加载器的主要责任是从classpath环境变量或者系统属性java.class.path所指定的目录中加载类。它加载应用程序中的类,包括用户自定义的类和第三方库。
  4. 默认父加载器:

    • 系统类加载器是用户自定义的类加载器的默认父加载器。当用户没有明确指定类加载器时,系统类加载器会作为默认的加载器。
  5. 获取系统类加载器:

    • 一般情况下,通过ClassLoader.getSystemClassLoader()方法可以直接获取系统类加载器的实例。

总体而言,系统类加载器是Java类加载器体系中的应用类加载器,负责加载应用程序中的类。它是由Java语言编写的,父加载器是扩展类加载器,采用双亲委派模型确保加载的一致性和安全性。系统类加载器通常是程序中默认的类加载器。

public class ClassLoaderDemo {
    public static void main(String[] args) {
        //自己编写的类使用的类加载器
        ClassLoader classLoader = ClassLoaderDemo.class.getClassLoader();
        System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader
    }
}

小结

  1. 三种类加载器相互配合:

    • 在程序开发中,Java虚拟机使用启动类加载器、扩展类加载器和系统类加载器相互配合来加载类。不同的加载器负责加载不同来源的类,形成了类加载器体系,保证了类加载的一致性和安全性。
  2. 按需加载的方式:

    • Java虚拟机对class文件采用按需加载的方式。这意味着只有在需要使用某个类时,才会将该类的class文件加载到内存中生成对应的Class对象。这种方式可以提高程序的性能和节省内存。
  3. 双亲委派模式:

    • 类加载采用了双亲委派模式。当需要加载某个类的时候,Java虚拟机将加载请求委托给父加载器处理。父加载器首先尝试加载类,只有在父加载器无法完成加载时,才由当前加载器自己来尝试加载。这种模式确保了类加载的一致性,防止类被重复加载。
  4. 任务委派模式:

    • 双亲委派模式可以看作是一种任务委派模式。父加载器接收到加载请求后,将任务委派给父加载器处理,直到找到合适的加载器或者到达根加载器。这样可以保证类加载的有序性和层次性。

类加载器的双亲委派机制

  1. 父加载器关系:
    • 除了根类加载器,其他类加载器都有自己的唯一父加载器。这种父子关系通过组合模式来实现,而不是通过继承。每个类加载器都包含一个指向父加载器的引用。
  2. 双亲委派机制:
    • 类加载过程采用双亲委派机制,即当一个类加载器需要加载一个类时,它首先委托其父加载器尝试加载。只有在父加载器无法完成加载时,才由当前加载器自己尝试加载。这样的机制保护了Java程序的安全性,避免了类的重复加载。
  3. 懒加载机制:
    • 每个类加载器都是懒加载的,即在加载类时都会先让其父加载器尝试加载。这种懒加载机制确保了类的按需加载,节省了资源。

简图如下: 1573995835576.png

public class ClassLoaderDemo1 {
    public static void main(String[] args) throws Exception{
        //演示类加载器的父子关系
        ClassLoader loader = ClassLoaderDemo1.class.getClassLoader();
        while(loader!=null){
            System.out.println(loader);
            loader = loader.getParent();
        }
    }
}
运行结果:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1b6d3586

使用双亲委派机制的好处:

  1. 避免类的重复加载:
    • 双亲委派模式可以避免类的重复加载。当一个类加载器需要加载某个类时,它首先委托其父加载器尝试加载。如果父加载器已经加载了该类,就无需再由子加载器加载一次,从而避免了重复加载。这提高了加载的效率,同时确保了类的一致性。
  2. 考虑安全因素:
    • 双亲委派模式有助于确保Java核心API的安全性。由于核心API中定义的类型不会被随意替换,即使通过网络传递了一个名为java.lang.Object的类,通过双亲委派模式传递到启动类加载器,启动类加载器在核心Java API中发现已加载了这个类,就直接返回已加载过的Object.class,而不会重新加载。这有效地防止了核心API库被随意篡改,提高了Java程序的安全性。
//定义一个类,注意包名
package java.lang;

public class MyObject {

}
//加载该类
public static void main(String[] args) {
    Class clazz = MyObject.class;
    System.out.println(clazz.getClassLoader());
}
//输出结果
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang

​ 因为java.lang包属于核心包,只能由根类加载器进行加载,而根据类加载的双亲委派机制,根类加载器是加载不到这个MyObject类的(自定义的),所以只能由AppClassLoader进行加载,而这又不是允许的,所以会报出“Prohibited package name: java.lang”(禁止的包名)错误。

ClassLoader

所有的类加载器(除了根类加载器)都必须继承java.lang.ClassLoader。它是一个抽象类,主要方法如下:

loadClass

在ClassLoader的源码中,有一个方法loadClass(String name,boolean resolve),这里就是双亲委派模式的代码实现。从源码中我们可以观察到它的执行顺序。需要注意的是,只有父类加载器加载不到类时,会调用findClass方法进行类的查找,所以,在定义自己的类加载器时,不要覆盖掉该方法,而应该覆盖掉findClass方法。

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
{
    // 获取同步锁
    synchronized (getClassLoadingLock(name)) {
        // 首先检查类是否已加载
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 尝试从父加载器加载
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    // 如果没有父加载器,尝试从引导类加载器加载
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 如果父加载器加载失败,捕获ClassNotFoundException异常
            }

            if (c == null) {
                // 如果仍未找到类,调用findClass方法进行查找
                long t1 = System.nanoTime();
                c = findClass(name);

                // 记录类加载统计信息
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        // 如果resolve为true,解析类
        if (resolve) {
            resolveClass(c);
        }
        // 返回加载的类
        return c;
    }
}
  1. 同步块:
    • 整个方法被一个synchronized块包裹,通过调用getClassLoadingLock(name)方法获取用于同步的锁。这是为了确保在多线程环境中对类加载的安全控制。
  2. 检查是否已加载:
    • 首先,通过findLoadedClass(name)方法检查类是否已经被加载。如果已加载,直接返回已加载的Class对象。
  3. 尝试从父加载器加载:
    • 如果类尚未加载,尝试从父加载器加载,如果有父加载器。如果没有父加载器,则尝试从引导类加载器加载。
  4. 捕获ClassNotFoundException:
    • 如果加载过程中发生ClassNotFoundException异常,表示类未找到,会捕获该异常,不抛出。
  5. 调用findClass方法:
    • 如果仍未找到类,调用findClass(name)方法,该方法是ClassLoader的一个抽象方法,需要在子类中实现。这是实际进行类加载的步骤。
  6. 记录类加载统计信息:
    • 记录类加载的统计信息,包括父类加载器委派时间、查找类的时间以及总加载类的数量。
  7. 如果resolve为true,解析类:
    • 如果resolve参数为true,调用resolveClass(c)方法来确保类的正确性。这是可选的,可以根据需要选择是否进行解析。
  8. 返回加载的类:
    • 返回加载的Class对象。

findClass

一般而言,当我们要加载的类不在内置的三个类加载器(启动类加载器、扩展类加载器、应用类加载器)的加载范围内时,比如从网络、数据库或其他非标准位置加载类,那么才需要自定义类加载器并重写 findClass 方法。

protected Class<?> findClass(String name) throws ClassNotFoundException {
    // 默认实现抛出ClassNotFoundException异常
    throw new ClassNotFoundException(name);
}

defineClass

defineClass 方法的作用是将字节数组(包含类的字节码)转换为虚拟机能够识别的 Class 对象。这个方法通常与自定义类加载器中的 findClass 方法配合使用。

  1. 字节码转换: 接受一个字节数组,其中包含了类的字节码。这个字节数组通常是通过自定义类加载器的 findClass 方法获取的,该方法负责从特定位置加载类文件的字节码。

  2. 生成 Class 对象: 将字节码转换为虚拟机能够识别的 Class 对象。这个对象表示了加载的类,并可以在程序中被使用。

  3. ClassLoader 间的沟通: defineClass 方法是 ClassLoader 类的一个保护方法,允许子类(包括自定义的类加载器)将二进制数据转换为 Class 对象。这样,自定义类加载器可以在加载类时使用自己的逻辑。

在自定义类加载器中,我们会在 findClass 方法中获取类的字节码,然后调用 defineClass 方法生成对应的 Class 对象。这样,自定义类加载器就能够实现加载自定义位置的类文件,并将其转换为虚拟机可用的类。

protected final Class<?> defineClass(String name,byte[] b,int off,int len)
                              throws ClassFormatError

resolveClass

resolveClass 方法是 ClassLoader 类中的一个保护方法,其主要作用是连接加载的类,确保其在使用前完成初始化。

URLClassLoader

URLClassLoader 是 Java 提供的一个用于从指定的 URL 地址加载类的类加载器。它扩展了 ClassLoader,允许通过 URL 指定要加载的类所在的位置,包括本地文件系统或网络上的资源。

关于 URLClassLoader 的构造方法:

  1. public URLClassLoader(URL[] urls): 创建一个 URLClassLoader 实例,指定要加载的类所在的 URL 地址。父类加载器默认为系统类加载器。
  2. public URLClassLoader(URL[] urls, ClassLoader parent): 创建一个 URLClassLoader 实例,同样指定要加载的类所在的 URL 地址,并且可以指定父类加载器。

使用 URLClassLoader,可以实现动态加载远程或本地的类文件。这在一些动态加载模块、插件系统或者需要从网络上动态获取类的应用中非常有用。

案例1:加载磁盘上的类

public static void main(String[] args) throws Exception{
		File file = new File("d:/");
		URI uri = file.toURI();
		URL url = uri.toURL();
        URLClassLoader classLoader = new URLClassLoader(new URL[]{url});
        System.out.println(classLoader.getParent());
        Class aClass = classLoader.loadClass("com.test.Demo");
        Object obj = aClass.newInstance();
    }

package com.test;
public class Demo{
    static{
        System.out.println("Hello,World");
    }
}
  1. 通过 File 类获取目录路径,并将其转换为 URI
  2. URI 转换为 URL
  3. 创建 URLClassLoader 实例,指定要加载的类所在的 URL 地址为上述转换得到的 URL
  4. 输出 URLClassLoader 的父类加载器。
  5. 使用 loadClass 方法加载名为 "com.test.Demo" 的类。
  6. 创建该类的实例。
  7. 获取实例的类加载器,并输出。

输出结果如下

sun.misc.Launcher$AppClassLoader@18b4aac2
Hello,World
classLoader1 = java.net.URLClassLoader@677327b6

需要注意的是,loadClass 方法加载的类需要符合一定的条件,即该类需要在指定的 URL 地址中存在。而且,该类的依赖关系也需要在指定的路径中找到。

此外,使用 loadClass 方法加载类后,可以通过反射创建该类的实例,并获取实例的类加载器。在你的示例中,输出了实例的类加载器。

请确保目录 "d:/" 中存在 "com/test/Demo.class" 文件,以便成功加载类。如果加载失败,请检查类路径和文件的正确性。

自定义类加载器

一般情况下只需继承 ClassLoader 类,并覆盖 findClass 方法。在这个方法中,我们需要根据类的名称加载字节码,并使用defineClass 方法将其转换为 Class 对象。

自定义文件类加载器

public class MyFileClassLoader extends ClassLoader {
    //要加载的类所的文件目录
    private String directory;

    public MyFileClassLoader(String directory) {
        this.directory = directory;
    }

    public MyFileClassLoader(String directory, ClassLoader parent) {
        super(parent);
        this.directory = directory;
    }

    @Override
    protected Class<?> findClass(String name) {
        String path = directory + File.separator + name.replace(".", File.separator) + ".class";
        System.out.println(path);
        byte[] bytes;
        try {
            bytes = Files.readAllBytes(Paths.get(path));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return defineClass(name, bytes, 0, bytes.length);
    }

    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        MyFileClassLoader myFileClassLoader = new MyFileClassLoader("d:");
        Class<?> aClass = myFileClassLoader.loadClass("com.test.Demo");
        aClass.newInstance();

    }
}
  1. 成员变量:

    • directory: 用于指定要加载的类所在的文件目录。
  2. 构造方法:

    • MyFileClassLoader(String directory): 构造方法,接收一个参数 directory,用于设置要加载的类所在的文件目录。
    • MyFileClassLoader(String directory, ClassLoader parent): 构造方法,接收两个参数,分别是 directoryparent 类加载器。通过调用父类的构造方法来设置父类加载器。
  3. findClass 方法:

    • findClass(String name): 覆盖了 ClassLoaderfindClass 方法,用于实际查找和加载类的字节码。这里根据传入的类名,拼接出类文件的路径,然后读取类文件的字节码内容。
    • Files.readAllBytes(Paths.get(path)): 使用 NIO 的 Files 工具类读取类文件的字节码内容。
    • defineClass(name, bytes, 0, bytes.length): 调用 defineClass 方法将字节码转换成 Class 对象。这个方法是 ClassLoader 的 protected 方法,用于将字节数组转换为 Class 对象。在这里,它的作用是将通过文件读取得到的字节数组转换为 Class 对象。
  4. main 方法:

    • main 方法中,创建了一个 MyFileClassLoader 对象,指定了加载类的目录为 "d:"。
    • 调用 loadClass 方法加载类 "com.test.Demo",并通过 newInstance 方法创建类的实例。

类的显式与隐式加载

  1. 显式加载:

    • 通过显式的Java代码调用 ClassLoader 的方法来加载类,如 Class.forName(String name) 或者 this.getClass().getClassLoader().loadClass()
  2. 隐式加载:

    • 发生在虚拟机自动加载类的情境,通常是由于其他类的引用而触发。例如,当加载某个类时,如果该类引用了另外一个类的对象,那么这个对象的字节码文件就会被虚拟机自动加载到内存中。

总的来说,显式加载是由开发人员通过代码手动触发的,而隐式加载是由虚拟机在运行时根据类的引用关系自动完成的。

线程上下文类加载器

当涉及到Java的服务提供者接口(SPI)时,SPI是一套用于由第三方实现或扩展的API。这些SPI接口一般属于核心类库,存在于rt.jar包中,由根类加载器加载。然而,SPI的实现类通常作为第三方依赖jar包存放在classpath路径下。

在SPI中,接口类(如JDBC中的java.sql.Driver)由根类加载器加载。由于根类加载器无法直接加载classpath下的具体实现类,Bootstrap类加载器也不能反向委托AppClassLoader加载SPI的具体实现类。

为了解决这个问题,Java引入了线程上下文类加载器。线程上下文类加载器可以通过java.lang.ThreadgetContextClassLoader()获取,也可以通过setContextClassLoader(ClassLoader cl)来设置。如果没有手动设置上下文类加载器,线程将继承其父线程的上下文类加载器,初始线程的上下文类加载器是系统类加载器(AppClassLoader)。

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

public static <S> ServiceLoader<S> load(Class<S> service,
                                        ClassLoader loader)
{
    return new ServiceLoader<>(service, loader);
}

private ServiceLoader(Class<S> svc, ClassLoader cl) {
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    reload();
}

1574730595189.png