ClassLoader揭秘

90 阅读10分钟

ClassLoader做什么的?

负责将Class的字节码转换为内存形式的Class对象。字节码可以来自于磁盘文件 *.class,也可以是 jar 包里的 *.class,也可以来自远程服务器提供的字节流,字节码的本质就是一个字节数组 []byte,它有特定的复杂的内部格式。

图片

每个 Class 对象的内部都有一个 classLoader 字段来标识自己是由哪个 ClassLoader 加载的。ClassLoader 就像一个容器,里面装了很多已经加载的 Class 对象。

public final class Class<T> implements java.io.Serializable,
                              GenericDeclaration,
                              Type,
                              AnnotatedElement {
    ...
    // Initialized in JVM not by private constructor
    // This field is filtered from reflection access, i.e. getDeclaredField
    // will throw NoSuchFieldException
    private final ClassLoader classLoader;
    ...
}

延迟加载

JVM 运行并不是一次性加载所需要的全部类的,它是按需加载,也就是延迟加载。程序在运行的过程中会逐渐遇到很多不认识的新类,这时候就会调用 ClassLoader 来加载这些类。加载完成后就会将 Class 对象存在 ClassLoader 里面,下次就不需要重新加载了。

比如你在调用某个类的静态方法时,首先这个类肯定是需要被加载的,但是并不会触及这个类的实例字段,那么实例字段的类别 Class 就可以暂时不必去加载,但是它可能会加载静态字段相关的类别,因为静态方法会访问静态字段。而实例字段的类别需要等到你实例化对象的时候才可能会加载。

各司其职

JVM 运行实例中会存在多个 ClassLoader,不同的 ClassLoader 会从不同的地方加载字节码文件。它可以从不同的文件目录加载,也可以从不同的 jar 文件中加载,也可以从网络上不同的静态文件服务器来下载字节码再加载。

JVM 中内置了三个重要的 ClassLoader,分别是 BootstrapClassLoader、ExtensionClassLoader 和 AppClassLoader。

BootstrapClassLoader 负责加载 JVM 运行时核心类,这些类位于 $JAVA_HOME/lib/rt.jar 文件中,我们常用内置库 java.xxx.* 都在里面,比如 java.util. java.io. 、java.nio. 、java.lang. 等等。这个 ClassLoader 比较特殊,它是由 C 代码实现的,我们将它称之为「根加载器」。

