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
的相关构造器参数:
CodeSource
PermissionCollection
,如果不为null
,会设置权限为只读,表示权限在使用过程中不再修改;同时检查是否需要设置拥有全部权限。ClassLoader
Principal[]
这样看来,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
前置处理defineClassX
postDefineClass
后置处理
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) |