Java类加载器与双亲委派机制和线程上下文类加载器专项解读分析
作者:MinggeQingchun
一、类加载器
类加载器就是根据类的二进制名(binary name)读取java编译器编译好的字节码文件(.class文件),并且转化生成一个java.lang.Class类的一个实例。
每个实例用来表示一个Java类,jvm就是用这些实例来生成java对象的。
如new一个String对象;反射生成一个String对象,都会用到String.class 这个java.lang.Class类的对象。
基本上所有的类加载器都是java.lang.ClassLoader 类的一个实例
类加载器3大分类
类加载器 | 加载类 | 说明 |
启动类加载器(Bootstrap ClassLoader) | JAVA_HOME/jre/lib | 无法直接访问 |
拓展类加载器(Extension ClassLoader) | JAVA_HOME/jre/lib/ext | 上级为 Bootstrap,显示为 null |
应用类加载器(Application ClassLoader) | classpath | 上级为 Extension |
自定义类加载器 | 自定义 | 上级为 Application |
类加载器加载顺序如下
类加载器的核心方法
方法名 | 说明 |
---|---|
getParent() | 返回该类加载器的父类加载器 |
loadClass(String name) | 加载名为name的类,返回java.lang.Class类的实例 |
findClass(String name) | 查找名字为name的类,返回的结果是java.lang.Class类的实例 |
findLoadedClass(String name) | 查找名字为name的已经被加载过的类,返回的结果是java.lang.Class类的实例 |
defineClass(String name,byte[] b,int off,int len) | 根据字节数组b中的数据转化成Java类,返回的结果是java.lang.Class类的实例 |
上述方法的name参数都是binary name(类的二进制名字),如
java.lang.String <包名>.<类名>
java.concurrent.locks.AbstractQueuedSynchronizer$Node <包名>.<类名>$<内部类名>
java.net.URLClassLoader$1 <包名>.<类名>.<匿名内部类名>
1.启动类加载器
启动类加载器是jvm在运行时,内嵌在jvm中的一段特殊的用来加载java核心类库的C++代码
String.class 对象就是由启动类加载器加载的,启动类加载器具体加载哪些核心代码可以通过获取值为 "sun.boot.class.path" 的系统属性获得。
启动类加载器不是java原生代码编写的,所以其也不是java.lang.ClassLoader类的实例,其没有getParent方法
public static void main(String[] args) throws ClassNotFoundException { Class<?> aClass = Class.forName("com.mycompany.load.F"); System.out.println(aClass.getClassLoader()); // AppClassLoader ExtClassLoader }
输出
cd D:\Java\JavaProject\jvm-demo\myjvm\out\production\myjvm
D:\Java\JavaProject\jvm-demo\myjvm\out\production\myjvm>java -Xbootclasspath/a:. com.mycompany.load.Load4
bootstrap F init
null
打印 null,表示它的类加载器是 Bootstrap ClassLoader
-Xbootclasspath 表示设置 bootclasspath
其中 /a:. 表示将当前目录追加至 bootclasspath 之后
用这个办法替换核心类
java -Xbootclasspath:<new bootclasspath>
java -Xbootclasspath/a:<后追加路径>
java -Xbootclasspath/p:<前追加路径>
2.拓展类加载器
拓展类加载器用来加载jvm实现的一个拓展目录,该目录下的所有java类都由此类加载器加载。
此路径可以通过获取"java.ext.dirs"的系统属性获得。拓展类加载器就是java.lang.ClassLoader类的一个实例,其getParent方法返回的是引导类加载器(在 HotSpot虚拟机中用null表示引导类加载)
D:\Java\JavaProject\jvm-demo\myjvm\out\production\myjvm>jar -cvf my.jar com/mycompany/load/F.class
已添加清单
正在添加: com/mycompany/load/F.class(输入 = 481) (输出 = 322)(压缩了 33%)
将 jar 包拷贝到 JAVA_HOME/jre/lib/ext,重新执行代码即可
3.应用类加载器
应用类加载器又称为系统类加载器,开发者可用通过 java.lang.ClassLoader.getSystemClassLoader()方法获得此类加载器的实例,系统类加载器也因此得名。其主要负责加载程序开发者自己编写的java类
一般来说,java应用都是用此类加载器完成加载的,可以通过获取"java.class.path"的系统属性(也就是我们常说的classpath)来获取应用类加载器加载的类路径。应用类加载器是java.lang.ClassLoader类的一个实例,其getParent方法返回的是拓展类加载器
4.类的命名空间
在程序运行过程中,一个类并不是简单由其二进制名字(binary name)定义的,而是通过其二进制名和其定义加载器所确定的命名空间(run-time package)所共同确定的。
同一个二进制名的类由不同的定义加载器加载时,其返回的Class对象不是同一个,那么由不同的Class对象所创建的对象,其类型也不是相同的。
类似Test cannot be cast to Test的java.lang.ClassCastException 的奇怪错误很多情况下都是类的二进制名相同,而定义加载器不同造成的
package com.mycompany.load; import sun.misc.Launcher; public class Load6 { public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException { ClassLoader classLoader = new Launcher().getClassLoader(); //1 new一个新的类加载器 System.out.println(classLoader); /* 这是因为 1处获取的应用类加载器a和jvm用来加载Load6.class对象的应用类加载器b不是同一个实例, 那么构成这两个类的run-time package也就是不同的。所以即使它们的二进制名字相同, 但是由a定义的Load6类所创建的对象显然不能转化为由b定义的Load6类的实例。 这种情况下jvm就会抛出ClassCastException * */ Class<?> aClass = classLoader.loadClass("com.mycompany.load.Load6"); Load6 load6 = (Load6)aClass.newInstance(); //2 //Exception in thread "main" java.lang.ClassCastException: com.mycompany.load.Load6 cannot be cast to com.mycompany.load.Load6 } }
报出异常:
java.lang.ClassCastException: com.mycompany.load.Load6 cannot be cast to com.mycompany.load.Load6
这是因为 1处获取的应用类加载器a和jvm用来加载Load6.class对象的应用类加载器b不是同一个实例, 那么构成这两个类的run-time package也就是不同的。
所以即使它们的二进制名字相同, 但是由a定义的Load6类所创建的对象显然不能转化为由b定义的Load6类的实例。 这种情况下jvm就会抛出ClassCastException
相同二进制名字的类,如果其定义加载器不同,也算是不同的两个类
二、双亲委派机制
双亲委派机制 Parent Delegation Model,又称为父级委托模型。所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则(双亲,理解为上级更为合适,因为它们之间并没有继承关系)
1.类加载机制流程
Java编译器把Java源文件编译成.class文件,再由JVM装载.class文件到内存中,JVM装载完成后得到一个Class对象字节码。有了字节码对象,就可以实例化使用
2.类加载器加载顺序
3.双亲委派机制流程
1、加载类MyClass.class,从低层级到高层级一级一级委派,先由应用层加载器委派给扩展类加载器,再由扩展类委派给启动类加载器
(1)如果是自定义加载器挂载到应用程序类加载器
(2)应用程序类加载器将类加载请求委托给扩展类加载器
(3)扩展类加载器将类加载请求委托给启动类加载器
2、启动类加载器载入失败,再由扩展类加载器载入,扩展类加载器载入失败,最后由应用类加载器载入
(1)启动类加载器在加载路径下查找并加载Class文件,如果未找到目标Class文件,则交由扩展类加载器加载
(2)扩展类加载器在加载路径下查找并加载Class文件,如果未找到目标Class文件,则交由应用程序类加载器加载
(3)应用程序类加载器在加载路径下查找并加载Class文件,如果未找到目标Class文件,则交由自定义加载器加载
(4)在自定义加载器下查找并加载用户指定目录下的Class文件,如果在自定义加载路径下未找到目标Class文件,则抛出ClassNotFoud异常
3、如果应用类加载器也找不到那就报ClassNotFound异常了
4.源码分析
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; } }
执行流程为:
1、sun.misc.Launcher$AppClassLoader //1 处, 开始查看已加载的类,如果没有
2、sun.misc.Launcher$AppClassLoader // 2 处,委派上级 sun.misc.Launcher$ExtClassLoader.loadClass()
3、sun.misc.Launcher$ExtClassLoader // 1 处,查看已加载的类,如果没有
4、sun.misc.Launcher$ExtClassLoader // 3 处,没有上级了,则委派 BootstrapClassLoader 查找
5、BootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 类,没有
6、sun.misc.Launcher$ExtClassLoader // 4 处,调用自己的 findClass 方法,是在 JAVA_HOME/jre/lib/ext 下找 类,显然没有,回到 sun.misc.Launcher$AppClassLoader 的 // 2 处
7、继续执行到 sun.misc.Launcher$AppClassLoader // 4 处,调用它自己的 findClass 方法,在 classpath 下查找,找到了
5.双亲委派机制优缺点
优点:
1、保证安全性,层级关系代表优先级,也就是所有类的加载,优先给启动类加载器,这样就保证了核心类库类
2、避免类的重复加载,如果父类加载器加载过了,子类加载器就没有必要再去加载了,确保一个类的全局唯一性
缺点:
检查类是否加载的委派过程是单向的, 这个方式虽然从结构上说比较清晰,使各个 ClassLoader 的职责非常明确, 但是同时会带来一个问题, 即顶层的ClassLoader 无法访问底层的ClassLoader 所加载的类
通常情况下, 启动类加载器中的类为系统核心类, 包括一些重要的系统接口,而在应用类加载器中, 为应用类。 按照这种模式, 应用类访问系统类自然是没有问题, 但是系统类访问应用类就会出现问题。
如在系统类中提供了一个接口, 该接口需要在应用类中得以实现, 该接口还绑定一个工厂方法, 用于创建该接口的实例, 而接口和工厂方法都在启动类加载器中。 这时, 就会出现该工厂方法无法创建由应用类加载器加载的应用实例
三、线程上下文类加载器
线程上下文类加载器就是用来解决类的双亲委托模型的缺陷
在Java中,官方为我们提供了很多SPI接口,例如JDBC、JBI、JNDI等。这类SPI接口,官方往往只会定义规范,具体的实现则是由第三方来完成的,比如JDBC,不同的数据库厂商都需自己根据JDBC接口的定义进行实现。
而这些SPI接口直接由Java核心库来提供,一般位于rt.jar
包中,而第三方实现的具体代码库则一般被放在classpath
的路径下。而此时问题来了:
位于rt.jar
包中的SPI接口,是由Bootstrap类加载器完成加载的,而classpath
路径下的SPI实现类,则是App
类加载器进行加载的。
但往往在SPI接口中,会经常调用实现者的代码,所以一般会需要先去加载自己的实现类,但实现类并不在Bootstrap类加载器的加载范围内,经过前面的双亲委派机制的分析,我们已经得知:子类加载器可以将类加载请求委托给父类加载器进行加载,但这个过程是不可逆的。也就是父类加载器是不能将类加载请求委派给自己的子类加载器进行加载的,
此时就出现了这个问题:如何加载SPI接口的实现类?那就是打破双亲委派模型
SPI(Service Provider Interface):Java的SPI机制,其实就是可拔插机制。在一个系统中,往往会被分为不同的模块,比如日志模块、JDBC模块等,而每个模块一般都存在多种实现方案,如果在Java的核心库中,直接以硬编码的方式写死实现逻辑,那么如果要更换另一种实现方案,就需要修改核心库代码,这就违反了可拔插机制的原则。为了避免这样的问题出现,就需要一种动态的服务发现机制,可以在程序启动过程中,动态的检测实现者。而SPI中就提供了这么一种机制,专门为某个接口寻找服务实现的机制。如下:
当第三方实现者提供了服务接口的一种实现之后,在jar包的 META-INF/services/ 目录里同时创建一个以服务接口命名的文件,该文件就是实现该服务接口的实现类。而当外部程序装配这个模块的时候,就能通过该jar包 META-INF/services/ 里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。
基于这样一个约定就能很好的找到服务接口的实现类,而不需要在代码里制定。
同时,JDK官方也提供了一个查找服务实现者的工具类:java.util.ServiceLoader
线程上下文类加载器就是双亲委派模型的破坏者,可以在执行线程中打破双亲委派机制的加载链关系,从而使得程序可以逆向使用类加载器
1.线程上下文类加载器(Context Classloader)
线程上下文类加载器(Context Classloader)是从JDK1.2开始引入的,类Thread中的getContextClassLoader()和setContextClassLoader(ClassLoader cl)分别用来获取和设置上线文类加载器
如果没有通过setContextClassLoader(ClassLoader cl)进行设置的话,线程将继承其父线程的上下文类加载器。
Java应用运行时的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过该类加载器来加载类与资源
它可以打破双亲委托机制,父ClassLoader可以使用当前线程的Thread.currentThread().getContextClassLoader()所指定的classLoader来加载类,这就可以改变父ClassLoader不能使用子ClassLoader或是其他没有直接父子关系的ClassLoader加载的类的情况,即改变了双亲委托模型
对于SPI来说,有些接口是Java核心库所提供的,而Java核心库是由启动类加载器加载的,而这些接口的实现却是来自于不同jar包(厂商提供),Java的启动类加载是不会加载其他来源的jar包,这样传统的双亲委托模型就无法满足SPI的要求。而通过给当前线程设置上下文类加载器,就可以由设置的上线文类加载器来实现与接口实现类的加载
Java提供了很多核心接口的定义,这些接口被称为SPI接口,同时为了方便加载第三方的实现类,SPI提供了一种动态的服务发现机制(约定),只要第三方在编写实现类时,在工程内新建一个META-INF/services/
目录并在该目录下创建一个与服务接口名称同名的文件,那么在程序启动的时候,就会根据约定去找到所有符合规范的实现类,然后交给线程上下文类加载器进行加载处理
MySQL的Driver驱动类
在使用 JDBC 时,都需要加载 Driver 驱动,不写
Class.forName("com.mysql.jdbc.Driver")
或
Class.forName("com.mysql.cj.jdbc.Driver")
也可以让 com.mysql.jdbc.Driver 正确加载
在MySQL6.0之后的jar包中,遗弃了之前的com.mysql.jdbc.Driver驱动,而是使用com.mysql.cj.jdbc.Driver取而代之,因为后者不需要再自己通过Class.forName("com.mysql.jdbc.Driver")
这种方式手动注册驱动,全部都可以交由给SPI机制处理
在使用 JDBC,MySQL的com.mysql.cj.jdbc.Driver
的驱动类,主要就是用了Java中SPI定义的一个核心类:DriverManager
,该类位于rt.jar
包中,是Java中用于管理不同数据库厂商实现的驱动,同时这些各厂商实现的Driver
驱动类,都继承自Java的核心类java.sql.Driver
DriverManager 的类加载器
System.out.println(DriverManager.class.getClassLoader());
打印 null,表示它的类加载器是 Bootstrap ClassLoader,会到 JAVA_HOME/jre/lib 下搜索类,但 JAVA_HOME/jre/lib 下显然没有 mysql-connector-java-xx.xx.xx.jar 包
public class DriverManager { // 注册驱动的集合 private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>(); // 初始化驱动 static { loadInitialDrivers(); println("JDBC DriverManager initialized"); } }
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); // 这里的 ClassLoader.getSystemClassLoader() 就是应用程序类加载器 Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); } catch (Exception ex) { println("DriverManager.Initialize: load failed: " + ex); } } }
从DriverManager中的loadInitialDrivers我们可以得知,我们即使不使用Class.forName(“com.mysql.cj.jdbc.Driver”),mysql的驱动也能被加载,这是因为后期jdk使用了ServiceLoader
2 是使用 Class.forName 完成类的加载和初始化,关联的是应用程序类加载器,因此 可以顺利完成类加载
1 就是 Service Provider Interface (SPI) 约定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名为文件,文件内容是实现类名称
2.ServiceLoader
ServiceLoader 是一个简单的加载服务提供者的机制。通常服务提供者会实现服务当中所定义的接口。服务提供者可以以一种扩展的jar包的形式安装到java平台上扩展目录中,也可以添加到应用的classpath中。
1、服务提供者需要提供一个无参数的构造方法
2、服务提供者是通过在META-INF/services目录下相应的提供者配置文件,该配置文件的文件名由服务接口的包名组成。
3、提供者配置文件里面就是实现这个服务接口的类路径,每个服务提供者占一行。
4、ServiceLoader是按需加载和实例化提供者的,就是懒加载,ServiceLoader其中还包含一个服务提供者缓存,里面存放着已经加载的服务提供者。
5、ServiceLoader会返回一个iterator迭代器,会返回所有已经加载了的服务提供者
6、ServiceLoader是线程不安全的
使用ServiceLoader
ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class); Iterator<接口类型> iter = allImpls.iterator(); while(iter.hasNext()) { iter.next(); }
例:
ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class); Iterator<Driver> iterator = loader.iterator(); while (iterator.hasNext()){ Driver dirver = iterator.next(); System.out.println(dirver.getClass()+", 类加载器:"+dirver.getClass().getClassLoader()); } System.out.println("当前线程上线文类加载器:"+Thread.currentThread().getContextClassLoader()); System.out.println("ServiceLoader类加载器:"+loader.getClass().getClassLoader());
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 }
可参考
四、自定义类加载器
1、自定义类加载器场景
1、加载非 classpath 随意路径中的类文件
2、通过接口来使用实现,希望解耦时(常用在框架设计)
3、不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器
2、自定义类加载器步骤
1、继承 ClassLoader 父类
2、遵从双亲委派机制,重写 findClass 方法
注:不是重写 loadClass 方法,否则不会走双亲委派机制
3、读取类文件的字节码
4、调用父类的 defineClass 方法来加载类
5、使用者调用该类加载器的 loadClass 方法
public class Load7 { public static void main(String[] args) throws Exception { MyClassLoader classLoader = new MyClassLoader(); Class<?> c1 = classLoader.loadClass("TestServiceImpl"); Class<?> c2 = classLoader.loadClass("TestServiceImpl"); System.out.println(c1 == c2);//true MyClassLoader classLoader2 = new MyClassLoader(); Class<?> c3 = classLoader2.loadClass("TestServiceImpl"); //虽然相同类名,但不是同一个类加载器加载的 System.out.println(c1 == c3);//false c1.newInstance(); } } class MyClassLoader extends ClassLoader { @Override // name 就是类名称 protected Class<?> findClass(String name) throws ClassNotFoundException { String path = "D:\\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); } } }
到此这篇关于Java类加载器与双亲委派机制和线程上下文类加载器专项解读分析的文章就介绍到这了,更多相关Java类加载器内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!