ClassLoader
ClassLoader 源码分析
ClassLoader 解决的问题:把二进制的 class 字节流按需变成 JVM 里的 Class 对象,并且保证核心类库不被冒名顶替。核心思路是双亲委派——加载请求先逐级交给父加载器,全都找不到才轮到自己,而这套委派逻辑用模板方法写死在 loadClass 里,留给子类的扩展点只有 findClass。
// 基于 JDK 25 (本地 JAVA_Source 仓), java.lang.ClassLoader —— 双亲委派主线
public abstract class ClassLoader {
// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent; // 委派意义上的父, 不是继承; VM 硬编码了它的字段偏移, 新字段只能加在它后面
// ... 其余字段与构造器省略, 见下一段
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false); // 对外入口默认 resolve=false: 只加载不主动链接
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) { // 并行加载器按类名细分锁, 否则锁整个 loader 实例
Class<?> c = findLoadedClass(name); // 1. 先查缓存: 自己作为发起者加载过就直接返回
if (c == null) {
long t0 = System.nanoTime(); // 只为下面 PerfCounter 统计委派耗时, 与加载逻辑无关
try {
if (parent != null) {
c = parent.loadClass(name, false); // 2. 向上委派, 递归直到顶; 注意父加载时不 resolve
} else {
c = findBootstrapClassOrNull(name); // 3. parent==null 代表 Bootstrap, 它在 VM 内部没有 Java 对象
}
} catch (ClassNotFoundException e) {
// 父加载器找不到是正常流程, 吞掉异常轮到自己; 这就是为什么自定义加载失败的栈里常有一层被吞的 CNFE
}
if (c == null) {
long t1 = System.nanoTime();
c = findClass(name); // 4. 全链路都没有, 才调子类的 findClass 真正去读字节码
// ... PerfCounter 三行统计代码省略
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
// 唯一推荐的扩展点: 默认实现直接抛 CNFE, 逼着子类去实现"怎么找字节码"
// 只重写它而不动 loadClass, 委派逻辑就原封不动地保留了
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
static Class<?> findBootstrapClassOrNull(String name) {
if (!checkName(name)) return null;
return findBootstrapClass(name); // 进 VM 查 Bootstrap 加载的类, 查不到返回 null 而非抛异常
}
private static native Class<?> findBootstrapClass(String name);
protected final Class<?> findLoadedClass(String name) { // final: 缓存查询不许子类篡改
if (!checkName(name))
return null;
return findLoadedClass0(name); // 查 VM 里"以本 loader 为发起者"的加载记录, 委派给父成功的也算
}
private final native Class<?> findLoadedClass0(String name);
protected final void resolveClass(Class<?> c) {
if (c == null) {
throw new NullPointerException(); // 坑: 现在是空操作, 链接由 VM 首次使用时按需完成, "resolve 阶段"不由它触发
}
}
}// 基于 JDK 25 (本地 JAVA_Source 仓), java.lang.ClassLoader —— 并行加载锁
public abstract class ClassLoader {
// 类名 -> 锁对象。VM 同时用这个字段是否为 null 来判断 loader 是否 parallel capable
private final ConcurrentHashMap<String, Object> parallelLockMap;
private ClassLoader(Void unused, String name, ClassLoader parent) {
this.name = name;
this.parent = parent;
this.unnamedModule = new Module(this);
if (ParallelLoaders.isRegistered(this.getClass())) { // 必须在 static 块里 registerAsParallelCapable, 实例化后再注册无效
parallelLockMap = new ConcurrentHashMap<>();
assertionLock = new Object();
} else {
parallelLockMap = null; // 未注册: 退化为锁整个 loader 实例
assertionLock = this;
}
this.package2certs = new ConcurrentHashMap<>();
this.nameAndId = nameAndId(this);
}
protected Object getClassLoadingLock(String className) {
Object lock = this; // 默认粗粒度: 整个 loader 一把锁
if (parallelLockMap != null) {
Object newLock = new Object();
lock = parallelLockMap.putIfAbsent(className, newLock); // 每个类名一把锁, 不同类可并发加载
if (lock == null) {
lock = newLock;
}
}
return lock;
}
}// 基于 JDK 25 (本地 JAVA_Source 仓), java.lang.ClassLoader —— defineClass 链: byte[] 变 Class
public abstract class ClassLoader {
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null); // final: 字节码进 JVM 的口子不许子类绕过安检
}
private ProtectionDomain preDefineClass(String name,
ProtectionDomain pd)
{
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);
if ((name != null) && name.startsWith("java.")
&& this != getBuiltinPlatformClassLoader()) { // Java 层就堵死伪造 java.* 的路, invoke 包的防伪检查依赖这条不变式
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
if (pd == null) {
pd = defaultDomain;
}
if (name != null) {
checkCerts(name, pd.getCodeSource()); // 同包必须同签名者, 防止往已签名的包里塞恶意类
}
return pd;
}
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(this, name, b, off, len, protectionDomain, source); // 真正干活在 VM: 解析+校验类文件, 记录 defining loader
postDefineClass(c, protectionDomain);
return c;
}
static native Class<?> defineClass1(ClassLoader loader, String name, byte[] b, int off, int len,
ProtectionDomain pd, String source);
}原理串讲
以一次典型加载走通全链路:业务代码触发 loader.loadClass("com.foo.Bar"),进入 loadClass(name, false)。
第一件事是 getClassLoadingLock(name) 拿锁——如果这个 loader 在 static 块里调过 registerAsParallelCapable(),锁是 parallelLockMap 里按类名分配的独立 Object,不同类名可以并发加载;否则锁就是 loader 自己。
为什么要按类名细分?因为在 OSGi 这类非层次委派环境里,两个 loader 可能互相委派:A 持有自己的实例锁去请求 B,B 同时持有自己的锁反过来请求 A,一把粗锁直接死锁。
类名粒度的锁把”加载 X”和”加载 Y”解耦,环形委派只要不落在同一个类名上就不会互相卡死。
拿到锁后先 findLoadedClass(name),它透过 native 的 findLoadedClass0 查 VM 的记录:只要本 loader 曾作为发起加载器(initiating loader)加载过这个名字,直接返回缓存的 Class,这保证了同一个 loader 对同一个名字永远给出同一个 Class 对象。
缓存未命中才开始委派:parent != null 就递归调 parent.loadClass(name, false),一路爬到 AppClassLoader、PlatformClassLoader;到顶时 parent == null,改调 findBootstrapClassOrNull(name) 进 VM 查 Bootstrap。
为什么 Bootstrap 用 null 表示而不给个对象?因为它是 VM 自身的一部分(HotSpot 里是 C++ 实现),在 Java 堆里根本没有对应实例,null 就是它在 Java 世界的名字——这也是 String.class.getClassLoader() 返回 null 的原因。
整条父链都返回 null 或抛出 ClassNotFoundException(被 catch 静默吞掉,父找不到是正常分支),才轮到 findClass(name)。
这里是第二个”为什么这么设计”:委派顺序写死在 loadClass 里,而扩展点单独抽成 findClass,是标准的模板方法模式——JDK 不信任子类会自觉维护委派,干脆让子类根本碰不到委派逻辑就能完成自定义加载。
你写类加载器时重写 findClass,从磁盘/网络/解密后的字节流拿到 byte[],然后调 defineClass。
defineClass 先过 preDefineClass 安检:checkName 拦非法类名,接着是那条著名的 java. 前缀检查——普通 loader 想定义 java.lang.Object 直接 SecurityException。
这个检查放在 Java 层而 VM 层还有一道(Bootstrap 校验包归属),双保险是因为 java.lang.invoke.MemberName 的防伪逻辑整个建立在”java.* 不可能被冒充”这条不变式上,一旦破了就是任意代码提权。
checkCerts 再保证同一个包里所有类的签名证书一致。安检通过后 defineClass1 进 VM:解析类文件格式、字节码校验、创建 Class 对象,并把本 loader 记为 defining loader。
最后 postDefineClass 补登记包信息和签名者。回到 loadClass 尾部,resolve 为 true 会调 resolveClass(c)——但注意看源码,它现在只剩判空,链接(验证/准备/解析)早已由 VM 推迟到首次使用时按需进行,这个方法名留着只是兼容历史。
设计取舍
- 双亲委派是约定不是强制:
loadClass只是 protected 而非 final,Tomcat 的 Webapp 加载器、SPI(线程上下文加载器)都合法地打破了它。真正 final 的是findLoadedClass和defineClass这两个安全底线。 - 类的唯一标识是「类名 + defining loader」二元组:同一个 class 文件被两个 loader 各自 define,得到两个互不兼容的 Class,
instanceof为 false、强转抛ClassCastException——热部署框架的经典坑。 - 重写
findClass是”接入”委派,重写loadClass是”改写”委派;99% 的需求(自定义字节码来源、加密类)只需要前者。 - 委派链上父加载器一律传
resolve=false,链接的决定权留给最初的发起者;而如今resolveClass本身已是空操作,实际链接时机完全由 VM 掌控。 - 自定义并行加载器必须在 static 初始化块里调
registerAsParallelCapable(),且要求全部父类也已注册;实例创建之后parallelLockMap已定型,再注册无效。