ClassLoader 做什么的?
class Class{ ... private final ClassLoader classLoader; ...}
延迟加载
各司其职
JVM 运行实例中会存在多个 ClassLoader,不同的 ClassLoader 会从不同的地方加载字节码文件。它可以从不同的文件目录加载,也可以从不同的 jar 文件中加载,也可以从网络上不同的静态文件服务器来下载字节码再加载。
JVM 中内置了三个重要的 ClassLoader,分别是 BootstrapClassLoader、ExtensionClassLoader 和 AppClassLoader。
ClassLoader 传递性
双亲委派
class ClassLoader { ... private final 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 里面有三个重要的方法 loadClass()、findClass() 和 defineClass()。
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); }}
// ClassLoader 构造器protected ClassLoader(String name, ClassLoader parent);
双亲委派规则可能会变成三亲委派,四亲委派,取决于你使用的父加载器是谁,它会一直递归委派到根加载器。
Class.forName vs ClassLoader.loadClass
这两个方法都可以用来加载目标类,它们之间有一个小小的区别,那就是 Class.forName() 方法可以获取原生类型的 Class,而 ClassLoader.loadClass() 则会报错。
Class x = Class.forName("[I");System.out.println(x);x = ClassLoader.getSystemClassLoader().loadClass("[I");System.out.println(x);---------------------class [IException in thread "main" java.lang.ClassNotFoundException: [I...
钻石依赖
项目管理上有一个著名的概念叫着「钻石依赖」,是指软件依赖导致同一个软件包的两个版本需要共存而不能冲突。
我们平时使用的 maven 是这样解决钻石依赖的,它会从多个冲突的版本中选择一个来使用,如果不同的版本之间兼容性很糟糕,那么程序将无法正常编译运行。Maven 这种形式叫「扁平化」依赖管理。
$ cat ~/source/jcl/v1/Dep.javapublic class Dep { public void print() { System.out.println("v1"); }}$ cat ~/source/jcl/v2/Dep.javapublic class Dep { public void print() { System.out.println("v1"); }}$ cat ~/source/jcl/Test.javapublic class Test { public static void main(String[] args) throws Exception { String v1dir = "file:///Users/qianwp/source/jcl/v1/"; String v2dir = "file:///Users/qianwp/source/jcl/v2/"; URLClassLoader v1 = new URLClassLoader(new URL[]{new URL(v1dir)}); URLClassLoader v2 = new URLClassLoader(new URL[]{new URL(v2dir)}); Class depv1Class = v1.loadClass("Dep"); Object depv1 = depv1Class.getConstructor().newInstance(); depv1Class.getMethod("print").invoke(depv1); Class depv2Class = v2.loadClass("Dep"); Object depv2 = depv2Class.getConstructor().newInstance(); depv2Class.getMethod("print").invoke(depv2); System.out.println(depv1Class.equals(depv2Class)); }}
在运行之前,我们需要对依赖的类库进行编译
$ cd ~/source/jcl/v1$ javac Dep.java$ cd ~/source/jcl/v2$ javac Dep.java$ cd ~/source/jcl$ javac Test.java$ java Testv1v2false
在这个例子中如果两个 URLClassLoader 指向的路径是一样的,下面这个表达式还是 false,因为即使是同样的字节码用不同的 ClassLoader 加载出来的类都不能算同一个类
depv1Class.equals(depv2Class)
我们还可以让两个不同版本的 Dep 类实现同一个接口,这样可以避免使用反射的方式来调用 Dep 类里面的方法。
Class depv1Class = v1.loadClass("Dep");IPrint depv1 = (IPrint)depv1Class.getConstructor().newInstance();depv1.print()
分工与合作
这里我们重新理解一下 ClassLoader 的意义,它相当于类的命名空间,起到了类隔离的作用。位于同一个 ClassLoader 里面的类名是唯一的,不同的 ClassLoader 可以持有同名的类。ClassLoader 是类名称的容器,是类的沙箱。
Thread.contextClassLoader
如果你稍微阅读过 Thread 的源代码,你会在它的实例字段中发现有一个字段非常特别class Thread { ... private ClassLoader contextClassLoader; public ClassLoader getContextClassLoader() { return contextClassLoader; } public void setContextClassLoader(ClassLoader cl) { this.contextClassLoader = cl; } ...}
contextClassLoader「线程上下文类加载器」,这究竟是什么东西?
Thread.currentThread().getContextClassLoader().loadClass(name);
这意味着如果你使用 forName(string name) 方法加载目标类,它不会自动使用 contextClassLoader。那些因为代码上的依赖关系而懒惰加载的类也不会自动使用 contextClassLoader来加载。