【类加载器】java类加载器

article/2025/9/18 7:12:08

类装载器ClassLoader(一个抽象类)

描述一下JVM加载class文件的原理机制
类装载器就是寻找类或接口字节码文件进行解析并构造JVM内部对象表示的组件,在java中类装载器把一个类装入JVM,经过以下步骤:

1、装载:查找和导入Class文件
2、链接:其中解析步骤是可以选择的 (a)检查:检查载入的class文件数据的正确性 (b)准备:给类的静态变量分配存储空间 (c)解析:将符号引用转成直接引用
3、初始化:对静态变量,静态代码块执行初始化工作
在这里插入图片描述

类装载工作由ClassLoder和其子类负责。JVM在运行时会产生三个ClassLoader:

  • BootstrapClassloader根/启动类装载器
    根装载器不是ClassLoader的子类,由C++编写,因此在java中看不到他,负责装载JRE的核心类库,即<JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,如JRE目录下的rt.jar,charsets.jar等,出于安全考虑,Bootstrap启动类加载器其实只加载包名为java、javax、sun等开头的类。
  • ExtClassLoader扩展类装载器
    ExtClassLoader是ClassLoder的子类,加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定路径中的类库,即JRE扩展目录ext下的jar类包;
  • 系统类加载器AppClassLoader
    加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径。通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。
    在这里插入图片描述
    这三个类装载器存在父子层级关系,但注意双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码

Java装载类使用“全盘负责委托机制”。“全盘负责”是指当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,该类所依赖及引用的类也由这个ClassLoder载入;

“委托机制”是指先委托父类装载器寻找目标类,只有在找不到的情况下才从自己的类路径中查找并装载目标类。这一点是从安全方面考虑的,试想如果一个人写了一个恶意的基础类(如java.lang.String)并加载到JVM将会引起严重的后果,但有了全盘负责制,java.lang.String永远是由根装载器来装载,避免以上情况发生。

除了JVM默认的三个ClassLoder以外,第三方可以编写自己的类装载器,以实现一些特殊的需求。类文件被装载解析后,在JVM中都有一个对应的java.lang.Class对象,提供了类结构信息的描述。数组,枚举及基本数据类型,甚至void都拥有对应的Class对象。

Class类没有public的构造方法,Class对象是在装载类时由JVM通过调用类装载器中的defineClass()方法自动构造的。

为什么要使用这种双亲委托模式呢?

因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。
考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时被加载,所以用户自定义类是无法加载一个自定义的ClassLoader。
思考:假如我们自己写了一个java.lang.String的类,我们是否可以替换调JDK本身的类?
答案是否定的。我们不能实现。为什么呢?我看很多网上解释是说双亲委托机制解决这个问题,其实不是非常的准确。因为双亲委托机制是可以打破的,你完全可以自己写一个classLoader来加载自己写的java.lang.String类,但是你会发现也不会加载成功,*具体就是因为针对java.开头的类,jvm的实现中已经保证了必须由bootstrp来加载。

如果我们在classpath路径下自定义一个名为java.lang.SingleInterge类(该类是胡编的),该类并不存在java.lang中,经过双亲委托模式,传递到启动类加载器中,由于父类加载器路径下并没有该类,所以不会加载,将反向委托给子类加载器加载,最终会通过系统类加载器加载该类。但是这样做是不允许,因为java.lang是核心API包,需要访问权限,强制加载将会报出如下异常

java.lang.SecurityException: Prohibited package name: java.lang

ClassLoader方法介绍

为了能够完全掌控类的加载过程,需要自定义类加载器,且需要从ClassLoader继承。下面来介绍一下ClassLoader类中和热替换有关的一些重要方法。

protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// 先从缓存查找该class对象,找到就不用重新加载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 thrown if class not found// from the non-null parent class loader}if (c == null) {// If still not found, then invoke findClass in order// 如果都没有找到,则通过自定义实现的findClass去查找并加载c = findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {//是否需要在加载时进行解析resolveClass(c);}return c;}}
  • loadClass()
    加载类的入口方法,调用该方法完成类的显式加载。通过对该方法的重写,可以完全控制和管理类的加载过程。执行loadClass方法,只是单纯的把类加载到内存,并不是对类的主动使用,不会引起类的初始化。
    从loadClass实现也可以知道如果不想重新定义加载类的规则,也没有复杂的逻辑,只想在运行时加载自己指定的类,那么我们可以直接使用this.getClass().getClassLoder.loadClass(“className”),这样就可以直接调用ClassLoader的loadClass方法获取到class对象。

  • findLoadedClass()
    该方法会在对应加载器的名字空间中寻找指定的类是否已存在,如果存在就返回给类的引用,否则就返回null。每个类加载器都维护有自己的一份已加载类名字空间,其中不能出现两个同名的类。凡是通过该类加载器加载的类,无论是直接的还是间接的,都保存在自己的名字空间中,这里的直接是指,存在于该类加载器的加载路径上并由该加载器完成加载,间接是指,由该类加载器把类的加载工作委托给其他类加载器完成类的实际加载。

命名空间由加载器和所有的父加载器所加载的类构成
在同一个命名空间中,不可能出现类名相同的两个类
在不同的命名空间中,可能出现类名相同的两个类(类名指类全称)
由子加载器加载的类能看见父加载器加载的类,反之不可以;(比如java.lang.String类,我们自己写的类肯定能看见,但是父加载器肯定看不见我们自己定义的类)
如果两个加载器之间没有直接或者间接的父子关系,那么两个加载器加载的类是相互不可见的

关于该方法的一些说明

  1. 如果某个类是因为代码中主动(显示)加载的,即是用ClassLoader(CL)加载,那么只有加载该类的classLoader(即调用自己的defineClass方法) 调用findLoadedClass方法才能找到,其它的类加载器调用此方法返回null。
  2. 如果某个类是被动(隐式)加载的,比如classA(由classLoaderA加载),classB(由cLB加载)clB是clA的父加载器,A是B的子类(或者classA依赖引用classB的这种关系)。 现在在代码中主动调用cLA去加载classA,而初始化一个子类会去判断父类是否加载,所以cLA会去加载classB,而cLA会委派给其父类加载器cLB去加载,cLB会委派到后面的父类去省略…,反正cLB会加载成功classB,
    这是时候虽然classB是由cLB加载成功的,但是调用cLA的findLoadedClass也是能够找到classB的(在cLB加载成功后,即第二次进入cLA的loadClass的findLoadedClass方法时),因为B类是被动加载,是由cLA加载classA引发的,虽然classB最终不是cLA加载的,但是cLA会被标记为B类的初始类加载器。 cLB是classB的定义类加载器
  3. Class的getClassLoader方法返回的是该类的定义类加载器,不是初始类加载器。
  4. 某个Class引用的其他类也会由该Class的getClassLoader方法返回的类加载器加载,因而也是由该Class的定义类加载器发起加载。
  5. 主动调用ClassLoader的loadClass加载一个classA,但该ClassLoader并不是classA的定义类加载器,那么后面该ClassLoader的findLoadedClass方法对classA总是回返回null;如果该ClassLoader是classA的定义类加载器,则findLoadedClass会返回;
  6. 如果是JVM自己根据类加载机制加载的ClassA(被动加载),那么classA的初始类加载器(initiating loader)和classA的定义类加载器(defining loader),它们的findLoadedClass均会返回被加载的Class;
  • findClass(String)
    在JDK1.2之前,在自定义类加载时,总会去继承ClassLoader类并重写loadClass方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中,从前面的分析可知,findClass()方法是在loadClass()方法中被调用的,当loadClass()方法中父加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式。需要注意的是ClassLoader类中并没有实现findClass()方法的具体代码逻辑,取而代之的是抛出ClassNotFoundException异常,同时应该知道的是findClass方法通常是和defineClass方法一起使用的(稍后会分析),ClassLoader类中findClass()方法源码如下:
