基于 :JDK 8(class 文件版本 52.0)+ OpenJDK 8u HotSpot 源码(
classfile/oops模块),辅以《Java 虚拟机规范 SE8》第 5 章把「类加载」拆成三条链路逐层看源码:① 应用主动加载 (
Class.forName/loadClass→ native)② VM 解析期反向加载 (字节码符号引用 →loadClassInternal回调)③ 链接与初始化 (InstanceKlass::link_class/initialize_impl)。读完可回答:一个.class文件从磁盘字节流到可new的对象,中间每一站在哪份源码里、做了什么。
目录
- 全景:一次类加载的生命周期
- 加载入口:谁触发、从哪里进
- [Java 层源码:类加载器体系与双亲委派](#Java 层源码:类加载器体系与双亲委派)
- [HotSpot 层源码:从字节流到 InstanceKlass](#HotSpot 层源码:从字节流到 InstanceKlass)
- [VM 反向加载:解析期对 Java 层的 loadClassInternal 回调](#VM 反向加载:解析期对 Java 层的 loadClassInternal 回调)
- [链接与初始化:link_class 与 initialize_impl](#链接与初始化:link_class 与 initialize_impl)
- 关键类关系图汇总
- 常见扩展点:自定义加载器与打破双亲委派
- [常见问题 FAQ](#常见问题 FAQ)
- 参考资料与源码阅读地图
1. 全景:一次类加载的生命周期 ★★★
1.1 ASCII 总流程图
JVM 规范把类生命周期切成 加载 → 链接(验证/准备/解析)→ 初始化 → 使用 → 卸载 五步。源码视角的落点是:前三步分别由 Java 层 ClassLoader (找字节)→ HotSpot ClassFileParser (建 Klass)→ InstanceKlass (链接、执行 <clinit>)主导。

1.2 阶段速览表:每步在哪份源码里
| 阶段 | 核心动作 | 主导源码(JDK8/HotSpot8) | 产物 / 异常 |
|---|---|---|---|
| 加载·找字节 | 委派链搜索 class 字节 | jdk: java/lang/ClassLoader.java、sun/misc/Launcher.java、java/net/URLClassLoader.java |
ClassNotFoundException |
| 加载·入 VM | JNI 边界 | hotspot: prims/jvm.cpp(JVM_DefineClass*、JVM_FindClassFromClassLoader) |
--- |
| 加载·解析字节 | 魔数/版本/常量池/字段/方法 | classfile/classFileParser.cpp(ClassFileParser) |
ClassFormatError、UnsupportedClassVersionError |
| 加载·定义落位 | 字典登记 + 镜像 | classfile/systemDictionary.cpp、classfile/classLoaderData.cpp、classfile/javaClasses.cpp |
重复定义 → LinkageError |
| 链接·验证 | 字节码类型检查 | classfile/verifier.cpp(ClassVerifier) |
VerifyError |
| 链接·准备/解析 | 静态存储、父类接口递归解析 | oops/instanceKlass.cpp、oops/constantPool.cpp |
NoClassDefFoundError |
| 初始化 | 执行 <clinit> |
oops/instanceKlass.cpp(initialize_impl) |
ExceptionInInitializerError |
| 元数据存储 | Klass/方法字节码/常量池 | 元空间 Metaspace(Java 8 起取代 PermGen,见 \[JVM-内存结构-知识点]) | OutOfMemoryError: Metaspace |
关键认知 :规范层面的「加载→验证→准备→解析→初始化」是逻辑顺序 ,HotSpot 实现上大量合并与后移------父类解析在链接期才强制、绝大多数符号引用解析是惰性的(首次执行到那条字节码才解析并缓存)。所以看源码时不要把五步想成五个严格串行的时间窗口。
2. 加载入口:谁触发、从哪里进
2.1 两类入口路径
| 入口类型 | 触发者 | 代码路径 | 特征 |
|---|---|---|---|
| 应用主动加载 | 反射、Class.forName、自定义 loader 调用 |
Java loadClass/forName/defineClass → native → SystemDictionary::resolve_from_stream |
类名是"字符串参数",走完整加载流程 |
| VM 解析期加载 | 字节码执行到符号引用(new、invokevirtual、getstatic...) |
interpreter/link → SystemDictionary::resolve_* → ClassLoader::load_instance_class → 回调 Java loadClassInternal |
加载动作由 VM 发起,双亲委派仍由 Java 层执行 |
两条路径在 HotSpot 汇合于同一个函数:SystemDictionary::resolve_from_stream / resolve_instance_class_or_null(见第 4、5 节)。
2.2 触发初始化(主动使用)的 6 类场景
注意:「加载/链接」并不要求"主动使用" ,只有初始化严格限定在以下场景(《JVM 规范 SE8》5.5):
| # | 触发场景 | 举例 | 例外 |
|---|---|---|---|
| 1 | new / getstatic / putstatic / invokestatic 指令 |
new ArrayList<>()、System.out、Math.max() |
被 final static 编译期常量(进入常量池的)不触发 |
| 2 | java.lang.reflect 反射调用 |
Class.forName("X")、getMethod().invoke |
Class.forName 的 initialize=false 版不触发 |
| 3 | 初始化子类前先初始化父类 | new 子类 |
接口要求:Java 8 中接口定义了 default 方法时,实现类初始化前接口先初始化 |
| 4 | 虚拟机启动的 main 主类 | java com.example.Main |
--- |
| 5 | invokedynamic 动态调用点 | Lambda 表达式、字符串拼接、方法引用 | 解析到目标方法句柄时触发其类初始化 |
| 6 | JDK 7+ 动态语言支持的方法句柄 | MethodHandle 的 REF_invokeStatic 等 |
--- |
2.3 常见入口 API 对照表
| API | 语义 | 落点 native |
|---|---|---|
Class.forName(name) |
用调用者 的加载器加载并初始化 (默认 initialize=true) |
Class.forName0 → JVM_FindClassFromClassLoader |
ClassLoader.loadClass(name) |
只加载+链接(resolve 默认 false,不初始化) | 内部链式调用,最终 defineClass |
ClassLoader.loadClass(name, true) |
加载并立即链接 | 同上 + resolveClass0 → JVM_ResolveClass |
ClassLoader.findLoadedClass |
查字典,不加载 | JVM_FindLoadedClass → SystemDictionary::find_class |
ClassLoader.defineClass |
把已有字节数组定义成类 | JVM_DefineClass / JVM_DefineClassWithSource |
ClassLoader.getResource/AsStream |
只找资源,不加载类 | URLClassPath 逻辑 |
java
// 对照实验:三个入口的差异(初始化与否)
Class<?> a = Class.forName("com.example.Foo"); // 触发加载+链接+初始化(执行静态块)
Class<?> b = Foo.class.getClassLoader().loadClass("com.example.Foo"); // 已加载直接返回
Class<?> c = Class.forName("com.example.Foo", false, Foo.class.getClassLoader());
// 只加载,不初始化
3. Java 层源码:类加载器体系与双亲委派
3.1 JDK8 类加载器体系:sun.misc.Launcher
JDK8 应用层有三大加载器(rt.jar 里的 sun.misc.Launcher 构建),层级如下:
#mermaid-svg-q2ELvSOxKNGFkNTX{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-q2ELvSOxKNGFkNTX .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-q2ELvSOxKNGFkNTX .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-q2ELvSOxKNGFkNTX .error-icon{fill:#552222;}#mermaid-svg-q2ELvSOxKNGFkNTX .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-q2ELvSOxKNGFkNTX .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-q2ELvSOxKNGFkNTX .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-q2ELvSOxKNGFkNTX .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-q2ELvSOxKNGFkNTX .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-q2ELvSOxKNGFkNTX .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-q2ELvSOxKNGFkNTX .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-q2ELvSOxKNGFkNTX .marker{fill:#333333;stroke:#333333;}#mermaid-svg-q2ELvSOxKNGFkNTX .marker.cross{stroke:#333333;}#mermaid-svg-q2ELvSOxKNGFkNTX svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-q2ELvSOxKNGFkNTX p{margin:0;}#mermaid-svg-q2ELvSOxKNGFkNTX .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-q2ELvSOxKNGFkNTX .cluster-label text{fill:#333;}#mermaid-svg-q2ELvSOxKNGFkNTX .cluster-label span{color:#333;}#mermaid-svg-q2ELvSOxKNGFkNTX .cluster-label span p{background-color:transparent;}#mermaid-svg-q2ELvSOxKNGFkNTX .label text,#mermaid-svg-q2ELvSOxKNGFkNTX span{fill:#333;color:#333;}#mermaid-svg-q2ELvSOxKNGFkNTX .node rect,#mermaid-svg-q2ELvSOxKNGFkNTX .node circle,#mermaid-svg-q2ELvSOxKNGFkNTX .node ellipse,#mermaid-svg-q2ELvSOxKNGFkNTX .node polygon,#mermaid-svg-q2ELvSOxKNGFkNTX .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-q2ELvSOxKNGFkNTX .rough-node .label text,#mermaid-svg-q2ELvSOxKNGFkNTX .node .label text,#mermaid-svg-q2ELvSOxKNGFkNTX .image-shape .label,#mermaid-svg-q2ELvSOxKNGFkNTX .icon-shape .label{text-anchor:middle;}#mermaid-svg-q2ELvSOxKNGFkNTX .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-q2ELvSOxKNGFkNTX .rough-node .label,#mermaid-svg-q2ELvSOxKNGFkNTX .node .label,#mermaid-svg-q2ELvSOxKNGFkNTX .image-shape .label,#mermaid-svg-q2ELvSOxKNGFkNTX .icon-shape .label{text-align:center;}#mermaid-svg-q2ELvSOxKNGFkNTX .node.clickable{cursor:pointer;}#mermaid-svg-q2ELvSOxKNGFkNTX .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-q2ELvSOxKNGFkNTX .arrowheadPath{fill:#333333;}#mermaid-svg-q2ELvSOxKNGFkNTX .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-q2ELvSOxKNGFkNTX .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-q2ELvSOxKNGFkNTX .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-q2ELvSOxKNGFkNTX .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-q2ELvSOxKNGFkNTX .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-q2ELvSOxKNGFkNTX .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-q2ELvSOxKNGFkNTX .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-q2ELvSOxKNGFkNTX .cluster text{fill:#333;}#mermaid-svg-q2ELvSOxKNGFkNTX .cluster span{color:#333;}#mermaid-svg-q2ELvSOxKNGFkNTX div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-q2ELvSOxKNGFkNTX .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-q2ELvSOxKNGFkNTX rect.text{fill:none;stroke-width:0;}#mermaid-svg-q2ELvSOxKNGFkNTX .icon-shape,#mermaid-svg-q2ELvSOxKNGFkNTX .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-q2ELvSOxKNGFkNTX .icon-shape p,#mermaid-svg-q2ELvSOxKNGFkNTX .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-q2ELvSOxKNGFkNTX .icon-shape .label rect,#mermaid-svg-q2ELvSOxKNGFkNTX .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-q2ELvSOxKNGFkNTX .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-q2ELvSOxKNGFkNTX .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-q2ELvSOxKNGFkNTX :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 默认
由 Launcher 构造时
setParent0 置 null
Bootstrap ClassLoader
C/C++ 实现,无 Java 父类
加载 -Xbootclasspath:rt.jar/jce.jar 等
(JAVA_HOME/jre/lib)
ExtClassLoader(sun.misc.Launcher$ExtClassLoader)
URLClassLoader 子类,父 = Bootstrap
加载 java.ext.dirs / jre/lib/ext/*.jar
AppClassLoader(Launcher$AppClassLoader)
URLClassLoader 子类,父 = Ext
加载 java.class.path(-cp/-classpath)
自定义 ClassLoader
父默认为 AppClassLoader(系统加载器)
线程上下文类加载器 TCCL
默认 = AppClassLoader
java
// jdk/src/share/classes/sun/misc/Launcher.java(JDK8 核心片段)
public class Launcher {
private URLClassPath bootstrapClassPath; // 仅用于资源查找
private ClassLoader loader;
public Launcher() {
// 1. 先建 ExtClassLoader(父为空 = 引导类加载器)
ExtClassLoader extcl;
try {
extcl = new ExtClassLoader(ExtDirs);
} catch (IOException e) { throw new InternalError(...); }
// 2. 再建 AppClassLoader,构造时把 extcl 设为 parent
loader = AppClassLoader.getAppClassLoader(extcl);
// 3. 当前线程上下文类加载器默认 = AppClassLoader
Thread.currentThread().setContextClassLoader(loader);
}
// ...
static class ExtClassLoader extends URLClassLoader { /* 只加载 ext 目录,不重写 loadClass */ }
static class AppClassLoader extends URLClassLoader {
// 重写 loadClass:先检查包是否已在本加载器定义(sealing 校验),再 super.loadClass
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
int i = name.lastIndexOf('.');
if (i != -1) {
Package pkg = getPackage(name.substring(0, i));
if (pkg != null) { /* 校验 sealed 包不允许其他 code source 再定义 */ }
}
return super.loadClass(name, resolve);
}
// 注意:它并不重写 findClass ------ 父类 URLClassLoader.findClass 已能按 -cp 定位
}
}
关键认知 :JDK8 里
java.*核心类由 Bootstrap (VM 内 C++ 实现,class_loader句柄为 null)加载,它没有 Java 类;Ext 与 App 都是URLClassLoader的简单子类,双亲委派核心逻辑全部在祖先java.lang.ClassLoader#loadClass里,子类们几乎不重写 loadClass------这正是"委派模型可被重写打破"的前提。
3.2 ClassLoader#loadClass 源码走读(双亲委派实现处)
java
// jdk/src/share/classes/java/lang/ClassLoader.java
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) { // ① 按类名加锁,支持并行加载
Class<?> c = findLoadedClass(name); // ② 查已加载缓存(native→SystemDictionary)
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false); // ③ 父加载器先加载(委派核心!)
} else {
c = findBootstrapClassOrNull(name);// ④ 无父 → 找引导类(rt.jar)
}
} catch (ClassNotFoundException e) {
// 父抛 CNFE 表示"父链都没找到",吞掉,留给 findClass
}
if (c == null) {
long t1 = System.nanoTime();
c = findClass(name); // ⑤ 父都没找到 → 自己 findClass
// ⑥ 记录 PerfCounter:getParentDelegationTime / getFindClassTime
}
}
if (resolve) {
resolveClass(c); // ⑦ 可选:触发链接
}
return c;
}
}
// 包内私有,仅供 VM 调用(无安全检查)------见第 5 节
private Class<?> loadClassInternal(String name) throws ClassNotFoundException {
return loadClass(name);
}
逐行要点:
| 行 | 机制 | 说明 |
|---|---|---|
| ① | getClassLoadingLock(name) |
JDK7+ 的并行可加载 能力:同一加载器内不同类名用不同锁对象,避免串行;自定义加载器需 registerAsParallelCapable() 注册,否则 parallelLockMap=null 退化为 this 整锁 |
| ② | findLoadedClass |
本加载器已定义或已加载过的直接返回,保证同 (loader, name) 只加载一次 |
| ③ | parent.loadClass |
委派方向:类优先交给父加载器,父再交给祖父......直到 Bootstrap |
| ④ | findBootstrapClassOrNull |
父为 null(说明是 Bootstrap 的直接子),native 查 rt.jar |
| ⑤ | findClass |
URLClassLoader 实现:按 classpath 逐 URL 找 .class 字节(3.3 节) |
| ⑦ | resolve |
平时 loadClass(name) resolve=false:只加载不链接 (HotSpot 链接本身惰性,差异不大);forName 的 initialize 才管初始化 |
一句话概括 :双亲委派 = "先问长辈,长辈都没有才轮到自己 "。好处:同一全限定名全进程只有一份核心类(
String恒等)、核心类不受自定义加载器污染、避免重复加载。
3.3 URLClassLoader#findClass:字节从哪来
java
// jdk/src/share/classes/java/net/URLClassLoader.java
protected Class<?> findClass(final String name) throws ClassNotFoundException {
// 包名 → 路径:com.example.Foo → com/example/Foo.class
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false); // ucp = URLClassPath,按 URL 顺序找
if (res != null) {
try {
return defineClass(name, res); // 读字节 → defineClass 链
} catch (IOException e) { throw new ClassNotFoundException(name, e); }
}
return null; // → loadClass 抛 ClassNotFoundException
}
ucp是sun.misc.URLClassPath:把-cp解析成URLClassPath$JarLoader / FileLoader链表,懒打开 jar(首次真正找类时才 open),内部缓存 jar entry 索引;- Ext/App 加载器构造时把
java.ext.dirs、java.class.path转成 URL 数组喂给URLClassLoader; findClass不委派 ,只在自己管辖的 classpath 找------委派在loadClass,找路在findClass,这就是"类加载器模板方法"设计。
3.4 defineClass:跨过 JNI 边界进入 VM
java
// jdk/src/share/classes/java/lang/ClassLoader.java
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError {
protectionDomain = preDefineClass(name, protectionDomain); // ① 名称/安全域检查
// ② 经 native defineClass1 进入 HotSpot
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
private ProtectionDomain preDefineClass(String name, ProtectionDomain pd) {
if (!checkName(name)) // 名称合法性(不能含 '/')
throw new NoClassDefFoundError("IllegalName: " + name);
if ((name != null) && name.startsWith("java.")) { // ⚠️ 禁止冒充核心类
throw new SecurityException("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
if (pd == null) { /* 无 ProtectionDomain → 补默认全权限域 */ }
return pd;
}
关键认知 :
java.前缀是保护线 ------任何自定义加载器defineClass("java.lang.xxx")都抛SecurityException: Prohibited package name,核心包只许 Bootstrap 定义(引导类加载器进程内也无法"替换"java.lang.String,详见 FAQ Q5)。
4. HotSpot 层源码:从字节流到 InstanceKlass ★★★
本节目标:把 defineClass 传来的 byte[] 变成元空间里的 InstanceKlass,共三站。
4.1 第一站:jvm.cpp 的 JNI 入口
cpp
// hotspot/src/share/vm/prims/jvm.cpp
JVM_ENTRY(jclass, JVM_DefineClassWithSource(JNIEnv *env, const char *name,
jobject loader, const jbyte *buf,
jsize len, jobject pd,
const char *source))
// ① 把 Java 的 byte[] 包成内存流(不拷贝语义,指向 buf)
ClassFileStream st((u1*)buf, len, source);
Handle class_loader (THREAD, JNIHandles::resolve(loader));
Handle protection_domain (THREAD, JNIHandles::resolve(pd));
// ② 交给 SystemDictionary:这是"应用主动加载"路径的汇聚点
Klass* k = SystemDictionary::resolve_from_stream(name, class_loader,
protection_domain, &st,
THREAD);
// ③ 把 Klass 包装成 Java 层可见的 java.lang.Class(镜像)返回
return (k == NULL) ? NULL : (jclass) JNIHandles::make_local(THREAD, k->java_mirror());
JVM_END
同文件还有:
JVM_FindLoadedClass→SystemDictionary::find_class(name, loader)(只查字典不加载);JVM_FindClassFromClassLoader←Class.forName0:先resolve_or_fail加载,init=true时再InstanceKlass::initialize;JVM_ResolveClass←resolveClass(JNI 类级链接入口)。
4.2 第二站:SystemDictionary::resolve_from_stream------判重与定义
cpp
// hotspot/src/share/vm/classfile/systemDictionary.cpp(示意,保留关键步骤)
Klass* SystemDictionary::resolve_from_stream(Symbol* class_name,
Handle class_loader,
Handle protection_domain,
ClassFileStream* st,
TRAPS) {
// (1) 判重:dictionary 是 (loader, name) → Klass 的哈希表
// 同 (ClassLoaderData, 类名) 已存在 → LinkageError「duplicate class definition」
// (2) 字节流交给 ClassFileParser 解析成 InstanceKlass
instanceKlassHandle k =
ClassFileParser(st, class_name, loader_data, protection_domain,
NULL, NULL, &parsed_name, THREAD).parseClassFile(THREAD);
// (3) 登记:define_instance_class
// → dictionary()->add_klass + loader_data->add_class(klass 链)
// → jvmti ClassFileLoadHook/ClassLoad 通知、ClassLoadingService 计数
define_instance_class(k, THREAD);
return k();
}
SystemDictionary 是全局类字典 :以 <类名 Symbol, 类加载器(ClassLoaderData)> 为 key,value 为 Klass*。要点:
- 全局只有一份字典;同名类在不同加载器下是不同条目 ------所以同
.class字节能被两个加载器定义成两个互不兼容的 Class(见 8.2); - 它还持有常见系统类的入口:
SystemDictionary::String_klass()、Thread_klass()等,VM 内部找类不走字典时直接查这些快捷指针; resolve_from_stream是用户加载器的汇聚点 ;引导类加载器 (loader 为空)不走这,而走ClassLoader::load_class(见 5.2 A 分支)。
4.3 第三站:ClassFileParser::parseClassFile------按 class 文件格式逐段解析
cpp
// hotspot/src/share/vm/classfile/classFileParser.cpp
Klass* ClassFileParser::parseClassFile(Symbol* name, ClassLoaderData* loader_data,
Handle protection_domain, TRAPS) {
// ① 魔数与版本
u4 magic = cfs->get_u4_fast();
if (magic != 0xCAFEBABE) → ClassFormatError
u2 minor = cfs->get_u2_fast();
u2 major = cfs->get_u2_fast();
if (!is_supported_version(major, minor)) → UnsupportedClassVersionError
// 支持区间 [45.0, 52.0](Java 8 = 52.0)
// ② 常量池:parse_constant_pool(tag 循环读入,见下表)
// 其中 Utf8 → Symbol 进 SymbolTable;字符串字面量经 intern 进堆上 StringTable
// ③ 类级信息:access_flags / this_class / super_class / interfaces(按索引回查 CP)
// ④ 字段:parse_fields → FieldInfo(名字/描述符/ConstantValue 属性/签名)
// ⑤ 方法:parse_methods → Method*(含 Code 属性:max_stack、max_locals、
// 字节码数组、异常表、行号表)------ 方法字节码自此常驻元空间
// ⑥ 类属性:Signature / SourceFile / InnerClasses / EnclosingMethod /
// BootstrapMethods(invokedynamic 用,lambda 落点在此)
// ⑦ 组装:InstanceKlass::allocate_instance_klass + fill_instance_klass
// 产出元空间中的 InstanceKlass(字段偏移、vtable/itables 尺寸在此定稿)
}
class 文件顶层结构与常量池 tag(javap -v 能直接对出来):
| 偏移 | 内容 | 说明 |
|---|---|---|
| 0 | magic / minor / major | CA FE BA BE 00 00 00 34(52 的十六进制 = 0x34) |
| 8 | constant_pool_count + 表 | tag 见下 |
| --- | access_flags / this_class / super_class | this/super 是 CP 下标,指到 Class 条目 |
| --- | interfaces_count + 表 | 接口 CP 下标数组 |
| --- | fields_count + 表 | 字段(含 ConstantValue 属性) |
| --- | methods_count + 表 | 方法(Code 属性内嵌字节码) |
| --- | attributes_count + 表 | 类级属性 |
| tag | 常量池类型 | tag | 常量池类型 |
|---|---|---|---|
| 1 Utf8 | 所有名字/描述符的载体 | 10 Methodref | 方法符号引用 |
| 3/4 Integer/Float | 数值常量 | 11 InterfaceMethodref | 接口方法符号引用 |
| 5/6 Long/Double | 8 字节常量(占 2 槽) | 12 NameAndType | 名称+描述符对 |
| 7 Class | 类/接口符号引用 | 15 MethodHandle | 方法句柄(invokedynamic) |
| 8 String | 字符串字面量 | 16 MethodType | 方法类型 |
| 9 Fieldref | 字段符号引用 | 18 InvokeDynamic | lambda/字符串拼接引导点 |
关键认知 :
.class里的方法体引用一律是符号 (Class/Fieldref/Methodref等 tag),不指向内存地址。把符号解析成真实 Klass/Method/字段偏移的工作,就是"链接的解析"(第 6 节)------HotSpot 把它做成执行到才解析 + 缓存的惰性机制。
4.4 产物落位:InstanceKlass / ClassLoaderData / 镜像 / 数组类
加载成功后内存里有三样东西各司其职:

| 对象 | 位置 | 作用 | 关键字段/函数 |
|---|---|---|---|
InstanceKlass |
元空间 | 类的元数据本体(超级类/字段/方法/常量池/vtable) | _java_mirror、link_class()、initialize_impl() |
Class 镜像(mirror) |
堆 | Java 层看到的 Foo.class;静态 oop 字段的载体 |
Klass::java_mirror() ↔ java_lang_Class::as_Klass() |
ClassLoaderData |
元空间(结构体) | 每个类加载器一份:把 oop ClassLoader、其加载的 Klass 链、私有的元空间区块绑定 |
class_loader()、metaspace_or_null() |
SystemDictionary 条目 |
全局 | 保证 (loader,name) 唯一 | find_class / add_klass |
ArrayKlass |
元空间 | 数组类没有 class 文件,由 VM 按需合成 | SystemDictionary::resolve_array_class_or_null |
关键认知 :
Foo.class与Foo的实例是两代对象 ------镜像在堆(可被 GC 回收,普通对象指针 oop),元数据InstanceKlass在元空间(Java 8 移除 PermGen 后不被 GC 扫描,卸载粒度是整个 ClassLoaderData ,见 FAQ Q3)。实例对象头第 2 个字(klass word)指向 Klass,这也是instanceof/反射/方法分派的地基。详解见 \[JVM-内存结构-知识点]。
5. VM 反向加载:解析期对 Java 层的 loadClassInternal 回调 ★★★
5.1 触发链
上面第 4 节讲的是应用主动 把字节喂给 VM;还有一条更常见的路径是反的 :JVM 在执行字节码/链接时发现一个符号引用指向的类还没加载(例如 new 一个还没见过的类、父类/接口首次需要布局、instanceof 校验类型),此时 VM 需要"请 Java 层帮它加载":
text
解释器/链接器需要类 X(如解析 getstatic/父类)
│ SystemDictionary::resolve_instance_class_or_null(name, loader, ...)
▼
① 字典已命中? ──是──▶ 直接返回既有 Klass(校验 loader 约束)
│否
▼
② ClassLoader::load_instance_class(name, loader)
├─ loader==null(引导加载器)──▶ VM 自己读字节:load_class → ClassPathEntry 扫 bootclasspath
│ → ClassFileParser::parse_class_file(不经 Java 层)
└─ loader!=null ──▶ JavaCalls::call_virtual(loader, ClassLoader.loadClassInternal)
│
▼ (回到 Java,进入第 3 节双亲委派流程)
loadClass → findClass → defineClass → JVM_DefineClassWithSource
│
▼ (闭环)
resolve_from_stream → parse → define_instance_class
│
▼
③ 登记后返回,调用方继续(可能紧接着 link / initialize)
5.2 关键源码
cpp
// hotspot/src/share/vm/classfile/classLoader.cpp(示意)
InstanceKlass* ClassLoader::load_instance_class(Symbol* name,
Handle class_loader, TRAPS) {
// A) 引导类加载器:class_loader 为空 → VM 内部自取字节
if (class_loader.is_null()) {
return load_class(name, false, THREAD); // 逐个 ClassPathEntry 找文件/zip 流
}
// B) 用户/扩展/系统加载器:回调 Java 层 ClassLoader#loadClassInternal
JavaValue result(T_OBJECT);
Handle h_name(THREAD, JavaCalls::compute_String_from_symbol(THREAD, name));
JavaCalls::call_virtual(&result,
class_loader,
SystemDictionary::ClassLoader_klass(), // 目标类型=java.lang.ClassLoader
vmSymbols::loadClassInternal_name(), // 方法名 "loadClassInternal"
vmSymbols::string_class_signature(), // "(Ljava/lang/String;)Ljava/lang/Class;"
THREAD);
// 返回的 Class 对象 → 还原成 Klass 交还调用链
oop obj = (oop) result.get_jobject();
return (obj != NULL) ? InstanceKlass::cast(java_lang_Class::as_Klass(obj)) : NULL;
}
5.3 为什么 VM 不直接调 loadClass,而调私有的 loadClassInternal?
java
// jdk/src/share/classes/java/lang/ClassLoader.java(JDK8)
/*
* Invoked by the VM to support class loading without
* a security check.
*/
private Class<?> loadClassInternal(String name) throws ClassNotFoundException {
return loadClass(name);
}
| 原因 | 说明 |
|---|---|
| 走 Java 层委派 | 用户类加载器的自定义 findClass/字节来源只有 Java 层知道,VM 必须回调才能完成"由谁、从哪找字节"的决策 |
| 跳过安全检查 | VM 在链接路径上高频触发类加载,若每次走公开入口都做权限检查开销大且语义怪;loadClassInternal 私有、包级可控 |
| 隔离子类重写 | 若直接调受保护的 loadClass,VM 还要处理"子类 override 了 loadClass 做了别的安全决策"的语义;用 ClassLoader 基类上的私有 final 通道,行为可预期 |
关键认知 :双亲委派发生在 Java 层 ,但发起请求的往往是 C++ 层的 VM 。两条路径(应用主动 / VM 解析期)在
loadClassInternal这里交汇,之后都回到loadClass的同一份委派逻辑------这就是"委派机制与触发者无关"的源码证据。
6. 链接与初始化:link_class 与 initialize_impl ★★★
6.1 链接:InstanceKlass::link_class
loadClass(name) 返回的类"可用"前必须先链接;HotSpot 中链接入口是 InstanceKlass::link_class(TRAPS) → link_class_impl:
cpp
// hotspot/src/share/vm/oops/instanceKlass.cpp(流程示意)
bool InstanceKlass::link_class_impl(instanceKlassHandle this_k, TRAPS) {
// step 1 字节码重写 Rewriter::rewrite_instance_class
// 把字节码里大部分"常量池下标"改写为 ConstantPoolCache 下标
// (getstatic/invokestatic 等高频指令走 cpCache 直取,见 6.2)
// step 2 字节码验证:ClassVerifier 类型检查(Verifier::verify)
// -Xverify:all / remote(默认) / none 控制;失败的类标记 verify_error
// step 3 解析父类与接口:this_k->super() 为 null 且非接口 → NoClassDefFoundError
// resolve_super_or_fail:递归加载父类(又进入第 5 节路径)
// 子类字段偏移依赖父类布局 → 父类必须先在链上就位
// step 4 布局 vtable/itables:set_vtable_length、分配并填充(含 default 方法)
// step 5 完成,标记 linked;并发下用 _init_lock + linked 标志位去重
}
6.2 验证 / 准备 / 解析在 HotSpot 的实际落点
| 规范步骤 | HotSpot 实际行为 | 源码落点 |
|---|---|---|
| 验证 | 两种验证错位执行:格式验证嵌在 ClassFileParser(每段结构即验);类型验证(VerifyError 主来源)在链接 step 2,ClassVerifier 做数据流类型推导(局部变量/操作数栈的类型安全) |
classfile/verifier.cpp |
| 准备 | 静态字段存储定稿并置默认值(数值 0 / 引用 null);final static 编译期常量值已内联进常量池,不必等初始化 |
随解析完成;字段存储分两处:原生类型静态字段在 InstanceKlass 静态区内,引用型静态字段在 mirror 对象上 |
| 解析 | 绝大多数惰性 :符号引用在字节码首次执行时解析并写回 ConstantPoolCache(解释器下次直读缓存);父类/接口例外------链接 step 3 必须立即递归加载,因为字段布局、vtable 需要 |
oops/constantPool.cpp、oops/constantPoolCache.cpp、runtime/linkResolver.cpp、InterpreterRuntime::resolve_* |
cpp
// 惰性解析示意:getstatic 首次执行
// → InterpreterRuntime::resolve_get_put
// → ConstantPoolCacheEntry::resolve → LinkResolver::resolve_field
// → 找到字段偏移/所属 Klass 后写回 cpCache,下次执行不再走解析
关键认知(HotSpot 与规范的最大差异) :规范允许解析在首次使用时发生,HotSpot 把它用到了极致------一条
new/invokevirtual对应的类,可能在它即将被创建/调用的那一刻才真正被加载 。这解释了为何线上会出现"类加载日志时间点很晚"以及-XX:+TraceClassLoading输出顺序与直觉不符的现象。
6.3 初始化:initialize_impl 与 <clinit> 状态机
cpp
// hotspot/src/share/vm/oops/instanceKlass.cpp(流程示意)
void InstanceKlass::initialize_impl(instanceKlassHandle this_k, TRAPS) {
// ① 状态检查:_init_state == fully_initialized 直接返回(保证只初始化一次)
// ② 未链接先链接:link_class(初始化必须先于链接完成)
// ③ 加 _init_lock,置状态 being_initialized(记录 init_thread 防循环)
// ④ 若有父类:先初始化父类(initialize_super_interfaces:先父类再 default 方法接口)
// ⑤ 调用 <clinit>:JavaCalls::call_static(..., vmSymbols::class_initializer_name())
// <clinit> 是 javac 把静态块+静态字段赋值合并出的 ()V 方法
// ⑥ 状态置 fully_initialized,通知等待线程
// ⚠️ 失败路径:catch 后置 initialization_error 并缓存首个异常
// 此后任何主动引用都抛 ExceptionInInitializerError(getStaticField 详因)
}
初始化状态机(字段 InstanceKlass::_init_state):
| 状态 | 含义 | 关键行为 |
|---|---|---|
not_initialized |
未开始 | 初次触发进入 |
being_initialized |
正在初始化 | 持 _init_lock;同线程重入(循环初始化)直接放行返回,避免死锁 |
fully_initialized |
完成 | 后续引用直接放行 |
initialization_error |
失败 | 缓存首个异常;之后所有主动引用抛 ExceptionInInitializerError |
java
// 现象解释:静态块抛异常后
public class Boom { static { throw new RuntimeException("init fail"); } }
Boom.class.getClassLoader().loadClass("Boom"); // OK:loadClass 不初始化
new Boom(); // 第一次:ExceptionInInitializerError(cause = RuntimeException)
new Boom(); // 之后每次都抛 ExceptionInInitializerError(状态已被钉死在 error)
7. 关键类关系图汇总
7.1 Java 层类图
#mermaid-svg-sCECeivsWWozbzaB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-sCECeivsWWozbzaB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-sCECeivsWWozbzaB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-sCECeivsWWozbzaB .error-icon{fill:#552222;}#mermaid-svg-sCECeivsWWozbzaB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-sCECeivsWWozbzaB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-sCECeivsWWozbzaB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-sCECeivsWWozbzaB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-sCECeivsWWozbzaB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-sCECeivsWWozbzaB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-sCECeivsWWozbzaB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-sCECeivsWWozbzaB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-sCECeivsWWozbzaB .marker.cross{stroke:#333333;}#mermaid-svg-sCECeivsWWozbzaB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-sCECeivsWWozbzaB p{margin:0;}#mermaid-svg-sCECeivsWWozbzaB g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-sCECeivsWWozbzaB g.classGroup text .title{font-weight:bolder;}#mermaid-svg-sCECeivsWWozbzaB .cluster-label text{fill:#333;}#mermaid-svg-sCECeivsWWozbzaB .cluster-label span{color:#333;}#mermaid-svg-sCECeivsWWozbzaB .cluster-label span p{background-color:transparent;}#mermaid-svg-sCECeivsWWozbzaB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-sCECeivsWWozbzaB .cluster text{fill:#333;}#mermaid-svg-sCECeivsWWozbzaB .cluster span{color:#333;}#mermaid-svg-sCECeivsWWozbzaB .nodeLabel,#mermaid-svg-sCECeivsWWozbzaB .edgeLabel{color:#131300;}#mermaid-svg-sCECeivsWWozbzaB .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-sCECeivsWWozbzaB .label text{fill:#131300;}#mermaid-svg-sCECeivsWWozbzaB .labelBkg{background:#ECECFF;}#mermaid-svg-sCECeivsWWozbzaB .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-sCECeivsWWozbzaB .classTitle{font-weight:bolder;}#mermaid-svg-sCECeivsWWozbzaB .node rect,#mermaid-svg-sCECeivsWWozbzaB .node circle,#mermaid-svg-sCECeivsWWozbzaB .node ellipse,#mermaid-svg-sCECeivsWWozbzaB .node polygon,#mermaid-svg-sCECeivsWWozbzaB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-sCECeivsWWozbzaB .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-sCECeivsWWozbzaB g.clickable{cursor:pointer;}#mermaid-svg-sCECeivsWWozbzaB g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-sCECeivsWWozbzaB g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-sCECeivsWWozbzaB .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-sCECeivsWWozbzaB .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-sCECeivsWWozbzaB .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-sCECeivsWWozbzaB .dashed-line{stroke-dasharray:3;}#mermaid-svg-sCECeivsWWozbzaB .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-sCECeivsWWozbzaB #compositionStart,#mermaid-svg-sCECeivsWWozbzaB .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-sCECeivsWWozbzaB #compositionEnd,#mermaid-svg-sCECeivsWWozbzaB .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-sCECeivsWWozbzaB #dependencyStart,#mermaid-svg-sCECeivsWWozbzaB .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-sCECeivsWWozbzaB #dependencyStart,#mermaid-svg-sCECeivsWWozbzaB .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-sCECeivsWWozbzaB #extensionStart,#mermaid-svg-sCECeivsWWozbzaB .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-sCECeivsWWozbzaB #extensionEnd,#mermaid-svg-sCECeivsWWozbzaB .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-sCECeivsWWozbzaB #aggregationStart,#mermaid-svg-sCECeivsWWozbzaB .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-sCECeivsWWozbzaB #aggregationEnd,#mermaid-svg-sCECeivsWWozbzaB .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-sCECeivsWWozbzaB #lollipopStart,#mermaid-svg-sCECeivsWWozbzaB .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-sCECeivsWWozbzaB #lollipopEnd,#mermaid-svg-sCECeivsWWozbzaB .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-sCECeivsWWozbzaB .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-sCECeivsWWozbzaB .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-sCECeivsWWozbzaB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-sCECeivsWWozbzaB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-sCECeivsWWozbzaB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} parent(委派)
ucp
parent
ClassLoader
#parent
+loadClass(name, resolve)
+findLoadedClass(name)
+findClass(name)
+defineClass(name, bytes, off, len)
-loadClassInternal(name)
+registerAsParallelCapable()
URLClassLoader
#ucp : URLClassPath
+findClass(name)
+findResource(name)
URLClassPath
+JarLoader / FileLoader
+getResource(path, check)
Launcher_ExtClassLoader
+加载 ext 目录
Launcher_AppClassLoader
+加载 -cp
+loadClass() : 包sealing校验
7.2 VM 层类图(元数据与对象模型)
#mermaid-svg-rJEpmrMoQiX4Xm58{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rJEpmrMoQiX4Xm58 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rJEpmrMoQiX4Xm58 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rJEpmrMoQiX4Xm58 .error-icon{fill:#552222;}#mermaid-svg-rJEpmrMoQiX4Xm58 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rJEpmrMoQiX4Xm58 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rJEpmrMoQiX4Xm58 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rJEpmrMoQiX4Xm58 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rJEpmrMoQiX4Xm58 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rJEpmrMoQiX4Xm58 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rJEpmrMoQiX4Xm58 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rJEpmrMoQiX4Xm58 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rJEpmrMoQiX4Xm58 .marker.cross{stroke:#333333;}#mermaid-svg-rJEpmrMoQiX4Xm58 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rJEpmrMoQiX4Xm58 p{margin:0;}#mermaid-svg-rJEpmrMoQiX4Xm58 g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-rJEpmrMoQiX4Xm58 g.classGroup text .title{font-weight:bolder;}#mermaid-svg-rJEpmrMoQiX4Xm58 .cluster-label text{fill:#333;}#mermaid-svg-rJEpmrMoQiX4Xm58 .cluster-label span{color:#333;}#mermaid-svg-rJEpmrMoQiX4Xm58 .cluster-label span p{background-color:transparent;}#mermaid-svg-rJEpmrMoQiX4Xm58 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rJEpmrMoQiX4Xm58 .cluster text{fill:#333;}#mermaid-svg-rJEpmrMoQiX4Xm58 .cluster span{color:#333;}#mermaid-svg-rJEpmrMoQiX4Xm58 .nodeLabel,#mermaid-svg-rJEpmrMoQiX4Xm58 .edgeLabel{color:#131300;}#mermaid-svg-rJEpmrMoQiX4Xm58 .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-rJEpmrMoQiX4Xm58 .label text{fill:#131300;}#mermaid-svg-rJEpmrMoQiX4Xm58 .labelBkg{background:#ECECFF;}#mermaid-svg-rJEpmrMoQiX4Xm58 .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-rJEpmrMoQiX4Xm58 .classTitle{font-weight:bolder;}#mermaid-svg-rJEpmrMoQiX4Xm58 .node rect,#mermaid-svg-rJEpmrMoQiX4Xm58 .node circle,#mermaid-svg-rJEpmrMoQiX4Xm58 .node ellipse,#mermaid-svg-rJEpmrMoQiX4Xm58 .node polygon,#mermaid-svg-rJEpmrMoQiX4Xm58 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rJEpmrMoQiX4Xm58 .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-rJEpmrMoQiX4Xm58 g.clickable{cursor:pointer;}#mermaid-svg-rJEpmrMoQiX4Xm58 g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-rJEpmrMoQiX4Xm58 g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-rJEpmrMoQiX4Xm58 .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-rJEpmrMoQiX4Xm58 .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-rJEpmrMoQiX4Xm58 .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-rJEpmrMoQiX4Xm58 .dashed-line{stroke-dasharray:3;}#mermaid-svg-rJEpmrMoQiX4Xm58 .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-rJEpmrMoQiX4Xm58 #compositionStart,#mermaid-svg-rJEpmrMoQiX4Xm58 .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-rJEpmrMoQiX4Xm58 #compositionEnd,#mermaid-svg-rJEpmrMoQiX4Xm58 .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-rJEpmrMoQiX4Xm58 #dependencyStart,#mermaid-svg-rJEpmrMoQiX4Xm58 .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-rJEpmrMoQiX4Xm58 #dependencyStart,#mermaid-svg-rJEpmrMoQiX4Xm58 .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-rJEpmrMoQiX4Xm58 #extensionStart,#mermaid-svg-rJEpmrMoQiX4Xm58 .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-rJEpmrMoQiX4Xm58 #extensionEnd,#mermaid-svg-rJEpmrMoQiX4Xm58 .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-rJEpmrMoQiX4Xm58 #aggregationStart,#mermaid-svg-rJEpmrMoQiX4Xm58 .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-rJEpmrMoQiX4Xm58 #aggregationEnd,#mermaid-svg-rJEpmrMoQiX4Xm58 .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-rJEpmrMoQiX4Xm58 #lollipopStart,#mermaid-svg-rJEpmrMoQiX4Xm58 .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-rJEpmrMoQiX4Xm58 #lollipopEnd,#mermaid-svg-rJEpmrMoQiX4Xm58 .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-rJEpmrMoQiX4Xm58 .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-rJEpmrMoQiX4Xm58 .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-rJEpmrMoQiX4Xm58 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-rJEpmrMoQiX4Xm58 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-rJEpmrMoQiX4Xm58 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 按 loader_data 分桶
1..N
0..N
java_mirror
(name,loader)→Klass 判重
SystemDictionary
+resolve_from_stream()
+resolve_instance_class_or_null()
+define_instance_class()
+find_class()
+String_klass() / Thread_klass()
ClassLoaderData
+_class_loader : oop
+_metaspace
+_klasses : Klass 链
+add_class()
InstanceKlass
+_name/_super/_interfaces
+_methods/_fields/_constants
+_java_mirror
+_init_state
+link_class()
+initialize_impl()
ArrayKlass
+数组类(VM 合成)
MirrorClass
+java.lang.Class 实例(堆内)
+as_Klass()/java_mirror()
7.3 全链路时序:应用加载一个类的完整调用
ClassFileParser SystemDictionary HotSpot(JVM_DefineClass) URLClassLoader(findClass) 父加载器链(Bootstrap/Ext) ClassLoader(loadClass) 应用代码 ClassFileParser SystemDictionary HotSpot(JVM_DefineClass) URLClassLoader(findClass) 父加载器链(Bootstrap/Ext) ClassLoader(loadClass) 应用代码 #mermaid-svg-ydLgugaUXzchwoDj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ydLgugaUXzchwoDj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ydLgugaUXzchwoDj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ydLgugaUXzchwoDj .error-icon{fill:#552222;}#mermaid-svg-ydLgugaUXzchwoDj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ydLgugaUXzchwoDj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ydLgugaUXzchwoDj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ydLgugaUXzchwoDj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ydLgugaUXzchwoDj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ydLgugaUXzchwoDj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ydLgugaUXzchwoDj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ydLgugaUXzchwoDj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ydLgugaUXzchwoDj .marker.cross{stroke:#333333;}#mermaid-svg-ydLgugaUXzchwoDj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ydLgugaUXzchwoDj p{margin:0;}#mermaid-svg-ydLgugaUXzchwoDj .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ydLgugaUXzchwoDj text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-ydLgugaUXzchwoDj .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-ydLgugaUXzchwoDj .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-ydLgugaUXzchwoDj .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-ydLgugaUXzchwoDj .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-ydLgugaUXzchwoDj #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-ydLgugaUXzchwoDj .sequenceNumber{fill:white;}#mermaid-svg-ydLgugaUXzchwoDj #sequencenumber{fill:#333;}#mermaid-svg-ydLgugaUXzchwoDj #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-ydLgugaUXzchwoDj .messageText{fill:#333;stroke:none;}#mermaid-svg-ydLgugaUXzchwoDj .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ydLgugaUXzchwoDj .labelText,#mermaid-svg-ydLgugaUXzchwoDj .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-ydLgugaUXzchwoDj .loopText,#mermaid-svg-ydLgugaUXzchwoDj .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-ydLgugaUXzchwoDj .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-ydLgugaUXzchwoDj .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-ydLgugaUXzchwoDj .noteText,#mermaid-svg-ydLgugaUXzchwoDj .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-ydLgugaUXzchwoDj .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ydLgugaUXzchwoDj .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ydLgugaUXzchwoDj .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ydLgugaUXzchwoDj .actorPopupMenu{position:absolute;}#mermaid-svg-ydLgugaUXzchwoDj .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-ydLgugaUXzchwoDj .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ydLgugaUXzchwoDj .actor-man circle,#mermaid-svg-ydLgugaUXzchwoDj line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-ydLgugaUXzchwoDj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ① findLoadedClass → 未命中 ② 父链失败 → 委派链反向 首次主动使用才触发: link_class(验证/准备/父类递归解析) initialize_impl(<clinit>) loadClass("com.example.Foo") parent.loadClass(...) 递归 均未找到(返回null) findClass(name) ucp.getResource(读.class字节) 字节数组 defineClass → JVM_DefineClassWithSource resolve_from_stream(判重) parseClassFile(魔数/常量池/字段/方法) InstanceKlass(元空间) + 镜像 define_instance_class 登记 java.lang.Class 对象 Class 对象可用
8. 常见扩展点:自定义加载器与打破双亲委派 ★★★
8.1 可运行示例:自定义加载器骨架(模板方法的标准用法)
双亲委派的正确"自定义姿势":只重写 findClass,别动 loadClass(不破坏委派,逻辑由父类模板驱动):
java
// DemoLoader.java ------ 从任意目录加载字节,不破坏双亲委派
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class DemoLoader extends ClassLoader {
private final Path base; // 类文件根目录
public DemoLoader(Path base, ClassLoader parent) {
super(parent); // 显式指定父(默认为系统加载器)
this.base = base;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Path file = base.resolve(name.replace('.', '/').concat(".class"));
if (!Files.exists(file)) {
return null; // 找不到 → loadClass 抛 CNFE
}
try {
byte[] bytes = Files.readAllBytes(file);
return defineClass(name, bytes, 0, bytes.length); // 唯一合法"造类"入口
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
}
}
java
// Foo.java ------ 被加载的样例类(javac 产出后可复制到任意目录)
public class Foo {
static { System.out.println("Foo 静态块执行: loader=" + Foo.class.getClassLoader()); }
public String hello() { return "hello from " + getClass().getClassLoader(); }
}
bash
# 运行观察:同字节、不同加载器 → 不同 Class,互不可见
mkdir -p /tmp/demo/classes
javac -d /tmp/demo/classes Foo.java DemoLoader.java
# 方案一:系统加载器加载(在 classpath 上)
java -cp /tmp/demo/classes Foo的启动类?(换成可执行 Main 略)
# 方案二:自定义 loader 从文件系统指定目录加载,控制台会看到:
# Foo 静态块执行: loader=DemoLoader@xxxx (而非 sun.misc.Launcher$AppClassLoader)
8.2 打破双亲委派的四种经典场景
| 场景 | 打破方式 | 原因与源码事实 |
|---|---|---|
| JDBC SPI(服务提供者机制) | 线程上下文类加载器 TCCL | 核心 java.sql.DriverManager 由 Bootstrap 加载,Driver 实现(mysql-connector)在应用 classpath------委派链找得到核心类、找不到 SPI 实现,因此 DriverManager 用 TCCL(默认 App)去 ServiceLoader 找驱动 |
| Tomcat 应用隔离 | WebappClassLoader 先查 `WEB-INF/classes |
lib,再考虑父加载器(delegate=false` 默认) |
| OSGi 插件化 | 网状委派:按 Import-Package 声明的 bundle 解析类 | 模块级细粒度委派,父加载器概念被"包导出者"取代 |
| 热部署 / 热替换 | 新版本 = 新加载器实例 | 类无法"原地更新",只能由新 loader 重读新字节;旧 loader 连同旧类在无引用后随 GC 整体卸载(见 FAQ Q3) |
破坏后的代价 ⚠️:同名的类若被两个加载器各自定义,就产生两个类型 ------它们 instanceof 互相为 false、互相强转抛 ClassCastException,这是类冲突/NoSuchMethodError 排查的原点(参考 \[服务熔断降级限流-知识点] 里对 SPI 多实现加载的排查思路同理)。
8.3 自定义加载器避坑清单
| 规则 | 原因 |
|---|---|
✅ 一般只重写 findClass |
loadClass 已实现"先查缓存→委派父→兜底 findClass"模板,重写它=放弃委派与缓存 |
✅ 打破委派时只"定向"抢前缀(如 com.example.*) |
全局颠倒会使 java.*、应用公共库分裂成多份类型 |
✅ 记得 registerAsParallelCapable() |
未注册时 getClassLoadingLock 退化为 this 整锁,高并发下同 loader 串行加载 |
❌ 不要 defineClass("java.*") |
preDefineClass 硬编码拒绝,SecurityException: Prohibited package name |
❌ 不要在 findClass 里递归调 loadClass |
死循环或无限委派 |
| ❌ 不要期望"改字节后旧 Class 生效" | 类不可变;需新 loader + 新 Class 对象(JVM TI redefineClasses 仅限结构不变的字节码替换) |
9. 常见问题 FAQ
Q1:ClassNotFoundException / NoClassDefFoundError / LinkageError 怎么区分?
| 异常 | 抛出方/时机 | 典型含义 |
|---|---|---|
ClassNotFoundException |
Java 层 loadClass/findClass 主动查找失败 |
"没找着":委派链+自己的 classpath 都没有该类(类名错、jar 缺失、SPI 缺实现) |
NoClassDefFoundError |
VM 解析期需要某类而不可得 | "曾经见过/应该存在,现在不可用":编译期存在运行期缺 jar、类的静态初始化用到的类缺失、加载器链变化 |
LinkageError(含 VerifyError) |
链接期 | 类与类之间契约被破坏:重复定义、版本错位、字节码不安全 |
UnsupportedClassVersionError |
加载早期 | class 文件版本 > 运行 JDK 支持范围(52 是 Java 8,用 JDK7 跑即抛) |
Q2:线上如何快速看类到底是谁加载的、加载没加载?
bash
java -verbose:class -cp . Main # 每次类加载打一行: [Loaded com.example.Foo from file:...]
java -XX:+TraceClassLoading -cp . Main # 同上(VM 侧日志,含内部类,量大)
java -XX:+TraceClassUnloading -cp . Main # 观察卸载
# arthas: sc -d com.example.Foo / classloader / classloader -t
# jmap -clstats <pid> # 各 ClassLoaderData 统计(name/加载数/存活/字节)
Q3:类什么时候被卸载?为什么自定义加载器是热部署的关键?
- 卸载条件(周志明《深入理解 Java 虚拟机》归纳):① 该加载器加载的类无任何实例 ;② 加载器本身无引用 (不可达);③ 类的
Class镜像无反射引用;④ 类之间无相互引用。核心是②:类以 ClassLoaderData 为粒度整体回收; - HotSpot 在 GC 的类卸载阶段扫描可回收的 ClassLoaderData(
-XX:+TraceClassUnloading观察);CMS/G1 时代默认开启,但卸载发生要等到对应收集器的 GC 时机 ,所以"改了 jar 就生效"是错觉------热部署必须换新加载器实例,而非等旧类卸载; - Metaspace 满了但类又卸不掉 →
OutOfMemoryError: Metaspace,典型元凶就是"每次热部署 new 一个 loader 却不释放引用"。
Q4:Class.forName 和 classLoader.loadClass 有什么区别?
| 维度 | Class.forName(name) |
loader.loadClass(name) |
|---|---|---|
| 加载器 | 调用者的加载器 | 指定 loader |
| 初始化 | ✅ 默认触发(initialize=true) |
❌ 不触发(不执行静态块) |
| 典型用途 | 驱动注册(JDBC 老代码 Class.forName("com.mysql.jdbc.Driver")) |
运行时按名取类、框架类加载(Spring/MyBatis 全走 loadClass) |
| 找不到 | ClassNotFoundException |
ClassNotFoundException |
Q5:自定义加载器真的能加载一份"自己的 java.lang.String"吗?
不能直接 defineClass("java.lang.String", ...)------preDefineClass 抛 SecurityException: Prohibited package name。即便用某些手段绕过检查注入字节,类型系统也会被破坏(虚拟机内部大量代码假定 java.lang.* 唯一),最终得到 NoClassDefFoundError/类冲突。核心包只能由 Bootstrap 加载;java.* 下扩展包(java.lang.reflect 等)同理受保护。
Q6:同名字节被两个加载器加载,Java 里表现为?
java
// 两个 Class 对象:Foo.class != 自定义 loader 加载的 Foo
ClassLoader l = new DemoLoader(Paths.get("/tmp/demo/classes"), DemoLoader.class.getClassLoader());
Class<?> foo2 = l.loadClass("Foo"); // 自定义加载器版本
Object o = foo2.getConstructor().newInstance();
System.out.println(o instanceof Foo); // false!两个"Foo"不是同一类型
类型身份 = 全限定名 + 定义加载器;instanceof/强转/方法签名重载都以此为准。
10. 参考资料与源码阅读地图
规范与书籍
- 《Java 虚拟机规范(Java SE 8 版)》第 5 章 加载、链接与初始化:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html
- 周志明,《深入理解 Java 虚拟机:JVM 高级特性与最佳实践(第 3 版)》第 7 章 虚拟机类加载机制
- class 文件格式(第 4 章):https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html
JDK8 / HotSpot8 源码(按阅读顺序)
| 源码文件 | 重点函数 | 对应章节 |
|---|---|---|
jdk/src/share/classes/java/lang/ClassLoader.java |
loadClass / loadClassInternal / preDefineClass |
3.2、3.4、5.3 |
jdk/src/share/classes/sun/misc/Launcher.java |
构造方法、ExtClassLoader/AppClassLoader |
3.1 |
jdk/src/share/classes/java/net/URLClassLoader.java |
findClass |
3.3 |
hotspot/src/share/vm/prims/jvm.cpp |
JVM_DefineClassWithSource 等 |
4.1 |
hotspot/src/share/vm/classfile/systemDictionary.cpp |
resolve_from_stream / resolve_instance_class_or_null |
4.2、5.1 |
hotspot/src/share/vm/classfile/classLoader.cpp |
load_instance_class / load_class |
5.2 |
hotspot/src/share/vm/classfile/classFileParser.cpp |
parseClassFile |
4.3 |
hotspot/src/share/vm/classfile/classLoaderData.cpp |
ClassLoaderData 生命周期 |
4.4、FAQ Q3 |
hotspot/src/share/vm/oops/instanceKlass.cpp |
link_class_impl / initialize_impl |
6.1、6.3 |
hotspot/src/share/vm/oops/constantPool.cpp + runtime/linkResolver.cpp |
惰性解析与 cpCache | 6.2 |
源码浏览入口:https://github.com/openjdk/jdk8u (jdk/ 与 hotspot/ 两个子仓库)