ClassLoader是Java中的神秘技术之一。网上也陆续有文章。经我个人鉴定,大部分内容都是误导他人。在这篇文章中,我将带读者彻底了解ClassLoader,以后就不用再看其他相关文章了。类加载器是做什么的?顾名思义就是用来加载Class的。它负责将字节码形式的Class转换为内存形式的Class对象。字节码可以来自磁盘文件*.class,也可以是jar包中的*.class,也可以是远程服务器提供的字节流。字节码的本质是一个字节数组[]byte,具有特定复杂的内部格式。有很多字节码加密技术都是依赖自定义ClassLoader来实现的。先用工具加密字节码文件,运行时用自定义的ClassLoader解密文件内容再加载解密后的字节码。每个Class对象内部都有一个classLoader字段来标识它是由哪个ClassLoader加载的。ClassLoader就像一个容器,里面装着很多加载的Class对象。classClass{...privatefinalClassLoaderclassLoader;...}延迟加载JVM操作并不是一次性加载所有需要的类,它是按需加载,即延迟加载。程序在运行的时候,会逐渐遇到很多自己不知道的新类。这时候会调用ClassLoader来加载这些类。加载完成后,Class对象会保存在ClassLoader中,下次无需重新加载。比如当你调用某个类的静态方法时,首先要加载这个类,但是不会触及这个类的实例字段,那么这个实例字段的类别类就暂时加载不出来,但可能是加载了静态字段相关的类,因为静态方法访问的是静态字段。在实例化对象之前,可能不会加载实例字段的类别。每个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代码实现的,我们称它为“rootloader”。ExtensionClassLoader负责加载JVM扩展类,如swing系列、内置js引擎、xml解析器等,这些库的名称通常以javax开头,其jar包位于$JAVA_HOME/lib/ext/*。罐。有很多罐子袋。AppClassLoader是直接面向我们用户的加载器。它将加载Classpath环境变量中定义的路径中的jar包和目录。我们自己写的代码和使用的第三方jar包一般都是由它加载的。对于网络上静态文件服务器提供的那些jar包和class文件,jdk内置了URLClassLoader。用户只需将标准化的网络路径传递给构造函数,即可使用URLClassLoader加载远程类库。URLClassLoader不仅可以加载远程类库,还可以加载本地路径的类库,具体取决于构造函数中地址形式的不同。ExtensionClassLoader和AppClassLoader都是URLClassLoader的子类,它们都是从本地文件系统加载类库。AppClassLoader可以通过ClassLoader类提供的静态方法getSystemClassLoader()获取。也就是我们所说的“系统类加载器”。我们用户写的类代码一般都是由它加载的。当我们的main方法执行时,第一个用户类加载器是AppClassLoader。当一个ClassLoader传递程序在运行过程中遇到一个未知的类,它会选择哪个ClassLoader来加载它呢?虚拟机的策略是使用调用者的Class对象的ClassLoader来加载当前未知的类。调用者类对象是什么?遇到这个未知类,虚拟机肯定是在运行方法调用(静态方法或者实例方法),这个方法挂在哪个类上,那么这个类就是调用者Class对象。前面我们提到,每个Class对象都有一个classLoader属性,记录了当前类是谁加载的。因为ClassLoader的传递性,所有懒加载的类都会由最初调用main方法的ClassLoader,即AppClassLoader全权负责。双亲委托我们前面提到,AppClassLoader只负责加载Classpath下的类库。遇到没有加载的系统类库怎么办,AppClassLoader必须把系统类库的加载交给BootstrapClassLoader和ExtensionClassLoader。这就是我们常说的“家长委派”。当AppClassLoader加载未知类名时,它不会立即搜索Classpath。它会先把类名交给ExtensionClassLoader加载。如果ExtensionClassLoader可以加载它,那么AppClassLoader就不用费心了。否则它搜索类路径。当ExtensionClassLoader加载未知类名时,它不会立即搜索ext路径。它会先把类名交给BootstrapClassLoader加载。如果BootstrapClassLoader可以加载它,那么ExtensionClassLoader就不用费心了。否则会搜索ext路径下的jar包。这三个ClassLoader之间形成了级联的父子关系。每个ClassLoader都非常懒惰,试图把工作交给父亲。父亲做不到,他就自己做。每个ClassLoader对象都有一个指向其父加载器的父属性。classClassLoader{...privatefinalClassLoaderparent;...}值得注意的是图中ExtensionClassLoader的parent指针画了一条虚线,因为它的parent的值为null,当parent字段为null时,表示它的parentloader是“根加载器”。如果一个Class对象的classLoader属性值为null,说明这个类也被“根加载器”加载。注意这里的parent不是super也不是父类,而是ClassLoader的一个内部字段。Class.forName我们在使用jdbc驱动的时候,经常会使用Class.forName方法来动态加载驱动类。Class.forName("com.mysql.cj.jdbc.Driver");其原理是在mysql驱动的Driver类中有一个静态代码块,在Driver类加载时执行。此静态代码块将向全局jdbc驱动程序管理器注册mysql驱动程序实例。classDriver{static{try{java.sql.DriverManager.registerDriver(newDriver());}catch(SQLExceptionE){thrownewRuntimeException("Can'tregisterdriver!");}}...}forName方法也使用调用者Class对象加载目标类的ClassLoader。不过forName也提供了多参数的版本,可以指定使用哪个ClassLoader来加载。Class>forName(Stringname,booleaninitialize,ClassLoadercl)可以通过这种形式的forName方法突破内置加载器的限制,让我们可以通过自定义类加载器自由加载任何来源的其他类库。根据ClassLoader的传递性,传递引用目标类库的其他类库也会被自定义加载器加载。自定义加载器ClassLoader中有3个重要的方法:loadClass()、findClass()和defineClass()。loadClass()方法是加载目标类的入口点。它首先会检查目标类是否已经加载到当前的ClassLoader及其父类中。如果没有找到,父母会尝试加载它。如果两个父类都无法加载,它将调用findClass()让自定义加载器自己加载目标类。ClassLoader的findClass()方法需要被子类重写,不同的loader会使用不同的逻辑获取目标类的字节码。得到字节码后,调用defineClass()方法将字节码转化为Class对象。下面我用伪代码表达基本过程:classClassLoader{//加载入口,定义双亲委托规则ClassloadClass(Stringname){//Classt是否已经加载=this.findFromLoaded(name);if(t==null){//交给父母t=this.parent.loadClass(name)}if(t==null){//父母做不到,只能靠自己t=this.findClass(name);}return;}//给子类实现ClassfindClass(Stringname){throwClassNotFoundException();}//组装Class对象ClassdefineClass(byte[]code,Stringname){returnbuildClassFromCode(code,name);}}classCustomClassLoaderextendsClassLoader{ClassfindClass(Stringname){//查找字节码byte[]code=findCodeFromSomewhere(name);//AssemblyClassobjectreturnthis.defineClass(code,name);}}自定义类加载器不容易破坏双亲委派规则,千万不要轻松覆盖loadClass方法。否则,自定义加载器可能无法加载内置的核心类库。使用自定义加载器时,需要明确其父加载器是谁,通过子类的构造函数传递父加载器。如果父类加载器为null,则表示父类加载器是“根加载器”。//ClassLoader构造函数protectedClassLoader(Stringname,ClassLoaderparent);双亲委托规则可能会变成三亲委托,四亲委托,取决于你使用的父加载器,它总是会递归地委托给根加载器。Class.forNamevsClassLoader.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[IExceptioninthread"main"java.lang.ClassNotFoundException:[I...钻石依赖项目管理有一个众所周知的概念所谓“钻石依赖”是指导致同一个软件包的两个版本共存而不冲突的软件依赖。我们平时使用的maven就是这样解决钻石依赖的。它将选择多个冲突版本中的一个来使用。如果不同版本之间的兼容性不好,程序将无法正常编译运行。这种形式的Maven称为“平面”依赖管理。使用ClassLoader可以解决菱形依赖问题。不同版本的软件包使用不同的ClassLoader加载,不同ClassLoader中的同名类实际上是不同的类。让我们尝试一个使用URLClassLoader的简单示例,它的默认父级是AppClassLoader。$cat~/source/jcl/v1/Dep.javapublicclassDep{publicvoidprint(){System.out.println("v1");}}$cat~/source/jcl/v2/Dep.javapublicclassDep{publicvoidprint(){System.out.println("v1");}}$cat~/source/jcl/Test.javapublicclassTest{publicstaticvoidmain(String[]args)throwsException{Stringv1dir="file:///Users/qianwp/source/jcl/v1/";Stringv2dir="file:///Users/qianwp/source/jcl/v2/";URLClassLoaderv1=newURLClassLoader(newURL[]{newURL(v1dir)});URLClassLoaderv2=newURLClassLoader(newURL[]{newURL(v2dir)});Class>depv1Class=v1.loadClass("Dep");Objectdepv1=depv1Class.getConstructor().newInstance();depv1Class.getMethod("print").invoke(depv1);Class>depv2Class=v2.loadClass("Dep");Objectdepv2=depv2Class.getConstructor().newInstance();depv2Class.getMethod("print").invoke(depv2);System.out.println(depv1Class.equals(depv2Class));}}在运行之前,我们需要对依赖的类库进行编译:$cd~/source/jcl/v1$javacDep.java$cd~/source/jcl/v2$javacDep.java$cd~/source/jcl$javacTest.java$javaTestv1v2false在这个例子中,如果两个URLClassLoader指向的路径相同,下面的表达式仍然是false,因为即使是同一个字节码用不同的ClassLoader加载的Class也不能算同一个类班级。Class>depv1Class=v1.loadClass("Dep");IPrintdepv1=(IPrint)depv1Class.getConstructor().newInstance();depv1.print()ClassLoader可以解决依赖冲突,但是也限制了不同软件的操作接口包的必须通过反射或接口的方式动态调用。Maven没有这个限制,它依赖于虚拟机默认的懒加载策略,如果运行时自定义的ClassLoader没有显示,那么从头到尾使用AppClassLoader,不同版本的同名加载必须使用不同的ClassLoader,所以Maven不能***解析菱形依赖。如果想知道有没有开源的包管理工具可以解决钻石依赖,推荐大家了解下sofa-ark,这是蚂蚁金服开源的轻量级类隔离框架。分工合作这里我们重新理解ClassLoader的含义,它相当于一个类的命名空间,起到类隔离的作用。同一个ClassLoader中的类名是唯一的,不同的ClassLoader可以持有同名的类。ClassLoader是类名的容器,也是类的沙箱。不同的ClassLoader之间也会有协作,它们之间的协作是通过parent属性和parent委托机制来完成的。parent有更高的加载优先级。此外,parent还表示共享关系。当多个子类加载器共享同一个父类时,可以认为这个父类中包含的类是所有子类加载器共享的。这也是为什么BootstrapClassLoader被所有类加载器视为祖先加载器,JVM核心类库自然应该共享的原因。Thread.contextClassLoader如果你稍微读过Thread的源码,你会发现它的实例字段中有一个非常特殊的字段。classThread{...privateClassLoadercontextClassLoader;publicClassLoadergetContextClassLoader(){returncontextClassLoader;}publicvoidsetContextClassLoader(ClassLoadercl){this.contextClassLoader=cl;}...}contextClassLoader“线程上下文类加载器”,这是什么?首先contextClassLoader是那种需要显式使用的类加载器,如果你不显式使用它,你永远不会在任何地方使用它。您可以通过以下方式使用它来显示它。Thread.currentThread().getContextClassLoader().loadClass(名称);这意味着如果您使用forName(stringname)方法加载目标类,它不会自动使用contextClassLoader。由于代码依赖而延迟加载的类不会使用contextClassLoader自动加载。其次,线程的contextClassLoader默认继承自父线程。所谓父线程就是创建当前线程的线程。程序启动时主线程的contextClassLoader就是AppClassLoader。这意味着如果没有手动设置,所有线程的contextClassLoader都会是AppClassLoader。那么这个contextClassLoader到底是用来做什么的呢?我们将使用前面提到的类加载器分工协作的原理来解释它的使用。只要它们共享相同的contextClassLoader,就可以跨线程共享类。contextClassLoader在父线程和子线程之间自动传递,因此共享将是自动的。如果不同的线程使用不同的contextClassLoader,可以隔离不同线程使用的类。如果我们划分业务,不同的业务使用不同的线程池,线程池内部共享同一个contextClassLoader,线程池之间使用不同的contextClassLoader,可以起到很好的隔离和保护作用,避免类版本冲突。如果我们不自定义contextClassLoader,那么所有的线程都会默认使用AppClassLoader,所有的类都是共享的。线程使用contextClassLoader的情况比较少见,如果上面的逻辑比较晦涩,不用太担心。JDK9加入模块功能后,对类加载器的结构设计进行了一定程度的修改,但类加载器的原理还是类似的。作为类容器,起到类隔离的作用,也需要依赖双亲委托机制。建立不同类加载器之间的协作关系。