深入学习 ClassLoader

原文来自 知乎-码洞

ClassLoader是做什么的?

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

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

延迟加载

JVM运行并不是一次就加载所有类,它是按需加载的,也就是延迟加载。程序在运行时首次遇到未加载的类便会调用ClassLoader来加载这些类。加载完成后Class对象就会存在于该ClassLoader中,下次就不需要加载了。

各司其职

JVM运行实例中会存在多个ClassLoader,不同的是ClassLoader会从不同的地方加载字节码文件。它可以从不同的文件目录去加载,也可以从jar文件中加载,也可以从网络上不同的地址去加载。

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

BootStrapClassLoader负责加载JVM运行时核心类库,这些类位于JAVA_HOME/lib、rt.jar 文件中,我们常用的内置库,java.lang、java_io、java_util等都在里面。这个ClassLoader比较特殊一些,是由C代码实现,我们称之为 [根加载器]。

ExtensionClassLoader负责加载JVM拓展类,比如swing系列、内置的js引擎、xml解析器等等,它们的jar包位于JAVA、_HOME/lib/ext/*.jar中。

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

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

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

双亲委派

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


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

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

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

当ClassLoader的parent字段为null时就表示它的父加载器是【根加载器】。如果某个class对象的classLoader属性是null,那个就表示该类也是由【根加载器】加载的。

Class.forName

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

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

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

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

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

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

自定义加载器

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

自定义类加载器不易破坏双亲委派规则,不要轻易覆盖 loadClass 方法。否则可能会导致自定义加载器无法加载内置的核心类库。在使用自定义加载器时,要明确好它的父加载器是谁,将父加载器通过子类的构造器传入。如果父类加载器是 null,那就表示父加载器是「根加载器」。

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

Class.forName vs ClassLoader.loadClass

这两个方法都可以用来加载目标类,它们之间有一个小小的区别,那就是 Class.forName() 方法可以获取原生类型的 Class,而 ClassLoader.loadClass() 则会报错。

分工与合作

这里我们重新理解一下ClassLoader的意义,它相当于类的命名空间,起到了类隔离的作用。位于同一个ClassLoader里面的类名是唯一的,不同的ClassLoader可以持有相同的类。
ClassLoader是类名称的容器,是类的沙箱。

不同的ClassLoader之间也会有合作,他们之间的合作是通过parent属性和双亲委派机制来完成的。parent具有更高的加载优先级。除此之外,parent还表达了一种共享关系,当多个子ClassLoader共享一个parent时,那么这个parent里面包含的类可以认为是所有子ClassLoader共享的。这也是为什么BootStrapClassLoader被所有的加载器视为祖先加载器,JVM核心类库自然应该是共享。