Module / ModuleLayer / Instrumentation 模块与插桩源码
概述
java.lang.Module 与 java.lang.module 是 JDK 9 引入的模块系统(Project Jigsaw):把程序拆分为模块,显式声明依赖与导出,由 Configuration(解析后的依赖图)和 ModuleLayer(运行时的模块实例层)承载。java.lang.instrument 则是字节码插桩体系:通过 agent 在类加载/重定义时注入转换器,是 APM、热修复、Mock 框架的基础。
两条主线都作用于"类是如何被定义和加载"这一底层机制:模块系统决定"能否访问",插桩决定"加载成什么样"。
本文基于 OpenJDK 21 源码,先拆解 Module / Configuration / ModuleLayer 的模块解析与层定义,再深入 Instrumentation 的 agent 加载、类转换与重定义实现。
核心源码解析
① Module 的模块描述
public final class Module implements AnnotatedElement {
private final ModuleDescriptor descriptor; // 模块描述:名称、requires、exports、opens、uses、provides
private final ModuleLayer layer; // 所属模块层
private final ClassLoader loader; // 定义类加载器
private final ModuleReference reference; // 模块引用(定位模块内容,系统模块有)
private volatile Names exports; // 运行时导出状态(可被 addExports 修改)
private volatile Names opens; // 运行时打开状态
private volatile boolean isSystem;
}descriptor:静态声明,来自module-info.class。含模块名、requires(依赖)、exports/opens(导出/开放包)、uses/provides(服务)、contains(包含的包集合)。layer:该模块所属的ModuleLayer,决定它在解析后的模块图中的位置。loader:定义该模块类的ClassLoader(系统模块为null,应用模块为ClassLoader实例)。- 与
descriptor的静态声明不同,exports/opens是运行时可变状态(volatile),配合addExports实现动态开放。
② Module.isExported(String pn) 的导出检查
public boolean isExported(String pn) {
return isExported(pn, null); // 对"所有模块"是否导出
}
public boolean isExported(String pn, Module other) {
// ① 运行时状态优先:addExports 动态添加的导出立即生效
if (exports.contains(pn)) return true;
if (other != null && isNamed() && other.isNamed())
return descriptor.isExportedOrOpen(pn, other); // ② 描述符静态声明检查
return false;
}ModuleDescriptor.isExportedOrOpen(pn, other) 的检查逻辑:
- 模块对某个包
exports分三种粒度:exports p(对所有模块)、exports p to A, B(限定导出给指定模块)、opens(开放反射访问)。 - 校验顺序:先看是否有全量导出 → 再看限定导出列表是否包含
other模块名(限定导出需要解析模块名与other的对应关系,涉及模块层查询)。 - 运行时优先:
addExports修改的exports状态在判断时先于descriptor静态声明,保证反射开放等动态场景立即生效。
③ Module.addExports(String pn, Module other) 的运行时导出
public void addExports(String pn, Module other) {
if (isNamed() && other.isNamed()) {
// 校验当前模块确实包含该包
if (!descriptor.contains(pn))
throw new IllegalArgumentException(pn + " not a package");
if (descriptor.isExportedOrOpen(pn, other))
return; // 静态声明已覆盖 → 无需动态开放
}
implAddExports(pn, other);
}
private void implAddExports(String pn, Module other) {
exports.add(pn); // 更新运行时导出集合
if (other == null) {
// 导出给所有模块:native 更新模块系统反射白名单
implAddExports0(pn, null);
} else {
implAddExports0(pn, other);
}
}implAddExports0是native方法,直接修改 HotSpot 内部每个包级别的导出标记(ModuleEntry::set_exported与PackageEntry的exported位)。- 用途:JDK 9+ 的反射开放白名单——
Field.setAccessible(true)跨模块访问时,Reflection.ensureMemberAccess会触发对目标模块执行addExports/addOpens,使原本非导出的包对反射调用方临时开放。 addOpens是addExports的加强版:不仅可访问,还开放反射(opens语义),反射调用前若模块只exports而未opens,JVM 会提示使用--add-opens或addOpensAPI。
④ ModuleLayer.boot() 的引导层
public static ModuleLayer boot() {
return bootLayer;
}引导层在 JVM 启动早期初始化(System.initPhase2 中调用 ModuleBootstrap.boot()),核心步骤:
ModuleFinder bootFinder = ModuleFinder.ofSystem(); // ① 扫描 jrt:/ 文件系统的全部系统模块
if (appFinder != null)
bootFinder = ModuleFinder.compose(bootFinder, appFinder); // ② 叠加应用模块路径
Configuration cf = Configuration.resolve(bootFinder,
List.of(Configuration.empty()), // 父配置:空配置
rootModules); // ③ 根模块集
bootLayer = ModuleLayer.boot().defineModules(cf, ModuleLoader::bootLoader);ModuleFinder.ofSystem():以jrt文件系统为源,枚举 JDK 自带的所有系统模块(java.base、java.sql、jdk.jfr等),生成ModuleReference集合。- 根模块:启动时由
--module/--add-modules决定,默认包含java.se(聚合模块)。 - 引导层是所有其他模块层的父层,
bootLayer单例在 JVM 生命周期内固定。
⑤ Configuration.resolveRequires(...) 的模块图解析
public static Configuration resolveRequires(ModuleFinder before,
List<Configuration> parents,
Collection<String> roots) {
Resolver resolver = new Resolver(before, parents);
return resolver.resolveRequires(roots);
}Resolver(java.lang.module.Resolver)解析过程:
- 收集根模块:对每个根名称查找
ModuleReference(先在beforefinder,再在父配置中找已解析模块)。 - 依赖闭包:对每个模块递归处理
requires(含requires transitive的传递依赖),用Deque广度遍历,把依赖到的模块加入待解析集合。 - 拓扑排序:构造依赖图后进行拓扑排序,保证"被依赖的模块先于依赖方"出现——
Configuration内部的ResolvedModule顺序即为此序,类加载时被依赖模块可先被定义。 - 绑定服务:处理
uses/provides,把服务提供者模块也纳入解析(bind()阶段)。
// Resolver 核心:按依赖顺序产出 ResolvedModule 图
private void buildGraph(...) {
// depth-first traversal:依赖 → 递归解析 → 拓扑序入队
}最终 Configuration 是不可变对象:graph(ResolvedModule → 其依赖集合)、nameToModule、parents 在构造后不再变化,所有解析结果固化。
⑥ ModuleLayer.defineModules(Configuration cf, Function<String, ClassLoader> clf) 的层定义
public ModuleLayer defineModules(Configuration cf, Function<String, ClassLoader> clf) {
...
Map<String, Module> nameToModule = new HashMap<>();
for (ResolvedModule resolvedModule : cf.modules()) { // 按拓扑序遍历
ModuleReference mref = resolvedModule.reference();
String name = mref.descriptor().name();
ClassLoader cl = clf.apply(name); // 用户回调决定类加载器
Module module = new Module(this, cl, mref); // 创建 Module 实例
nameToModule.put(name, module);
}
// 建立模块间的依赖绑定:把 requires 关系映射为对具体 Module 对象的引用
...
return new ModuleLayer(cf, nameToModule, parents);
}cf.modules()返回已按拓扑序排序的ResolvedModule列表,确保依赖模块先实例化。clf函数:调用方(如LayerInstantiationException场景、容器框架)通过该函数为每个模块名选择ClassLoader——这使一个模块层可以横跨多个类加载器,是"模块化应用服务器"的基础能力。- 创建后的
ModuleLayer通过findModule(String)按名称查模块、configuration()取回配置,供反射/资源访问使用。
⑦ Instrumentation 的 agent 机制
java.lang.instrument 提供两种 agent 入口(由 JAR 的 MANIFEST.MF 声明):
| 清单属性 | 触发时机 | 入口方法 |
|---|---|---|
Premain-Class | JVM 启动 -javaagent:agent.jar | public static void premain(String args, Instrumentation inst) |
Agent-Class | 运行时 attach 动态加载 | public static void agentmain(String args, Instrumentation inst) |
Launcher-Agent-Class | 仅启动器加载(不经过 agent 链路) | 直接由 launcher 加载 |
-javaagent 的处理链路:
java -javaagent:agent.jar Main
→ JVM 初始化早期调用 AgentInitializer
→ InstrumentationImpl 创建(同时创建 NativeMethodPrefix 等内部组件)
→ 反射调用 agent.jar 中 Premain-Class.premain(args, instrumentation)
→ premain 中 addTransformer(...) 注册类转换器
→ 后续类加载时触发转换InstrumentationImpl 是核心实现:持有 TransformerManager(按 canRetransform 分两组管理 ClassFileTransformer),retransformClasses / redefineClasses 的 native 调用直接对接 HotSpot 的类重定义机制。
⑧ ClassFileTransformer.transform() 的字节码转换
public interface ClassFileTransformer {
default byte[] transform(Module module,
ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException {
return null; // 返回 null 表示不修改
}
}Instrumentation.addTransformer(transformer, canRetransform) 注册后,每次类加载(或重定义)都会调用所有已注册的 transform:
- 参数语义:
loader为该类的定义类加载器;className为 JVM 内部格式的类名(如java/lang/String);classBeingRedefined仅在重定义/重转换时非空;classfileBuffer是当前字节码(加载时为原始字节,重定义时为 JVM 持有的字节码)。 - 返回值约定:返回
null表示不改变类;返回新的byte[]则替换类字节码。抛出IllegalClassFormatException可中止转换(但加载仍继续,使用原始字节码)。 - 多个 transformer 链式执行:前一个的输出作为后一个的
classfileBuffer输入,因此顺序敏感。 - 转换只发生在类首次定义阶段(对已加载类需用
retransformClasses强制重新转换)。
⑨ Instrumentation.retransformClasses(Class<?>... classes) 的类重转换
// InstrumentationImpl
public void retransformClasses(Class<?>... classes) {
retransformClasses0(new NativeClassPrefix(this), classes); // native
}
private native void retransformClasses0(ClassFileTransformer[] transformers, Class<?>[] classes);retransformClasses0在 HotSpot 内部对目标类执行重定义(ClassFileParser重新解析 +VM_RedefineClasses操作),但不会执行初始化器,且类的方法体被替换后原方法仍保留(old)版本用于栈上未返回调用。- 触发后,JVM 依次调用所有注册的、
canRetransform = true的ClassFileTransformer,把每个 transformer 的返回字节码作为下一阶段输入。 - 与
redefineClasses(ClassDefinition[])的区别:redefineClasses直接提供新字节码,retransformClasses则基于现有字节码重新跑转换链(无 transformer 修改时等价于用原字节码重新定义)。 - 限制:不能增删字段/方法/修饰符(仅允许方法体级别的修改),否则抛
UnsupportedOperationException/ClassFormatError——这由 HotSpot 的类重定义校验保证。
⑩ Instrumentation.getObjectSize(Object object) 的对象大小估算
// InstrumentationImpl
public long getObjectSize(Object objectToSize) {
return getObjectSize0(objectToSize);
}
private native long getObjectSize0(Object objectToSize);- native 实现查询 HotSpot 的
ObjectSizeCalculator式估计:对象头(mark word + klass 指针,含压缩指针影响)+ 所有实例字段大小(对齐到 8 字节)。 - 结果是近似值:不含可达对象(引用的对象本身不计入),不计堆外/JIT 侧数据,也不考虑某些 JVM 优化(如字段重排、逃逸分析导致的对象消除)。
- 主要用途:衡量单个对象的内存占用(对比不同数据结构),配合
jol(Java Object Layout)可得到更精确的布局视图。
⑪ java.lang.instrument 的 java agent 热插拔
// com.sun.tools.attach.VirtualMachine
VirtualMachine vm = VirtualMachine.attach(pid); // ① 附加到目标 JVM
try {
vm.loadAgent("/path/to/agent.jar", args); // ② 加载 agent
} finally {
vm.detach(); // ③ 分离
}动态 attach 全链路:
VirtualMachine.attach(pid):通过AttachListener与目标 JVM 建立 IPC——Linux 用UNIX domain socket(/tmp/.java_pid<pid>),Windows 用命名管道(\\.\pipe\javatool<pid>)。目标 JVM 必须开启 attach(默认允许,-XX:+DisableAttachMechanism会关闭)。loadAgent(jar):attach 请求经AttachListener线程分发,目标 JVM 解析 agent JAR 的MANIFEST.MF,找到Agent-Class。agentmain(String, Instrumentation)回调:InstrumentationImpl.loadAgent反射调用agentmain,把Instrumentation实例传入——agent 由此addTransformer、retransformClasses或redefineClasses修改已加载的类,实现不重启 JVM 的热插拔。
这也正是各种热部署工具(Arthas、Btrace、SkyWalking 探针)的统一工作方式:attach → loadAgent → premain/agentmain 拿到 Instrumentation → 注册 transformer / 重定义类。
总结
模块系统与插桩体系分别回答了"能访问什么"与"加载成什么":
| 关注点 | 模块系统(java.lang.module) | 插桩(java.lang.instrument) |
|---|---|---|
| 核心对象 | Module / Configuration / ModuleLayer | Instrumentation / ClassFileTransformer |
| 构建阶段 | resolveRequires 解析依赖图 + defineModules 创建模块 | addTransformer 注册转换器 |
| 运行时变更 | addExports / addOpens native 修改导出位 | retransformClasses / redefineClasses 重定义类 |
| 底层机制 | HotSpot ModuleEntry / PackageEntry | HotSpot VM_RedefineClasses |
两条链路都依赖 native 桥接进 JVM 内部:模块的导出检查走 ModuleEntry 位标记,agent 的类重转换走 VM_RedefineClasses 与字节码校验。理解 Configuration 的拓扑序与 Instrumentation 的 transformer 链,就能掌握模块化容器(自定义 ModuleLayer + 类加载器)与字节码增强工具(agent + retransform)的实现原理。