Java 类加载器源码分析
组织类加载工作:loadClass
当 Java 程序启动的时候,Java 虚拟机会调用 java.lang.ClassLoader#loadClass(java.lang.String) 加载 main 方法所在的类。
1 | public Class<?> loadClass(String name) throws ClassNotFoundException { |
根据注释可知,此方法加载具有指定二进制名称的类,它由 Java 虚拟机调用来解析类引用,调用它等同于调用 loadClass(name, false)。
1 | protected Class<?> loadClass(String name, boolean resolve) |
根据注释可知,java.lang.ClassLoader#loadClass(java.lang.String, boolean) 同样是加载“具有指定二进制名称的类”,此方法的实现按以下顺序搜索类:
- 调用
findLoadedClass(String)以检查该类是否已加载。 - 在父·类加载器上调用
loadClass方法。如果父·类加载器为空,则使用虚拟机内置的类加载器。 - 调用
findClass(String)方法来查找该类。
如果使用上述步骤找到了该类(找到并定义类),并且解析标志为 true,则此方法将对生成的 Class 对象调用 resolveClass(Class) 方法。鼓励 ClassLoader 的子类重写 findClass(String),而不是此方法。除非被重写,否则此方法在整个类加载过程中以 getClassLoadingLock 方法的结果进行同步。
注意:父·类加载器并非父类·类加载器(当前类加载器的父类),而是当前的类加载器的
parent属性被赋值另外一个类加载器实例,其含义更接近于“可以委派类加载工作的另一个类加载器(一个帮忙干活的上级)”。虽然绝大多数说法中,当一个类加载器的parent值为null时,它的父·类加载器是引导类加载器(bootstrap class loader),但是当看到findBootstrapClassOrNull方法时,我有点困惑,因为我以为会看到语义类似于loadClassByBootstrapClassLoader这样的方法名。从注释和代码的语义上看,bootstrap class loader不像是任何一个类加载器的父·类加载器,但是从类加载的机制设计上说,它是,只是因为它并非由 Java 语言编写而成,不能实例化并赋值给parent属性。findBootstrapClassOrNull方法的语义更接近于:当一个类加载器的父·类加载器为null时,将准备加载的目标类先当作启动类(Bootstrap Class)尝试查找,如果找不到就返回null。
怎么并行地加载类 getClassLoadingLock
需要加载的类可能很多很多,我们很容易想到如果可以并行地加载类就好了。显然,JDK 的编写者考虑到了这一点。
此方法返回类加载操作的锁对象。为了向后兼容,此方法的默认实现的行为如下。如果此 ClassLoader 对象注册为具备并行能力,则该方法返回与指定类名关联的专用对象。 否则,该方法返回此 ClassLoader 对象。
简单地说,如果 ClassLoader 对象注册为具备并行能力,那么一个 name 一个锁对象,已创建的锁对象保存在 ConcurrentHashMap 类型的 parallelLockMap 中,这样类加载工作可以并行;否则所有类加载工作共用一个锁对象,就是 ClassLoader 对象本身。
这个方案意味着非同名的目标类可以认为在加载时没有冲突?
1 | protected Object getClassLoadingLock(String className) { |
什么是 “ClassLoader 对象注册为具有并行能力”呢?
AppClassLoader 中有一段 static 代码。事实上 java.lang.ClassLoader#registerAsParallelCapable 是将 ClassLoader 对象注册为具有并行能力唯一的入口。因此,所有想要注册为具有并行能力的 ClassLoader 都需要调用一次该方法。
1 | static { |
java.lang.ClassLoader#registerAsParallelCapable 方法有一个注解 @CallerSensitive,这是因为它的代码中调用的 native 方法 sun.reflect.Reflection#getCallerClass() 方法。由注释可知,当且仅当以下所有条件全部满足时才注册成功:
- 尚未创建调用者的实例(类加载器尚未实例化)
- 调用者的所有超类(
Object类除外)都注册为具有并行能力。
怎么保证这两个条件成立呢?
- 对于第一个条件,可以通过将调用的代码写在
static代码块中来实现。如果写在构造器方法里,并且通过单例模式保证只实例化一次可以吗?答案是不行的,后续会解释这个“注册”行为在构造器方法中是如何被使用以及为何不能写在构造器方法里。 - 对于第二个条件,由于
Java虚拟机加载类时,总是会先尝试加载其父类,又因为加载类时会先调用static代码块,因此父类的static代码块总是先于子类的static代码块。
你可以看到 AppClassLoader->URLClassLoader->SecureClassLoader->ClassLoader 均在 static 代码块实现注册,以保证满足以上两个条件。
注册工作做了什么?
简单地说就是保存了类加载器所属 Class 的 Set。
1 |
|
方法 java.lang.ClassLoader.ParallelLoaders#register。ParallelLoaders 封装了一组具有并行能力的加载器类型。就是持有 ClassLoader 的 Class 实例的集合,并保证添加时加同步锁。
1 | // private 修饰,只有其外部类 ClassLoader 才可以使用 |
“注册”怎么和锁产生联系?
但是以上的注册过程只是起到一个“标记”作用,没有涉及和锁相关的代码,那么这个“标记”是怎么和真正的锁产生联系呢?ClassLoader 提供了三个构造器方法:
1 | private ClassLoader(Void unused, ClassLoader parent) { |
ClassLoader 的构造器方法最终都调用 private 修饰的 java.lang.ClassLoader#ClassLoader(java.lang.Void, java.lang.ClassLoader),又因为父类的构造器方法总是先于子类的构造器方法被执行,这样一来,所有继承 ClassLoader 的类加载器在创建的时候都会根据在创建实例之前是否注册为具有并行能力而做不同的操作。
为什么注册的代码不能写在构造器方法里?
使用“注册”的代码也解释了 java.lang.ClassLoader#registerAsParallelCapable 为了满足调用成功的第一个条件为什么不能写在构造器方法中,因为使用这个机制的代码先于你在子类构造器方法里编写的代码被执行。
同时,不论是 loadLoader 还是 getClassLoadingLock 都是由 protect 修饰,允许子类重写,来自定义并行加载类的能力。
todo: 讨论自定义类加载器的时候,印象里似乎对并行加载类的提及比较少,之后留意一下。
检查目标类是否已加载
加载类之前显然需要检查目标类是否已加载,这项工作最终是交给 native 方法,在虚拟机中执行,就像在黑盒中一样。
todo: 不同类加载器同一个类名会如何判定?
1 | protected final Class<?> findLoadedClass(String name) { |
保证核心类库的安全性:双亲委派模型
正如在代码和注释中所看到的,正常情况下,类的加载工作先委派给自己的父·类加载器,即 parent 属性的值——另一个类加载器实例。一层一层向上委派直到 parent 为 null,代表类加载工作会尝试先委派给虚拟机内建的 bootstrap class loader 处理,然后由 bootstrap class loader 首先尝试加载。如果被委派方加载失败,委派方会自己再尝试加载。
正常加载类的是应用类加载器 AppClassLoader,它的 parent 为 ExtClassLoader,ExtClassLoader 的 parent 为 null。
在网上也能看到有人提到以前大家称之为“父·类加载器委派机制”,“双亲”一词易引人误解。
为什么要用这套奇怪的机制
这样设计很明显的一个目的就是保证核心类库的类加载安全性。比如 Object 类,设计者不希望编写代码的人重新写一个 Object 类并加载到 Java 虚拟机中,但是加载类的本质就是读取字节数据传递给 Java 虚拟机创建一个 Class 实例,使用这套机制的目的之一就是为了让核心类库先加载,同时先加载的类不会再次被加载。
通常流程如下:
AppClassLoader调用loadClass方法,先委派给ExtClassLoader。ExtClassLoader调用loadClass方法,先委派给bootstrap class loader。bootstrap class loader在其设置的类路径中无法找到BananaTest类,抛出ClassNotFoundException异常。ExtClassLoader捕获异常,然后自己调用findClass方法尝试进行加载。ExtClassLoader在其设置的类路径中无法找到BananaTest类,抛出ClassNotFoundException异常。AppClassLoader捕获异常,然后自己调用findClass方法尝试进行加载。
注释中提到鼓励重写 findClass 方法而不是 loadClass,因为正是该方法实现了所谓的“双亲委派模型”,java.lang.ClassLoader#findClass 实现了如何查找加载类。如果不是专门为了破坏这个类加载模型,应该选择重写 findClass;其次是因为该方法中涉及并行加载类的机制。
查找类资源:findClass
默认情况下,类加载器在自己尝试进行加载时,会调用 java.lang.ClassLoader#findClass 方法,该方法由子类重写。AppClassLoader 和 ExtClassLoader 都是继承 URLClassLoader,而 URLClassLoader 重写了 findClass 方法。根据注释可知,该方法会从 URL 搜索路径查找并加载具有指定名称的类。任何引用 Jar 文件的 URL 都会根据需要加载并打开,直到找到该类。
过程如下:
- 将
name转换为path,比如com.example.BananaTest转换为com/example/BananaTest.class。 - 使用
URL搜索路径URLClassPath和path中获取Resource,本质上就是轮流将可能存放的目录列表拼接上文件路径进行查找。 - 调用
URLClassLoader的私有方法defineClass,该方法调用父类SecureClassLoader的defineClass方法。
1 | protected Class<?> findClass(final String name) |
查找类的目录列表:URLClassPath
URLClassLoader 拥有一个 URLClassPath 类型的属性 ucp。由注释可知,URLClassPath 类用于维护一个 URL 的搜索路径,以便从 Jar 文件和目录中加载类和资源。URLClassPath 的核心构造器方法:
1 | public URLClassPath(URL[] urls, |
URLClassPath#getResource
URLClassLoader 调用 sun.misc.URLClassPath#getResource(java.lang.String, boolean) 方法获取指定名称对应的资源。根据注释,该方法会查找 URL 搜索路径上的第一个资源,如果找不到资源,则返回 null。
显然,这里的 Loader 不是我们前面提到的类加载器。Loader 是 URLClassPath 的内部类,用于表示根据一个基本 URL 创建的资源和类的加载器。也就是说一个基本 URL 对应一个 Loader。
1 | public Resource getResource(String name, boolean check) { |
URLClassPath#getNextLoader
获取下一个 Loader,其实根据 index 从一个存放已创建 Loader 的 ArrayList 中获取。
1 | private synchronized Loader getNextLoader(int[] cache, int index) { |
URLClassPath#getLoader(int)
- 用
index到存放已创建Loader的列表中去获取(调用方传入的index从0开始不断递增直到超过范围)。 - 如果
index超过范围,说明已有的Loader都找不到目标Resource,需要到未打开的URL中查找。 - 从未打开的
URL中取出(pop)一个来创建Loader,如果urls已经为空,则返回null。
1 | private synchronized Loader getLoader(int index) { |
URLClassPath#getLoader(java.net.URL)
根据指定的 URL 创建 Loader,不同类型的 URL 会返回不同具体实现的 Loader。
- 如果
URL不是以/结尾,认为是Jar文件,则返回JarLoader类型,比如file:/C:/Users/xxx/.jdks/corretto-1.8.0_342/jre/lib/rt.jar。 - 如果
URL以/结尾,且协议为file,则返回FileLoader类型,比如file:/C:/Users/xxx/IdeaProjects/java-test/target/classes/。 - 如果
URL以/结尾,且协议不会file,则返回Loader类型。
1 | private Loader getLoader(final URL url) throws IOException { |
URLClassPath.FileLoader#getResource
以 FileLoader 的 getResource 为例,如果文件找到了,就会将文件包装成一个 FileInputStream,再将 FileInputStream 包装成一个 Resource 返回。
1 | Resource getResource(final String name, boolean check) { |
ClassLoader 的搜索路径
从上文可知,ClassLoader 调用 findClass 方法查找类的时候,并不是漫无目的地查找,而是根据设置的类路径进行查找,不同的 ClassLoader 有不同的类路径。
以下是通过 IDEA 启动 Java 程序时的命令,可以看到其中通过 -classpath 指定了应用·类加载器 AppClassLoader 的类路径,该类路径除了包含常规的 JRE 的文件路径外,还额外添加了当前 maven 工程编译生成的 target\classes 目录。
1 | C:\Users\xxx\.jdks\corretto-1.8.0_342\bin\java.exe -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:52959,suspend=y,server=n -javaagent:C:\Users\xxx\AppData\Local\JetBrains\IntelliJIdea2022.3\captureAgent\debugger-agent.jar -Dfile.encoding=UTF-8 -classpath "C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\charsets.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\access-bridge-64.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\cldrdata.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\dnsns.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\jaccess.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\jfxrt.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\localedata.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\nashorn.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\sunec.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\sunjce_provider.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\sunmscapi.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\sunpkcs11.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\zipfs.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\jce.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\jfr.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\jfxswt.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\jsse.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\management-agent.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\resources.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\rt.jar;C:\Users\xxx\IdeaProjects\java-test\target\classes;C:\Program Files\JetBrains\IntelliJ IDEA 2022.3.3\lib\idea_rt.jar" org.example.BananaTest |
bootstrap class loader
启动·类加载器 bootstrap class loader,加载核心类库,即 <JRE_HOME>/lib 目录中的部分类库,如 rt.jar,只有名字符合要求的 jar 才能被识别。 启动 Java 虚拟机时可以通过选项 -Xbootclasspath 修改默认的类路径,有三种使用方式:
-Xbootclasspath::完全覆盖核心类库的类路径,不常用,除非重写核心类库。-Xbootclasspath/a:以后缀的方式拼接在原搜索路径后面,常用。-Xbootclasspath/p:以前缀的方式拼接再原搜索路径前面.不常用,避免引起不必要的冲突。
在 IDEA 中编辑启动配置,添加 VM 选项,-Xbootclasspath:C:\Software,里面没有类文件,启动虚拟机失败,提示:
1 | Error occurred during initialization of VM |
ExtClassLoader
扩展·类加载器 ExtClassLoader,加载 <JRE_HOME>/lib/ext/ 目录中的类库。启动 Java 虚拟机时可以通过选项 -Djava.ext.dirs 修改默认的类路径。显然修改不当同样可能会引起 Java 程序的异常。
AppClassLoader
应用·类加载器 AppClassLoader ,加载应用级别的搜索路径中的类库。可以使用系统的环境变量 CLASSPATH 的值,也可以在启动 Java 虚拟机时通过选项 -classpath 修改。
CLASSPATH 在 Windows 中,多个文件路径使用分号 ; 分隔,而 Linux 中则使用冒号 : 分隔。以下例子表示当前目录和另一个文件路径拼接而成的类路径。
- Windows:
.;C:\path\to\classes - Linux:
.:/path/to/classes
事实上,AppClassLoader 最终的类路径,不仅仅包含 -classpath 的值,还会包含 -javaagent 指定的值。
字节数据转换为 Class 实例:defineClass
方法 defineClass,顾名思义,就是定义类,将字节数据转换为 Class 实例。在 ClassLoader 以及其子类中有很多同名方法,方法内各种处理和包装,最终都是为了使用 name 和字节数据等参数,调用 native 方法获得一个 Class 实例。
以下是定义类时最终可能调用的 native 方法。
1 | private native Class<?> defineClass0(String name, byte[] b, int off, int len, |
其方法参数有:
name,目标类的名称。byte[]或ByteBuffer类型的字节数据,off和len只是为了定位传入的字节数组中关于目标类的字节数据,通常分别是 0 和字节数组的长度,毕竟专门构造一个包含无关数据的字节数组很无聊。ProtectionDomain,保护域,todo:source,CodeSource的位置。
defineClass 方法的调用过程,其实就是从 URLClassLoader 开始,一层一层处理后再调用父类的 defineClass 方法,分别经过了 SecureClassLoader 和 ClassLoader。
URLClassLoader#defineClass
此方法是再 URLClassLoader 的 findClass 方法中,获得正确的 Resource 之后调用的,由 private 修饰。根据注释,它使用从指定资源获取的类字节来定义类,生成的类必须先解析才能使用。
1 | private Class<?> defineClass(String name, Resource res) throws IOException { |
Resource 类提供了 getBytes 方法,此方法以字节数组的形式返回字节数据。
1 | public byte[] getBytes() throws IOException { |
在 getByteBuffer 之后会缓存 InputStream 以便调用 getBytes 时使用,方法由 synchronized 修饰。
1 | private synchronized InputStream cachedInputStream() throws IOException { |
在这个例子中,Resource 的实例是 URLClassPath 中的匿名类 FileLoader 以 Resource 的匿名类的方式创建的。
1 | public InputStream getInputStream() throws IOException |
SecureClassLoader#defineClass
URLClassLoader 继承自 SecureClassLoader,SecureClassLoader 提供并重载了 defineClass 方法,两个方法的注释均比代码长得多。
由注释可知,方法的作用是将字节数据(byte[] 类型或者 ByteBuffer 类型)转换为 Class 类型的实例,有一个可选的 CodeSource 类型的参数。
1 | protected final Class<?> defineClass(String name, |
方法中只是简单地将 CodeSource 类型的参数转换成 ProtectionDomain 类型,就调用 ClassLoader 的 defineClass 方法。
1 | private ProtectionDomain getProtectionDomain(CodeSource cs) { |
getPermissions
根据注释可知,此方法会返回给定 CodeSource 对象的权限。此方法由 protect 修饰,AppClassLoader 和 URLClassLoader 都有重写。当前 ClassLoader 是 AppClassLoader。
AppClassLoader#getPermissions,添加允许从类路径加载的任何类退出 VM的权限。
1 | protected PermissionCollection getPermissions(CodeSource codesource) |
SecureClassLoader#getPermissions,添加一个读文件或读目录的权限。
1 | protected PermissionCollection getPermissions(CodeSource codesource) |
SecureClassLoader#getPermissions,延迟设置权限,在创建 ProtectionDomain 时再设置。
1 | protected PermissionCollection getPermissions(CodeSource codesource) |
ProtectionDomain
ProtectionDomain 的相关构造器参数:
CodeSourcePermissionCollection,如果不为null,会设置权限为只读,表示权限在使用过程中不再修改;同时检查是否需要设置拥有全部权限。ClassLoaderPrincipal[]
这样看来,SecureClassLoader 为了定义类做的处理,就是简单地创建一些关于权限的对象,并保存了 CodeSource->ProtectionDomain 的映射作为缓存。
ClassLoader#defineClass
抽象类 ClassLoader 中最终用于定义类的 native 方法 define0,define1,define2 都是由 private 修饰的,ClassLoader 提供并重载了 defineClass 方法作为使用它们的入口,这些 defineClass 方法都由 protect final 修饰,这意味着这些方法只能被子类使用,并且不能被重写。
1 | protected final Class<?> defineClass(String name, byte[] b, int off, int len) |
主要步骤:
preDefineClass前置处理defineClassXpostDefineClass后置处理
preDefineClass
确定保护域 ProtectionDomain,并检查:
- 未定义
java.*类 - 该类的签名者与包(
package)中其余类的签名者相匹配
1 | private ProtectionDomain preDefineClass(String name, |
defineClassSourceLocation
确定 Class 的 CodeSource 位置。
1 | private String defineClassSourceLocation(ProtectionDomain pd) |
defineClassX 方法
这些 native 方法使用了 name,字节数据,ProtectionDomain 和 source 等参数,像黑盒一样,在虚拟机中定义了一个类。
postDefineClass
在定义类后使用 ProtectionDomain 中的 certs 补充 Class 实例的 signer 信息,猜测在 native 方法 defineClassX 方法中,对 ProtectionDomain 做了一些修改。事实上,从代码上看,将 CodeSource 包装为 ProtectionDomain 传入后,除了 defineClassX 方法外,其他地方都是取出 CodeSource 使用。
1 | private void postDefineClass(Class<?> c, ProtectionDomain pd) |