Java 类加载器的作用就是在运行时加载类. 它通过加载 class 文件, 网络上的字节流或其他来源构造 Class 对象, 用于生成对象在程序中运行.
在平时的程序开发中, 我们一般不需要操作类加载, 因为 Java 本身的类加载器机制已经帮我们做了很多事情. 但后面很多时候, 比如说自己开发框架或者排查问题的时候, 我们需要理解类加载的机制和如何按自己的需求去自定义类加载器. 类加载器知识就像 Java 开发的一道门, 门内外隔离了开发人员对类加载的使用, 而了解类加载器就掌握了这道门的钥匙.
什么是类加载器
类加载器是一个用来加载类文件的类. Java 源代码通过编译器编译后, 类加载器加载文件中的字节码来执行程序, 字节码的来源也可以来自于网络. Java 有 3 中默认的类加载器: Bootstrap 类加载器, Extension 类加载器和 System 类加载器(或者叫做 Application 类加载器). 每个类加载器都设定好从哪里加载类.
Bootstrap 类加载器: JRE/lib/rt.jar 中的 JDK 类文件
Bootstrap 类加载器是所有类加载器的父类(非类继承关系). 它的大部分由 C 来写的, 通过代码获取不到, 比如调用 String.class.getClassLoader(), 会返回 null.
Extension 类加载器: JRE/lib/ext 或者 java.ext.dirs 指向的目录
Extension 加载器由 sun.misc.Launcher$ExtClassLoader 实现 System 类加载器: CLASSPATH 环境变量, 由 - classpath 或 - cp 选项定义, 或者是 JAR 中的 Manifest 的 classpath 属性定义. System 类加载器由 sun.misc.Launcher$AppClassLoader 实现.
类加载器的工作原理
Java 的类加载性保证了很好的稳定性和拓展性. 类加载器的工作原理基于三个机制
委托机制
某加载器在尝试加载类的时候, 都会委托其父类加载器尝试加载类. 比如一个应用要加载 CLASSPAH 下 A.class. 加载这个类的请求由 System 类加载器委托父加载器 Extension 类加载器, Extension 类加载器会委托父加载器 Bootstrap 类加载器. Bootstrap 类加载器先从 rt.jar 中尝试加载这个类. 这个类在 rt.jar 中不存在, 所以加载工作回到 Extension 类加载器. Extension 类加载器会查看 jre/lib/ext 目录下有没有这个类, 如果存在那么这个类将被加载, 且只加载一次. 如果没找到, 加载请求有回到了 System 类加载器. System 类加载器从 CLASPATH 中查看该类, 如果存在则加载这个类, 如果没找到, 则报 java.lang.ClassNotFoundException.
可见性机制
子类加载器可以看到父类加载器加载的类, 而反之则不行.
单一性机制
父加载器加载过的类不能被子加载器加载第二次, 同一个类加载器实例也只能加载一个类一次.
如何加载类
我们可以使用显示调用的方式或者交由 JVM 隐式加载一个类. 在类加载的过程中, 一般有三个概念上的类加载器提供使用.
CurrentClassLoader, 称之为当前类加载器
SpecificClassLoader, 称之为指定的类加载器. 值得是一个特定的 ClassLoader 示例
ThreadContextClassLoader, 称之为线程上下文类加载器. 每个线程都会拥有一个 ClassLoader 引用, 而且可以通过 Thread.currentThread().setContextClassLoader(ClassLoader classLoader)进行切换
其中 CurrentClassLoader 的加载过程是 JVM 运行时候控制的, 非显示调用.
显示加载
Class.forName 和 ClassLoader.loadClass 都可以用来进行类加载. 比如
- Class.forName("com.xiaoyi.pandora.vo.A");
- Class.forName("com.xiaoyi.pandora.vo.A", true, customClassLoader1);
- customClassLoader1.loadClass("com.xiaoyi.pandora.vo.A");
Class.forName 的方式其实是使用了 CurrentClassLoader 这种方式, 本质上还是找到一个类加载器去执行 ClassLoader.loadClass 动作.
- @CallerSensitive
- public static Class<?> forName(String className)
- throws ClassNotFoundException {
- Class<?> caller = Reflection.getCallerClass();
- return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
- }
从代码中可以看出, 利用反射的机制获取执行方法的类示例 caller, 从而找到加载 caller 对应类的类加载器. 从而将加载的动作交由这个类加载去执行.
类名, 类加载器, 类示例这三者的关系是紧密相连的. 这里要提到一个数据结构来保存 Java 加载类过程中这三者的关系. SystemDictionary(系统字典).
SystemDictionary 以类名和类加载器实例作为一个 key,Class 对象引用为 value.Class 对象能过找到它的类加载器, 类名和类加器实例对应一个唯一的 Class 对象.
所以, Class.forName 的调用方式是先从 SystemDictionary 获取当前类加载器, 然后以 ClassLoader.loadClass 的方式去加载一个类.
隐式加载
大多数情况下, 程序中的类加载都是通过隐式加载的形式. 不需要显示调用 ClassLoader 对象去加载类. 我们看下面一个很普通的场景. 为了显示实际效果, 这里自定义了一个类加载器去显示类的加载动作.
- public class A {
- B b;
- public A() {
- this.b = new B();
- }
- public void show() {
- System.out.println("b's classLoader is " + b.getClass().getClassLoader());
- }
- }
在加载 A.class 后, 如果实例化类 A 对象, JVM 会自动帮我们利用类加载器帮我们加载 B 类.
- String classPath = "/Users/xiaoyi/work";
- CustomClassLoader customClassLoader1 = new CustomClassLoader(classPath, "xiao");
- Class aClazz = Class.forName("com.xiaoyi.pandora.vo.A", true, customClassLoader1);
- Object a = aClazz.newInstance();
控制台的输出如下
- xiao classLoader start load class :com.xiaoyi.pandora.vo.A
- xiao classLoader start load class :com.xiaoyi.pandora.vo.B
在不执行 Object a = aClazz.newInstance(); 这条语句的时候, 控制台不会输出 xiao classLoader start load class :com.xiaoyi.pandora.vo.B.
总结: 在加载 B 类时, JVM 会隐式获取加载 A 类的类加器去执行加载 B 类的工作.
类加载过程
java.lang.ClassLoader 中 Class<?> loadClass(String name, boolean resolve)方法介绍了详细的类加载过程. 类加载中重要的四个方法, loadClass 是下面 1,2,3 方法的入口
1,Class<?> findLoadedClass(String name)
判断类是否已经被加载过. 即从 SystemDictionary 中根据类型和当前类加载器作为 key, 查看是否能找到对应的 Class
2,Class<?> findClass(String name)
交给子类加载器的拓展
3,void resolveClass(Class<?> c)
将类名, 当前类加载器, 类示例对象关联起来, 存储在 SystemDictionary 中. Java 中规范在一个类被使用之前, 必须做关联(Before the class can be used it must be resolved)
4,Class<?> defineClass(String name, byte[] b, int off, int len)
根据类文件或者网络上的字节流, 转化为一个 Class 的实例. 常用于自定义类加载器.
java.lang.ClassLoader 中 Class<?> loadClass(String name, boolean resolve)的代码如下(省略了非核心流程的代码)
- 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 {
- // 委托 Bootstrap 类加载器去尝试加载
- c = findBootstrapClassOrNull(name);
- }
- } catch (ClassNotFoundException e) {
- }
- if (c == null) {
- // 类加载器本身去加载类
- c = findClass(name);
- }
- }
- if (resolve) {
- // 关联类和类加载器的关系
- resolveClass(c);
- }
- return c;
- }
- }
一些类加载器的使用场景
自定义类加载器
自定义类加载器是使用类加载器一个基本场景. 关键的方法在前面 "类加载过程" 已经给出. 主要有 2 点
1, 继承 ClassLoader
2, 重写 findClass 方法, 实现获取类的字节流转化为类实例返回
一个简单的自定义类加载器如下
- public class CustomClassLoader extends ClassLoader {
- private String name;
- private String classPath;
- public CustomClassLoader(String classPath, String name) {
- this.name = name;
- this.classPath = classPath;
- }
- @Override
- public Class<?> findClass(String name) throws ClassNotFoundException {
- System.out.println(this.name + "classLoader start load class :" + name);
- try {
- byte[] classData = getData(name);
- if(classData != null) {
- return defineClass(name, classData, 0, classData.length);
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- return super.findClass(name);
- }
- private byte[] getData(String className) throws IOException {
- InputStream in = null;
- ByteArrayOutputStream out = null;
- String path = classPath + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
- try {
- in = new FileInputStream(path);
- out = new ByteArrayOutputStream();
- byte[] buffer = new byte[2048];
- int length = 0;
- while ((length = in.read(buffer)) != -1) {
- out.write(buffer, 0, length);
- }
- return out.toByteArray();
- } catch (Exception e) {
- } finally {
- if(in != null) {
- in.close();
- }
- if(out != null) {
- out.close();
- }
- }
- return null;
- }
- }
应用隔离
从 SystemDictionary 数据结构中, 一个类名和类加载器实例, 对应一个 Class 实例. 也就是说一个类文件, 可以交由不同的类加载器去生成不同的类实例, 这些类实例是可以在 JVM 中并存的. 但是如果在对类加载器的访问上做好隔离, 这些类实例在 JVM 中是可以实现隔离的. 具体可以参考 Tomcat 容器如何利用 webappClassLoader 实现应用上的隔离的技术相关文档或者 Pandora 的隔离实现原理.
SPI
SPI 全称是 Service Provider Interface, 是 JDK 内置的一种服务提供发现机制. 是一种动态替换发现机制. 举个例子: 有个接口想在运行时才发现具体的实现类, 那么你只需在程序运行前添加一个实现即可. 常见的 SPI 有 JDBC,JCE,JNDI,JAXP 等.
这些 SPI 的接口是由 Java 核心库来提供, 而 SPI 的实现则是作为第二方 jar 包被包含进类路径 (classpath) 中. 以 JDBC 的 MySQL 为例子, 调用代码如下所示
Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydb", "root", "root");
com.MySQL.jdbc.Driver 中的代码如下, 在类被加载后, 向 DriverManager 中注册 MySQL 的 Driver.
- public class Driver extends NonRegisteringDriver implements java.sql.Driver {
- public Driver() throws SQLException {
- }
- static {
- try {
- DriverManager.registerDriver(new Driver());
- } catch (SQLException var1) {
- throw new RuntimeException("Can\'t register driver!");
- }
- }
- }
下面来分析一下其中的类加载过程
1, 隐式方式加载 DriverManager 类, 因为 DriverManager 在 JDK 的 rt.jar 中, 这是对应的类加载器是 Bootstrap 类加载器
2,DriverManager 类加载后, 执行静态代码块, 初始化 java.sql.Driver 的实现类. 按照 SPI 的规范, ServiceLoader 会去 / META-INF/services 下面查找 java.sql.Driver 资源
3,ServiceLoader 获取 ThreadContextClassLoader, 构造 ServiceLoader 实例, 并将 ThreadContextClassLoader 赋给 ServiceLoader 实例. 这时候的类加载器是 System 类加载器.
- 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);
- }
4,DriverManager 利用 ServiceLoader 去获取 / META-INF/services 下的 java.sql.Driver 文件, 加载对应的实现类, 并初始化具体的 Driver 类. 这时候的类加载器是第 3 步的 System 类加载器, 它能够加载 classpath 下面的类.
- ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
- Iterator<Driver> driversIterator = loadedDrivers.iterator();
- try{
- while(driversIterator.hasNext()) {
- driversIterator.next();
- }
- } catch(Throwable t) {
- // Do nothing
- }
- //driversIterator 在调用 next 方法时, 加载驱动类
- Class<?> c = null;
- try {
- c = Class.forName(cn, false, loader);
- } catch (ClassNotFoundException x) {
- fail(service, "Provider" + cn + "not found");
- }
- S p = service.cast(c.newInstance());
- providers.put(cn, p);
- return p;
5,Driver 类加载实例化后, 会执行静态代码将自己注册到 DriverManager 中
SPI 的实现方式其实是破坏了类加载器的委托机制, 在类加载的过程中, Bootstrap 类加载在获取不到具体的 SPI Provider 实现类的情况下, 委托 ThreadContextLoader 去加载 classpath 下的实现类.
来源: https://juejin.im/post/5c947c9d6fb9a070ef60a5ac