2026-10-31·by Sijie Wang

Class Loading

类加载:按需加载、双亲委派、打破委派做热更新

动机。 Java 运行时按需加载类:编译出的 .class 字节码,用到哪个类才变成内存里的类。好处是动态性(插件、热更新、SPI)加懒加载(启动时不用全加载)。

过程三步。 加载:读 .class 字节流,在方法区建成数据结构,同时在堆里建一个 Class 对象。链接:验证(字节码合不合法)、准备(静态字段赋默认值)、解析(符号引用换成直接引用,可以推迟)。初始化:执行 ,静态块加静态赋值按顺序来,父类先于子类。触发时机只有主动引用:new、访问静态字段和方法、反射、子类初始化、main 类。

加载器层次加双亲委派。 自顶向下:启动类加载器(Bootstrap,C++ 实现,加载核心类)、平台类加载器、应用类加载器(classpath),下面是自定义。双亲委派的意思是:先问父加载器,父找不到才自己找。两个好处:核心类永远不会被自定义的同名类替换(安全),同一个类不会被重复加载。

打破委派就是热更新的原理。 类的身份是“全限定名加加载器”,同一个类名、不同加载器就是不同的类。所以热更新的做法是:每个版本用一个新加载器加载,旧加载器一丢,旧类满足三个条件就可卸载:这个类的实例全 GC 了、Class 对象不可达、加载器本身不可达。自研 classloader 热更新那段,底子就是这个。

两个容易跟类加载搞混的邻居。

Graal:两个身份,两种“热”。做 JIT 编译器时,Graal 通过 JVMCI(JVM Compiler Interface,JVM 编译器接口)插进 HotSpot 替代 C2,它的“热”是运行时代码替换:先 profiling,再推测性优化(假设某个类型、某条分支),假设破了就去优化,退回解释执行或者重编译。换的是编译产物,不是类。做 Native Image 时,Graal 是反方向:提前编译、编译时封闭世界,运行时动态加载类受限,反射和资源都要清单。所以“Graal 可以热加载”如果指 Native Image,是反的;如果指 Graal JIT 的去优化热替换,是对的,但那不是类加载。

Lombok:编译期 trick,不是运行期。@Getter、@Setter、@Data、@Builder、@Slf4j 这些注解省样板代码。标准注解处理(JSR 269)只能生成新文件,不能改已有文件,Lombok 走得更远:它钻进 javac 的内部 API,在解析出 AST(抽象语法树)之后、生成字节码之前,直接在内存里改 AST,把方法、字段注进去。所以源码里看不见这些方法,编译出来的 class 里有。这也是它脆弱的地方:它依赖 JDK 内部 API,JDK 16 之后模块强封装(JEP 396/403),每次大版本都得适配,经常要加 --add-opens 才能跑。现在还没到不用的地步(@Builder、@Slf4j 没有 JDK 等价物),但简单的数据载体确实一部分被 record(JDK 16+)抢了。

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →

Class Loading — Sijie Wang