本条基于 Java 17(Minecraft 1.20.1 所用版本)。
Instrumentation(java.lang.instrument.Instrumentation)是 JavaAgent 修改类的标准接口,底层由 JVMTI 实现,JDK 中的具体实现类是 InstrumentationImpl。
获取
- 启动时:
-javaagent:xxx.jar,jar 清单声明Premain-Class,JVM 调用premain(String, Instrumentation)。 - 运行时附加:通过 Attach API(
VirtualMachine.attach(pid).loadAgent(jar)),清单声明Agent-Class,JVM 调用agentmain。Java 9 起默认不允许附加到自身进程(需-Djdk.attach.allowAttachSelf=true),常用的绕过方式见下文“运行时外部进程附加”。 - 重转换 / 重定义还需清单声明
Can-Retransform-Classes/Can-Redefine-Classes。
运行时外部进程附加
目前最常用的做法。Java 9 起 JVM 默认不允许附加到自身进程,所以由模组另起一个 Java 子进程,让子进程附加到游戏进程并加载 agent。整个过程不需要任何启动参数(既不用 -javaagent,也不用 -Djdk.attach.allowAttachSelf=true)。
附加时机
可以在 Forge提供的Services 加载、Mixin(如 mixin 配置插件加载)或 ModMain(模组主类构造)期间发起附加。附加得越早,越有机会在目标类加载前注册好转换器。
实现思路
- 取得游戏进程 PID:
ProcessHandle.current().pid()。 - 准备 agent jar:通常就是模组自身的 jar,清单中写
Agent-Class和Can-Retransform-Classes: true。 - 用当前 JVM 的 java 可执行文件(
java.home下的bin/java)启动子进程运行下面的辅助类,参数为 PID 和 agent jar 路径;父进程等待子进程结束。 - 子进程通过反射依次调用
VirtualMachine.attach→loadAgent→detach。用反射是为了编译时不依赖jdk.attach模块。 - 游戏进程里,JVM 调用 agent 的
agentmain(String, Instrumentation),agent 由此拿到 Instrumentation 实例。
辅助类示例:import java.lang.reflect.Method;public final class ExternalAttachHelper { private ExternalAttachHelper() { } public static void main(String[] args) throws Exception { if (args.length < 2) { throw new IllegalArgumentException("Usage: ExternalAttachHelper <pid> <agentJar>"); } String pid = args[0]; String agentJar = args[1]; Class<?> vmClass = Class.forName("com.sun.tools.attach.VirtualMachine"); Method attach = vmClass.getMethod("attach", String.class); Object vm = attach.invoke(null, pid); try { Method loadAgent = vmClass.getMethod("loadAgent", String.class, String.class); loadAgent.invoke(vm, agentJar, "external-attach"); } finally { Method detach = vmClass.getMethod("detach"); detach.invoke(vm); } }}
注意事项
- 运行子进程的 Java 需要包含
jdk.attach模块(完整 JDK 有,部分精简 JRE 没有)。 agentmain所在类由系统类加载器加载,和模组类不在同一个类加载器里,通常要借助反射或系统类加载器可见的类,把 Instrumentation 交回模组。- 目标 JVM 带
-XX:+DisableAttachMechanism时无法附加。 - Java 21 起动态加载 agent 会打印警告(JEP 451),将来可能需要
-XX:+EnableDynamicAgentLoading才能使用;1.20.1 所用的 Java 17 不受影响。
常用方法
addTransformer(ClassFileTransformer, boolean):注册转换器,类加载或重转换时收到字节码- retransformClasses / redefineClasses:修改已加载的类,两者的区别(基线)见各自条目;隐藏类默认不可修改
getAllLoadedClasses、getObjectSizeappendToBootstrapClassLoaderSearch、redefineModule(可给模块加 opens)
特点与限制
- 能修改已加载的类和
java.*等 JDK 类,弥补了 Mixin 只能处理未加载的非 JDK 类的不足。 - 重转换 / 重定义只能改方法体,不能增删字段、方法,也不能改继承关系。
- 字节码一般配合 objectweb asm 或 javassist 修改。