//直接抛出异常
protected Class<?> findClass(String name) throws ClassNotFoundException {throw new ClassNotFoundException(name);
}
  • defineClass()
    该方法接收以字节数组表示的类字节码,并把它解析成JVM能够识别的Class对象(ClassLoader中已有该方法的一种实现),该方法转换一个类的同时,会先要求装载该类的父类以及实现的接口类。
    需要注意的是,如果直接调用defineClass()方法生成类的Class对象,这个类的Class对象并没有解析(也可以理解为链接阶段,毕竟解析是链接的最后一步),其解析操作需要等待初始化阶段进行。
    通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化class对象,如通过网络接收一个类的字节码,然后转换为byte字节流创建对应的Class对象,defineClass()方法通常与findClass()方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象,简单例子如下:
protected Class<?> findClass(String name) throws ClassNotFoundException {// 获取类的字节数组byte[] classData = getClassData(name);  if (classData == null) {throw new ClassNotFoundException();} else {//使用defineClass生成class对象return defineClass(name, classData, 0, classData.length);}}
  • resolveClass()
    使用该方法可以使用类的Class对象创建完成也同时被解析。前面我们说链接阶段主要是对字节码进行验证,为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用。

另外一些关键方法:

  • getSystemClassLoader()
    该方法返回系统使用的 ClassLoader。可以在自定义的类加载器中通过该方法把一部分工作转交给系统类加载器去处理。

类加载器间关系

上述为几个重要的类加载器方法。 一般我们更多的使用其子类:URLClassLoader,依赖关系如下图所示:
已经提供了findClass()、findResource()等方法的实现,这样我们在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类。如果我们在定义类加载器时选择继承ClassLoader类而非URLClassLoader,则必须手动编写findclass()方法的加载逻辑以及获取字节码流的逻辑。
在这里插入图片描述

拓展类加载器ExtClassLoader和系统类加载器AppClassLoader,这两个类都继承自URLClassLoader,是sun.misc.Launcher的静态内部类。sun.misc.Launcher主要被系统用于启动主应用程序,其类主要类结构如下:
在这里插入图片描述
ExtClassLoader并没有重写loadClass()方法,这足矣说明其遵循双亲委派模式,
AppClassLoader重载了loadClass()方法,但是最终调用的还是父类loadClass()方法,因此依然遵守双亲委派模式。

从sun.misc.Launcher中的各自的构造方法调用处可知:
启动类加载器,由C++实现,没有父类。
拓展类加载器(ExtClassLoader),由Java语言实现,父类加载器为null
系统类加载器(AppClassLoader),由Java语言实现,父类加载器为ExtClassLoader

自定义类加载器,父类加载器肯定为AppClassLoader。但其可以通过重写loadClass()方法破坏双亲委派模式。
因为继承自ClassLoader,可以参考ClassLoader抽象类的构造方法,其中parent设置为sun.misc.Launcher.getLauncher()的getClassLoader(),即为系统类加载器AppClassLoader。

public Launcher() {// 首先创建拓展类加载器ClassLoader extcl;try {extcl = ExtClassLoader.getExtClassLoader();} catch (IOException e) {throw new InternalError("Could not create extension class loader");}// Now create the class loader to use to launch the applicationtry {//再创建AppClassLoader并把extcl作为父加载器传递给AppClassLoaderloader = AppClassLoader.getAppClassLoader(extcl);} catch (IOException e) {throw new InternalError("Could not create application class loader");}//**设置线程上下文类加载器,稍后分析**Thread.currentThread().setContextClassLoader(loader);//省略其他没必要的代码......}}

显然Lancher初始化时首先会创建ExtClassLoader类加载器,然后再创建AppClassLoader并把ExtClassLoader传递给它作为父类加载器,这里还把AppClassLoader默认设置为线程上下文类加载器。另外,在创建ExtClassLoader强制设置了其父加载器为null。

//自定义ClassLoader,完整代码稍后分析
class FileClassLoader extends  ClassLoader{private String rootDir;public FileClassLoader(String rootDir) {this.rootDir = rootDir;}// 编写获取类的字节码并创建class对象的逻辑@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {//...省略逻辑代码}//编写读取字节流的方法private byte[] getClassData(String className) {// 读取类文件的字节//省略代码....}
}public class ClassLoaderTest {public static void main(String[] args) throws ClassNotFoundException {FileClassLoader loader1 = new FileClassLoader(rootDir);//抽象类ClassLoader的无参构造方法,其中的parent就是appCLassloaderSystem.out.println("自定义类加载器的父加载器: "+loader1.getParent());System.out.println("系统默认的AppClassLoader: "+ClassLoader.getSystemClassLoader());System.out.println("AppClassLoader的父类加载器: "+ClassLoader.getSystemClassLoader().getParent());System.out.println("ExtClassLoader的父类加载器: "+ClassLoader.getSystemClassLoader().getParent().getParent());/**输出结果:自定义类加载器的父加载器: sun.misc.Launcher$AppClassLoader@29453f44系统默认的AppClassLoader: sun.misc.Launcher$AppClassLoader@29453f44AppClassLoader的父类加载器: sun.misc.Launcher$ExtClassLoader@6f94fa3eExtClassLoader的父类加载器: null*/}
}

类与类加载器

在JVM中表示两个class对象是否为同一个类对象存在两个必要条件

  • 类的完整类名必须一致,包括包名。
  • 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。

即,在JVM中,即使这个两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的,这是因为不同的ClassLoader实例对象都拥有不同的独立的类名称空间,所以加载的class对象也会存在不同的类名空间中,但前提是覆写loadclass方法,因为在该方法第一步会通过Class<?> c = findLoadedClass(name) 从缓存查找,类名完整名称相同则不会再次被加载,因此我们必须绕过缓存查询即重写,才能重新加载class对象。 当然也可直接调用findClass()方法(和loadClass方法一样都返回Class<?> 对象),这样也可以避免从缓存查找。

String rootDir="/Users/zejian/Downloads/Java8_Action/src/main/java/";
//创建两个不同的自定义类加载器实例
FileClassLoader loader1 = new FileClassLoader(rootDir);
FileClassLoader loader2 = new FileClassLoader(rootDir);
//通过findClass创建类的Class对象
Class<?> object1=loader1.findClass("com.zejian.classloader.DemoObj");
Class<?> object2=loader2.findClass("com.zejian.classloader.DemoObj");System.out.println("findClass->obj1:"+object1.hashCode());
System.out.println("findClass->obj2:"+object2.hashCode());/*** 直接调用findClass方法输出结果:* findClass->obj1:723074861findClass->obj2:895328852生成不同的实例*/

如果调用父类的loadClass方法,结果如下,除非重写loadClass()方法去掉缓存查找步骤,不过现在一般都不建议重写loadClass()方法。

//直接调用父类的loadClass()方法
Class<?> obj1 =loader1.loadClass("com.zejian.classloader.DemoObj");
Class<?> obj2 =loader2.loadClass("com.zejian.classloader.DemoObj");//不同实例对象的自定义类加载器
System.out.println("loadClass->obj1:"+obj1.hashCode());
System.out.println("loadClass->obj2:"+obj2.hashCode());
//系统类加载器
System.out.println("Class->obj3:"+DemoObj.class.hashCode());/**
* 直接调用loadClass方法的输出结果,注意并没有重写loadClass方法
* loadClass->obj1:1872034366loadClass->obj2:1872034366Class->    obj3:1872034366都是同一个实例
*/

注意同一个类加载器的实例对同一个class文件只能加载一次,多次加载将报错,因此我们实现热部署之类的功能,必须让同一个class文件可以根据不同的类加载器重复加载。需要自定义实现ClassLoader:
一方面避免使用默认系统类加载器重复加载报错;另一方面绕过缓存方法。
鉴于前面介绍的缓存方法findLoadedClass()的判定逻辑复杂性,自己实现重复加载类,使用自定义实现的classLoader时,要么直接使用findClass方法,要么重写其loadClass()方法。

另外实现热替换时,不能强转为原有类型(原有类型直接写的话,一出现即是被动加载,即通过系统类加载器器加载的(一开始就加载的),两个其实不是一个“类型”),但可以通过一个共同的父类/接口来接收, 共同的这个东西,因为自定义类加载器的父加载器也是AppClassLoader,所以仍然是同一个。

java.lang.ClassCastException: com.tw.client.Foo cannot be cast to com.tw.client.Foo

自定义类加载器 FileClassLoader

public class DemoObj {@Overridepublic String toString() {return "I am DemoObj";}
}package com.zejian.classloader;
import java.io.*;public class FileClassLoader extends ClassLoader {private String rootDir;public FileClassLoader(String rootDir) {this.rootDir = rootDir;}/*** 编写findClass方法的逻辑* @param name* @return* @throws ClassNotFoundException*/@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {// 获取类的class文件字节数组byte[] classData = getClassData(name);if (classData == null) {throw new ClassNotFoundException();} else {//直接生成class对象return defineClass(name, classData, 0, classData.length);}}/*** 编写获取class文件并转换为字节码流的逻辑* @param className* @return*/private byte[] getClassData(String className) {// 读取类文件的字节String path = classNameToPath(className);try {InputStream ins = new FileInputStream(path);ByteArrayOutputStream baos = new ByteArrayOutputStream();int bufferSize = 4096;byte[] buffer = new byte[bufferSize];int bytesNumRead = 0;// 读取类文件的字节码while ((bytesNumRead = ins.read(buffer)) != -1) {baos.write(buffer, 0, bytesNumRead);}return baos.toByteArray();} catch (IOException e) {e.printStackTrace();}return null;}/*** 类文件的完全路径* @param className* @return*/private String classNameToPath(String className) {return rootDir + File.separatorChar+ className.replace('.', File.separatorChar) + ".class";}public static void main(String[] args) throws ClassNotFoundException {String rootDir="/Users/zejian/Downloads/Java8_Action/src/main/java/";//创建自定义文件类加载器FileClassLoader loader = new FileClassLoader(rootDir);try {//加载指定的class文件Class<?> object1=loader.loadClass("com.zejian.classloader.DemoObj");System.out.println(object1.newInstance().toString());//输出结果:I am DemoObj} catch (Exception e) {e.printStackTrace();}}
}

显然我们通过getClassData()方法找到class文件并转换为字节流,并重写findClass()方法,利用defineClass()方法创建了类的class对象。在main方法中调用了loadClass()方法加载指定路径下的class文件,由于启动类加载器、拓展类加载器以及系统类加载器都无法在其路径下找到该类,因此最终将有自定义类加载器加载,即调用findClass()方法进行加载。如果继承URLClassLoader实现,那代码就更简洁了,如下:

public class FileUrlClassLoader extends URLClassLoader {public FileUrlClassLoader(URL[] urls, ClassLoader parent) {super(urls, parent);}public FileUrlClassLoader(URL[] urls) {super(urls);}public FileUrlClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {super(urls, parent, factory);}public static void main(String[] args) throws ClassNotFoundException, MalformedURLException {String rootDir="/Users/zejian/Downloads/Java8_Action/src/main/java/";//创建自定义文件类加载器File file = new File(rootDir);//File to URIURI uri=file.toURI();URL[] urls={uri.toURL()};FileUrlClassLoader loader = new FileUrlClassLoader(urls);try {//加载指定的class文件Class<?> object1=loader.loadClass("com.zejian.classloader.DemoObj");System.out.println(object1.newInstance().toString());//输出结果:I am DemoObj} catch (Exception e) {e.printStackTrace();}}
}

线程上下文类加载器,双亲委派模型的破坏者

在Java应用中存在着很多服务提供者接口(Service Provider Interface,SPI),这些接口允许第三方为它们提供实现,如常见的 SPI 有 JDBC、JNDI等,这些 SPI 的接口属于 Java 核心库,一般存在rt.jar包中,由Bootstrap类加载器加载,而 SPI 的第三方实现代码则是作为Java应用所依赖的 jar 包被存放在classpath路径下,由于SPI接口中的代码经常需要加载具体的第三方实现类并调用其相关方法,但SPI的核心接口类是由Bootstrap类加载器来加载的,而Bootstrap类加载器无法直接加载SPI的实现类,同时由于双亲委派模式的存在,Bootstrap类加载器也无法反向委托AppClassLoader加载器SPI的实现类。在这种情况下,我们就需要一种特殊的类加载器来加载第三方的类库,而线程上下文类加载器就是很好的选择。

线程上下文类加载器(contextClassLoader)是从 JDK 1.2 开始引入的,我们可以通过java.lang.Thread类中的**getContextClassLoader()**和 **setContextClassLoader(ClassLoader cl)**方法来获取和设置线程的上下文类加载器。
如果没有手动设置上下文类加载器,线程将继承其父线程的上下文类加载器,初始线程的上下文类加载器是系统类加载器(AppClassLoader),在线程中运行的代码可以通过此类加载器来加载类和资源,如下图所示,以jdbc.jar加载为例:
在这里插入图片描述
从图可知rt.jar核心包是由Bootstrap类加载器加载的,其内包含SPI核心接口类,由于SPI中的类经常需要调用外部实现类的方法,而jdbc.jar包含外部实现类(jdbc.jar存在于classpath路径)无法通过Bootstrap类加载器加载,因此只能委派线程上下文类加载器把jdbc.jar中的实现类加载到内存以便SPI相关类使用。
显然这种线程上下文类加载器的加载方式破坏了“双亲委派模型”,它在执行过程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器,当然这也使得Java类加载器变得更加灵活。
为了进一步证实这种场景,不妨看看DriverManager类的源码,DriverManager是Java核心rt.jar包中的类,该类用来管理不同数据库的实现驱动,即Driver,它们都实现了Java核心包中的java.sql.Driver接口,如mysql驱动包中的com.mysql.jdbc.Driver,这里主要看看如何加载外部实现类,在DriverManager初始化时会执行如下代码:

//DriverManager是Java核心包rt.jar的类
public class DriverManager {//省略不必要的代码static {loadInitialDrivers();//执行该方法println("JDBC DriverManager initialized");}//loadInitialDrivers方法private static void loadInitialDrivers() {sun.misc.Providers()AccessController.doPrivileged(new PrivilegedAction<Void>() {public Void run() {//加载外部的Driver的实现类//ServiceLoader通过迭代器方法iterator()可遍历所有具体实现ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);//省略不必要的代码......}});}

其中的ServiceLoader类会去读取mysql的jdbc.jar下META-INF文件的内容,如下所示:
在这里插入图片描述
而com.mysql.jdbc.Driver继承类如下:

public class Driver extends com.mysql.cj.jdbc.Driver {public Driver() throws SQLException {super();}static {System.err.println("Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. "+ "The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.");}
}

从注释可以看出平常我们使用com.mysql.jdbc.Driver已被丢弃了,取而代之的是com.mysql.cj.jdbc.Driver,它可以通过spi技术自动加载该实现类了。
使用反射方法(Class.forName())强制创建某各类或者接口对应的java.lang.Class对象时,会进行类加载,(而类加载会将字节码的class文件读入内存,并为之创建一个Class对象)。
不再建议:

//不建议使用该方式注册驱动类
Class.forName("com.mysql.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/cm-storylocker?characterEncoding=UTF-8";
// 通过java库获取数据库连接
Connection conn = java.sql.DriverManager.getConnection(url, "root", "root@555");

而是可以直接使用了:

String url = "jdbc:mysql://localhost:3306/cm-storylocker?characterEncoding=UTF-8";
// 通过java库获取数据库连接
Connection conn = java.sql.DriverManager.getConnection(url, "root", "root@555");

上述的这些实现,归功于**ServiceLoader.load()**方法:

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

很明显了确实是通过线程上下文类加载器加载的,实际上核心包的SPI类对外部实现类的加载都是基于线程上下文类加载器执行的,通过这种方式实现了Java核心代码内部去调用外部实现类。
我们知道线程上下文类加载器默认情况下就是AppClassLoader,那为什么不直接通过getSystemClassLoader()获取类加载器来加载classpath路径下的类的呢?
其实是可行的,但这种直接使用getSystemClassLoader()方法获取AppClassLoader加载类有一个缺点,那就是代码部署到不同服务时会出现问题,如把代码部署到Java Web应用服务或者EJB之类的服务将会出问题,因为这些服务使用的线程上下文类加载器并非AppClassLoader,而是Java Web应用服自家的类加载器,类加载器不同。
所以我们应用该少用getSystemClassLoader()。总之不同的服务使用的可能默认ClassLoader是不同的,但使用线程上下文类加载器总能获取到与当前程序执行相同的ClassLoader,从而避免不必要的问题

通过ServiceLoader实现的SPI是个很不错的解耦机制,可以参考另一片具体查看
(以及非常类似的spring的SPI实现:从所有jar文件中找到MET-INF/spring.factories文件(注意是:classpath下的所有jar包,所以可插拔、扩展性超强))。

参考:
https://blog.csdn.net/m0_37556444/article/details/109626847
https://blog.csdn.net/javazejian/article/details/73413292


http://chatgpt.dhexx.cn/article/1bRNDEyI.shtml

相关文章

什么是类加载器?

类加载器 什么是类加载器&#xff0c;作用是什么&#xff1f; 类加载器就是加载字节码文件(.class)的类 Java语言是一种具有动态性的解释语言&#xff0c;类(CLASS) 只有被加载到 JVM 中后才能运行。当运行指定程序时&#xff0c;JVM会将编译生成的.class文件按照需求和一定的规…

类加载器

类加载过程 加载->连接->初始化。连接过程又可分为三步:验证->准备->解析。 类加载器分类 JVM 中内置了三个重要的 ClassLoader&#xff0c;除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader&#xff1a; 启动类加载器&…

类加载器作用

深入探讨 Java 类加载器 成 富, 软件工程师, IBM 中国软件开发中心 成富任职于 IBM 中国软件开发中心&#xff0c;目前在 Lotus 部门从事 IBM Mashup Center 的开发工作。他毕业于北京大学信息科学技术学院&#xff0c;获得计算机软件与理论专业硕士学位。他的个人网站是 http:…

java中的类加载器

文章目录 前言,一、加载器的作用是什么二、详解类加载器1.不得不说的双亲委派机制2.各个加载器加载的内容3.线程上下文类加载器4.类加载器的庐山真面目 总结 前言, java中一般来说有三种类加载器,分别为: 引导加载器,扩展加载器,应用加载器,还有一个线程上下文类加载器 一、加…

JVM类加载器

文章目录 一、类加载器二、类与类加载器三、双亲委派模型四、破坏双亲委派模型4.1、Tomcat4.1.1、WebApp类加载器4.1.2、Shared类加载器4.1.3、Catalina类加载器4.1.4、Common类加载器4.1.5、Jsp类加载器 4.2、JDBC 一、类加载器 从Java虚拟机的角度来讲&#xff0c;只存在两种…

自定义类加载器

目录 一、为什么要自定义类加载器&#xff1f; 二、常见的场景 三、实现方式 四、自定义类加载器示例 五、Java9新特性 一、为什么要自定义类加载器&#xff1f; 隔离加载类 在某些框架内进行中间件与应用的模块隔离&#xff0c;把类加载到不同的环境。比如&#xff1a;…

类加载器详解(自己实现类加载器)

目录&#xff1a; java虚拟机汇总 class文件结构分析 1).class文件常量池中的常量项结构 2). 常用的属性表的集合类加载过程 1).类加载器的原理以及实现<< 现在位置虚拟机结构分析 1).jdk1.7和1.8版本的方法区构造变化 2).常量池简单区分对象结构分析 1).压缩指针详解gc…

JVM——自定义类加载器

0. 为什么需要自定义类加载器 网上的大部分自定义类加载器文章&#xff0c;几乎都是贴一段实现代码&#xff0c;然后分析一两句自定义ClassLoader的原理。但是我觉得首先得把为什么需要自定义加载器这个问题搞清楚&#xff0c;因为如果不明白它的作用的情况下&#xff0c;还…

JVM - 类加载器

# 类加载器及类加载器执行过程 JDK版本&#xff1a;1.8 # 1、类加载器子系统 下图为类加载子系统&#xff1a; 类加载子系统负责从文件系统或者网络中加载class文件&#xff0c;class文件在文件开头有特定的文件标识&#xff08;CA FE BA BE&#xff09;。Classloader只负责cl…

java的类加载器以及如何自定义类加载器

ClassLoader作用 类加载流程的"加载"阶段是由类加载器完成的。 类加载器结构 结构&#xff1a;BootstrapClassLoader&#xff08;祖父&#xff09;–>ExtClassLoader&#xff08;爷爷&#xff09;–>AppClassLoader(也称为SystemClassLoader)&#xff08;爸…

类加载器(ClassLoader)

一、类加载器&#xff08;ClassLoader&#xff09; 1.1 什么是类加载器 Java的类加载器是Java虚拟机&#xff08;JVM&#xff09;的重要组成部分&#xff0c;它的主要作用是动态地将Java类加载到JVM中&#xff0c;以便在运行时使用这些类。Java类加载器通常是由JVM的实现者提供…

什么是类加载器,类加载器如何分类

一、类加载器 1.什么是类加载器 类加载器&#xff1a;负责将.class文件&#xff08;存储的物理文件&#xff09;加载到内存中 2.类加载时机&#xff1a; ① 创建类的实例&#xff08;对象&#xff09; ② 调用类的实例方法 ③ 访问类或者接口的类变量&#xff0c;或者为该…

深入理解Java虚拟机——再谈类的加载器——第十二章——中篇

深入理解Java虚拟机——Java虚拟机介绍——第一章 深入理解Java虚拟机——类加载子系统——第二章 深入理解Java虚拟机——运行时数据区和本地方法接口——详细篇——第三章 深入理解Java虚拟机——对象的实例化内存布局与访问定位——超级详细篇——第四章 深入理解Java虚拟机…

类加载器详解

类加载器的分类 JVM支持两种类型的类加载器,分别为引导类加载器(BootstrapClassLoader)和自定义类加载器(User-Defined ClassLoader) 从概念上来讲, 自定义类加载器一般指的是程序中由开发人员自定义的一类,类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类…

Java - 类加载器

文章目录 1. 类加载的过程2. 类加载器的分类2.1 引导类加载器 Bootstrap2.2 扩展类加载器 ExtClassLoader2.3 系统类加载器 AppClassLoader2.4 三者之间的关系2.5 自定义类加载器2.6 注&#xff1a;谁来准备类加载器呢? 3. 双亲委派机制4. ClassLoader抽象类5. URLClassLoader…

DAU 和 MAU

DAU 和 MAU 日活跃用户占月活跃用户的比例越高&#xff0c;表明用户对App的使用粘性越高。 DAU&#xff0c;即&#xff1a;Daily Active User&#xff0c;指日活跃用户数 MAU&#xff0c;即&#xff1a;Monthly Active User&#xff0c;指月活跃用户数。 *例子1&#xff1a;…

如何通过DAU分析活跃用户?(案例:python绘制箱体图)

前言&#xff1a;本文内容以游戏产品为基础进行讲解&#xff0c;内容为以下4部分&#xff1a; 1. 如何理解DAU反映了哪些问题&#xff1f; 2. 有哪些因素会影响DAU变动&#xff1f; 3. 如何解读DAU的“箱体图”&#xff1f; 4. 如何使用python绘制“箱体图”&#xff1f; DAU的…

【数据分析】产品日活DAU下降,怎么分析

目录 案例简介 第一步&#xff1a;确认数据真实性 第二步&#xff1a;明确定义&#xff0c;并拆解指标&#xff0c;进一步定位原异常部分 第三步&#xff1a;根据几个常见维度初步拆分数据 第四步&#xff1a;进一步做假设并细分深入&#xff0c;得出结论 案例分析 例题…

数据分析——DAU下降问题(转)

文章转自&#xff1a; DAU异常下降该如何分析 1. 梳理公司的用户增长模式 尽管不同业务形态、以及不同发展阶段的公司&#xff0c;其用户增长模式各有差异&#xff0c;但都可以从拉新策略和促活策略进行分解。 常见的拉新策略有&#xff1a; 流量采购。比如通过厂商预装、…

有赞一面:亿级用户DAU日活统计,有几种方案?

说在前面 在40岁老架构师 尼恩的读者社区(50)中&#xff0c;最近有小伙伴拿到了一线互联网企业如极兔、有赞、希音、百度、网易、滴滴的面试资格&#xff0c;遇到一几个很重要的面试题&#xff1a; (1) 亿级用户场景&#xff0c;如何高性能统计日活&#xff1f; (2) 如何实现亿…