ExtensionClassLoader 负责加载 JVM 扩展类,比如 swing 系列、内置的 js 引擎、xml 解析器 等等,这些库名通常以 javax 开头,它们的 jar 包位于 $JAVA_HOME/lib/ext/*.jar 中,有很多 jar 包。

AppClassLoader 才是直接面向我们用户的加载器,它会加载 Classpath 环境变量里定义的路径中的 jar 包和目录。我们自己编写的代码以及使用的第三方 jar 包通常都是由它来加载的。

那些位于网络上静态文件服务器提供的 jar 包和 class文件,jdk 内置了一个 URLClassLoader,用户只需要传递规范的网络路径给构造器,就可以使用 URLClassLoader 来加载远程类库了。URLClassLoader 不但可以加载远程类库,还可以加载本地路径的类库,取决于构造器中不同的地址形式。ExtensionClassLoader 和 AppClassLoader 都是 URLClassLoader 的子类,它们都是从本地文件系统里加载类库。

AppClassLoader 可以由 ClassLoader 类提供的静态方法 getSystemClassLoader() 得到,它就是我们所说的「系统类加载器」,我们用户平时编写的类代码通常都是由它加载的。当我们的 main 方法执行的时候,这第一个用户类的加载器就是 AppClassLoader。

ClassLoader传递性

程序在运行过程中,遇到了一个未知的类,它会选择哪个 ClassLoader 来加载它呢?虚拟机的策略是使用调用者 Class 对象的 ClassLoader 来加载当前未知的类。何为调用者 Class 对象?就是在遇到这个未知的类时,虚拟机肯定正在运行一个方法调用(静态方法或者实例方法),这个方法挂在哪个类上面,那这个类就是调用者 Class 对象。前面我们提到每个 Class 对象里面都有一个 classLoader 属性记录了当前的类是由谁来加载的。

因为 ClassLoader 的传递性,所有延迟加载的类都会由初始调用 main 方法的这个 ClassLoader 全全负责,它就是 AppClassLoader

双亲委派

前面我们提到 AppClassLoader 只负责加载 Classpath 下面的类库,如果遇到没有加载的系统类库怎么办,AppClassLoader 必须将系统类库的加载工作交给 BootstrapClassLoader 和 ExtensionClassLoader 来做,这就是我们常说的「双亲委派」。

图片

AppClassLoader 在加载一个未知的类名时,它并不是立即去搜寻 Classpath,它会首先将这个类名称交给 ExtensionClassLoader 来加载,如果 ExtensionClassLoader 可以加载,那么 AppClassLoader 就不用麻烦了。否则它就会搜索 Classpath 。

而 ExtensionClassLoader 在加载一个未知的类名时,它也并不是立即搜寻 ext 路径,它会首先将类名称交给 BootstrapClassLoader 来加载,如果 BootstrapClassLoader 可以加载,那么 ExtensionClassLoader 也就不用麻烦了。否则它就会搜索 ext 路径下的 jar 包。

这三个 ClassLoader 之间形成了级联的父子关系,每个 ClassLoader 都很懒,尽量把工作交给父亲做,父亲干不了了自己才会干。每个 ClassLoader 对象内部都会有一个 parent 属性指向它的父加载器。

class ClassLoader {
  ...
  private final ClassLoader parent;
  ...
}

上图中ExtensionClassLoader的parent画了虚线,这是因为它的parent值是null,当parent为null表示它的父类加载器就是根加载器BootstrapClassLoader。

这里的parent不是super,只是ClassLoader内部一个字段。

双亲委派的好处:

双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载

自定义加载器

自定义加载器的话,需要继承 ClassLoader 。如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法。

ClassLoader 里面有三个重要的方法 loadClass()、findClass() 和 defineClass()。

loadClass() 方法是加载目标类的入口,它首先会查找当前 ClassLoader 以及它的双亲里面是否已经加载了目标类,如果没有找到就会让双亲尝试加载,如果双亲都加载不了,就会调用 findClass() 让自定义加载器自己来加载目标类。ClassLoader 的 findClass() 方法是需要子类来覆盖的,不同的加载器将使用不同的逻辑来获取目标类的字节码。拿到这个字节码之后再调用 defineClass() 方法将字节码转换成 Class 对象。

class ClassLoader {
  // 加载入口,定义了双亲委派规则
  Class loadClass(String name) {
    // 是否已经加载了
    Class t = this.findFromLoaded(name);
    if(t == null) {
      // 交给双亲
      t = this.parent.loadClass(name)
    }
    if(t == null) {
      // 双亲都不行,只能靠自己了
      t = this.findClass(name);
    }
    return t;
  }
  
  // 交给子类自己去实现
  Class findClass(String name) {
    throw ClassNotFoundException();
  }
  
  // 组装Class对象
  Class defineClass(byte[] code, String name) {
    return buildClassFromCode(code, name);
  }
}
​
class CustomClassLoader extends ClassLoader {
​
  Class findClass(String name) {
    // 寻找字节码
    byte[] code = findCodeFromSomewhere(name);
    // 组装Class对象
    return this.defineClass(code, name);
  }
}

自定义类加载器不易破坏双亲委派规则,不要轻易覆盖 loadClass 方法。否则可能会导致自定义加载器无法加载内置的核心类库。

在使用自定义加载器时,要明确好它的父加载器是谁,将父加载器通过子类的构造器传入。如果父类加载器是 null,那就表示父加载器是「根加载器」。

// ClassLoader 构造器
protected ClassLoader(String name, ClassLoader parent);

双亲委派规则可能会变成三亲委派,四亲委派,取决于你使用的父加载器是谁,它会一直递归委派到根加载器。

Class.forName使用的例子

当我们在使用 jdbc 驱动时,经常会使用 Class.forName 方法来动态加载驱动类。

Class.forName("com.mysql.cj.jdbc.Driver");

其原理是 mysql 驱动的 Driver 类里有一个静态代码块,它会在 Driver 类被加载的时候执行。这个静态代码块会将 mysql 驱动实例注册到全局的 jdbc 驱动管理器里。

class Driver {
  static {
    try {
       java.sql.DriverManager.registerDriver(new Driver());
    } catch (SQLException E) {
       throw new RuntimeException("Can't register driver!");
    }
  }
  ...
}

forName 方法同样也是使用调用者 Class 对象的 ClassLoader 来加载目标类。不过 forName 还提供了多参数版本,可以指定使用哪个 ClassLoader 来加载

Class<?> forName(String name, boolean initialize, ClassLoader cl)

通过这种形式的 forName 方法可以突破内置加载器的限制,通过使用自定类加载器允许我们自由加载其它任意来源的类库。根据 ClassLoader 的传递性,目标类库传递引用到的其它类库也将会使用自定义加载器加载。

为什么要破坏双亲委派模型

这就要说到双亲委派的缺点了:顶层的classLoader无法访问底层的classLoader所加载的资源。

拿jdbc为例,jdbc驱动DriverManager,DriverManager是在rt.jar包中,是由顶层启动类加载器BootstrapClassLoader所加载,而mysql的驱动com.mysql.jdbc.Driver是由应用类加载器ApplicationClassLoader加载的,而 JDBC 运行时又需要去访问子类加载器加载的驱动。所以如果用启动类加载器加载底层加载器负责的com.mysql.jdbc.Driver,肯定加载不到(因为一般情况下启动类加载器只加载rt.jar包中的类

如何解决?破坏双亲委派模型

DriverManager加载时,会执行静态代码块,在静态代码块中,会执行loadInitialDrivers方法。而这个方法中会加载对应的驱动类。

public class DriverManager {
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
    private static void loadInitialDrivers() {
        // 省略部分代码
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                // 根据配置文件加载驱动实现类
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });
        // 省略部分代码
    }
}
​

我们就看他使用的是哪种类型的ClassLoader,可以看到通过执行Thread.currentThread().getContextClassLoader()获取了线程上下文加载器

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

ExtClassLoader和AppClassLoader都是通过Launcher类来创建的,在Launcher类的构造函数中我们可以看到线程上下文类加载器设置为AppClassLoader

public class Launcher {
​
    public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }
​
        try {
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }
​
        // 设置线程上下文类加载器为AppClassLoader
        Thread.currentThread().setContextClassLoader(this.loader);
​
        // 省略部分代码
​
    }
}

很明显,线程上下文类加载器让父类加载器能通过调用子类加载器来加载类,这打破了双亲委派模型的原则

class.forName vs ClassLoader.loadClass

类加载过程:加载、链接(验证、准备、解析)、初始化、使用、卸载

class.forName获取的对象是经过初始化的,因此会执行类中的静态方法块,调用forname0方法,传入initialize为true。

@CallerSensitive
public static Class<?> forName(String className)
    throws ClassNotFoundException {
    Class<?> caller = Reflection.getCallerClass();
    return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
​
private static native Class<?> forName0(String name, boolean initialize,
                                            ClassLoader loader,
                                            Class<?> caller)
        throws ClassNotFoundException;

classLoader.loadClass是没有经过链接的

public Class<?> loadClass(String name) throws ClassNotFoundException {
    // false,不进行链接
    return loadClass(name, false);
}
​
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    ...
        if (resolve) {
            resolveClass(c);
        }
    ...
}
​
protected final void resolveClass(Class<?> c) {
    resolveClass0(c);
}
// Links the specified class. 链接对应的class
private native void resolveClass0(Class<?> c);
​

对于例子:

class Test {
    static { 
        System.out.print("loaded");
    }
}

使用 Class.forName 加载 Test 类会输出 "loaded",而使用 ClassLoader.loadClass 加载则不会输出 "loaded"。ClassLoader.loadClass 会装载类到加载节点,不会执行链接操作,因此不会执行类中的静态方法块,在代码中调用 loadClass 时传入的 resolve 为 false,表示不链接类

newInstance和new的对比

总结:

newInstance(): 弱类型,低效率,只能调用无参构造。 new: 强类型,相对高效,能调用任何public构造。

Class.forName(“”)返回的是类。 Class.forName(“”).newInstance()返回的是object 。

例子:

String className = "Example";
class c = Class.forName(className);
factory = (ExampleInterface)c.newInstance();

优化后:

String className = readfromXMlConfig;//从xml 配置文件中获得字符串
class c = Class.forName(className);
factory = (ExampleInterface)c.newInstance();

可以看到,newInstance()实际上是把new这个方式分解为两步,即首先调用Class加载方法加载某个类,得到初始化完成的类对象,然后调用newInstance进行实例化。实现动态创建对象,灵活解耦