Java ClassLoader

 

ClassLoader 是 Java 届最为神秘的技术之一,无数人被它伤透了脑筋,摸不清门道究竟在哪里。网上的文章也是一篇又一篇,通过本人的亲自鉴定,绝大部份内容都是在误导别人。本文我带读者完全吃透 ClassLoader,之后其它的相关文章大家能够没必要再细看了。java

ClassLoader 作什么的?

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

有不少字节码加密技术就是依靠定制 ClassLoader 来实现的。先使用工具对字节码文件进行加密,运行时使用定制的 ClassLoader 先解密文件内容再加载这些解密后的字节码。sql

每一个 Class 对象的内部都有一个 classLoader 字段来标识本身是由哪一个 ClassLoader 加载的。ClassLoader 就像一个容器,里面装了不少已经加载的 Class 对象。数组

class Class<T> { ... private final ClassLoader classLoader; ... } 

 

延迟加载

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

好比你在调用某个类的静态方法时,首先这个类确定是须要被加载的,可是并不会触及这个类的实例字段,那么实例字段的类别 Class 就能够暂时没必要去加载,可是它可能会加载静态字段相关的类别,由于静态方法会访问静态字段。而实例字段的类别须要等到你实例化对象的时候才可能会加载。网络

各司其职

JVM 运行实例中会存在多个 ClassLoader,不一样的 ClassLoader 会从不一样的地方加载字节码文件。它能够从不一样的文件目录加载,也能够从不一样的 jar 文件中加载,也能够从网络上不一样的服务地址来加载。框架

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

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 包。ui

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 时就表示它的父加载器是「根加载器」。若是某个 Class 对象的 classLoader 属性值是 null,那么就表示这个类也是「根加载器」加载的。

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()。

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 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 [I Exception in thread "main" java.lang.ClassNotFoundException: [I ... 

钻石依赖

项目管理上有一个著名的概念叫着「钻石依赖」,是指软件依赖致使同一个软件包的两个版本须要共存而不能冲突。

 

咱们平时使用的 maven 是这样解决钻石依赖的,它会从多个冲突的版本中选择一个来使用,若是不一样的版本之间兼容性很糟糕,那么程序将没法正常编译运行。Maven 这种形式叫「扁平化」依赖管理。

 

使用 ClassLoader 能够解决钻石依赖问题。不一样版本的软件包使用不一样的 ClassLoader 来加载,位于不一样 ClassLoader 中名称同样的类其实是不一样的类。下面让咱们使用 URLClassLoader 来尝试一个简单的例子,它默认的父加载器是 AppClassLoader

$ cat ~/source/jcl/v1/Dep.java public class Dep { public void print() { System.out.println("v1"); } } $ cat ~/source/jcl/v2/Dep.java public class Dep { public void print() { System.out.println("v1"); } } $ cat ~/source/jcl/Test.java public 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 Test v1 v2 false 

在这个例子中若是两个 URLClassLoader 指向的路径是同样的,下面这个表达式仍是 false,由于即便是一样的字节码用不一样的 ClassLoader 加载出来的类都不能算同一个类

depv1Class.equals(depv2Class) 

咱们还可让两个不一样版本的 Dep 类实现同一个接口,这样能够避免使用反射的方式来调用 Dep 类里面的方法。

Class<?> depv1Class = v1.loadClass("Dep"); IPrint depv1 = (IPrint)depv1Class.getConstructor().newInstance(); depv1.print() 

ClassLoader 当然能够解决依赖冲突问题,不过它也限制了不一样软件包的操做界面必须使用反射或接口的方式进行动态调用。Maven 没有这种限制,它依赖于虚拟机的默认懒惰加载策略,运行过程当中若是没有显示使用定制的 ClassLoader,那么从头至尾都是在使用 AppClassLoader,而不一样版本的同名类必须使用不一样的 ClassLoader 加载,因此 Maven 不能完美解决钻石依赖。
若是你想知道有没有开源的包管理工具能够解决钻石依赖的,我推荐你了解一下 sofa-ark,它是蚂蚁金服开源的轻量级类隔离框架。

分工与合做

这里咱们从新理解一下 ClassLoader 的意义,它至关于类的命名空间,起到了类隔离的做用。位于同一个 ClassLoader 里面的类名是惟一的,不一样的 ClassLoader 能够持有同名的类。ClassLoader 是类名称的容器,是类的沙箱。

 

不一样的 ClassLoader 之间也会有合做,它们之间的合做是经过 parent 属性和双亲委派机制来完成的。parent 具备更高的加载优先级。除此以外,parent 还表达了一种共享关系,当多个子 ClassLoader 共享同一个 parent 时,那么这个 parent 里面包含的类能够认为是全部子 ClassLoader 共享的。这也是为何 BootstrapClassLoader 被全部的类加载器视为祖先加载器,JVM 核心类库天然应该被共享。

 

Thread.contextClassLoader

若是你稍微阅读过 Thread 的源代码,你会在它的实例字段中发现有一个字段很是特别

class Thread { ... private ClassLoader contextClassLoader; public ClassLoader getContextClassLoader() { return contextClassLoader; } public void setContextClassLoader(ClassLoader cl) { this.contextClassLoader = cl; } ... } 

contextClassLoader「线程上下文类加载器」,这到底是什么东西?

首先 contextClassLoader 是那种须要显示使用的类加载器,若是你没有显示使用它,也就永远不会在任何地方用到它。你可使用下面这种方式来显示使用它

Thread.currentThread().getContextClassLoader().loadClass(name); 

这意味着若是你使用 forName(string name) 方法加载目标类,它不会自动使用 contextClassLoader。那些由于代码上的依赖关系而懒惰加载的类也不会自动使用 contextClassLoader来加载。

其次线程的 contextClassLoader 是从父线程那里继承过来的,所谓父线程就是建立了当前线程的线程。程序启动时的 main 线程的 contextClassLoader 就是 AppClassLoader。这意味着若是没有人工去设置,那么全部的线程的 contextClassLoader 都是 AppClassLoader。

那这个 contextClassLoader 到底是作什么用的?咱们要使用前面提到了类加载器分工与合做的原理来解释它的用途。

它能够作到跨线程共享类,只要它们共享同一个 contextClassLoader。父子线程之间会自动传递 contextClassLoader,因此共享起来将是自动化的。

若是不一样的线程使用不一样的 contextClassLoader,那么不一样的线程使用的类就能够隔离开来。

若是咱们对业务进行划分,不一样的业务使用不一样的线程池,线程池内部共享同一个 contextClassLoader,线程池之间使用不一样的 contextClassLoader,就能够很好的起到隔离保护的做用,避免类版本冲突。

若是咱们不去定制 contextClassLoader,那么全部的线程将会默认使用 AppClassLoader,全部的类都将会是共享的。

线程的 contextClassLoader 使用场合比较罕见,若是上面的逻辑晦涩难懂也没必要过于计较。

JDK9 增长了模块功能以后对类加载器的结构设计作了必定程度的修改,不过类加载器的原理仍是相似的,做为类的容器,它起到类隔离的做用,同时还须要依靠双亲委派机制来创建不一样的类加载器之间的合做关系。

相关文章
相关标签/搜索