主题
Attach 机制与运行时注入
一句话:Attach 让外部进程通过本地 socket/管道连上目标 JVM,加载一个 agent,拿到
Instrumentation后重定义类;它把「重启才能改」变成「运行时能改」,代价是权限、稳定性和可审计性都要额外设计。
适用 JDK 17 / JDK 21。Attach 是本地机制,默认不跨主机;远程诊断通常走 JDWP 或跳板机。
为什么需要
JVM 启动后,类已经加载、JIT 已经编译,很多诊断只能在运行时做:线上方法耗时突然变高、某个类被谁加载、是否需要临时加一段监控。重启会丢现场,也会中断业务。
Attach 提供了这个入口:jcmd、jstack、jmap 这些工具都通过它连接目标 JVM;Java agent 也通过它把 Instrumentation 注入进去,实现字节码增强、类重定义和运行时诊断。
核心机制
两个 JVM 怎么连上
Attach 的链路可以分成四步:
- 外部进程调用
VirtualMachine.attach(pid)。 - 操作系统层建立一个本地连接。Linux 上通常是临时目录下的 Unix domain socket(如
/tmp/.java_pid<pid>),Windows 上是命名管道;具体路径和实现随平台/JDK 版本变化。 - 目标 JVM 的 Attach Listener 线程接收请求,解析协议命令。
- 外部进程发送
load命令,目标 JVM 加载 agent jar,调用agentmain。
Attach Listener 不是启动时就常驻的线程。首次 Attach 时,目标 JVM 通过信号/启动机制创建它,之后复用。这个设计避免了所有 JVM 都常驻一个监听线程,但也意味着第一次 Attach 有一笔启动成本。
premain 与 agentmain
| 入口 | 触发方式 | 时机 |
|---|---|---|
premain(String, Instrumentation) | -javaagent:agent.jar | main 之前 |
agentmain(String, Instrumentation) | VirtualMachine.loadAgent(jar, args) | JVM 运行中 |
agent jar 的 MANIFEST.MF 要声明入口和能力:
text
Manifest-Version: 1.0
Agent-Class: com.example.Agent
Can-Redefine-Classes: true
Can-Retransform-Classes: trueagentmain 拿到 Instrumentation 后,可以 addTransformer 注册转换器、retransformClasses 重新转换已加载的类、getAllLoadedClasses 枚举类。转换器在类加载或重转换时修改字节码。
Instrumentation 能做什么、不能做什么
能做:
- 修改方法体,插入耗时统计、日志、异常捕获。
- 重定义/重转换已加载的类(受 JVM 限制)。
- 查询对象大小、已加载类、类加载器。
- 把 jar 追加到 Bootstrap 搜索路径(受模块和权限约束)。
不能做:
- 增加或删除字段、方法,修改方法签名。
- 改变类的继承关系、实现接口。
- 绕过模块封装访问任意 JDK 内部类。
- 替换已经加载的 native 方法实现(只能加前缀,且限制很多)。
所以「热更新一个方法」可以,「热加一个字段」通常不行;后者需要换类加载器或重启。
超时与失败
Attach API 没有在所有平台统一的固定超时。attach(pid)、loadAgent 和 agent 初始化都可能因为目标进程繁忙、权限不足、socket 不可达而阻塞;具体行为以 JDK 官方文档和平台实现为准。
工程上不要把「10 秒/30 秒」当成规范值。需要上限时,在调用方用 timeout 10s jcmd ... 或自己的 Future 超时包一层,并记录失败原因。目标 JVM 正在长时间 GC 或安全点阻塞时,Attach 也可能迟迟不返回。
最小上手
先确认目标进程和基本连通性:
bash
jcmd -l
jcmd <pid> VM.version
jcmd <pid> GC.heap_info
jcmd <pid> Thread.print -l用 Attach API 加载一个最小 agent:
java
import com.sun.tools.attach.VirtualMachine;
public class AttachDemo {
public static void main(String[] args) throws Exception {
String pid = args[0];
VirtualMachine vm = VirtualMachine.attach(pid);
try {
vm.loadAgent(args[1], "from-attach"); // agent.jar 的绝对路径
} finally {
vm.detach();
}
}
}bash
javac --add-modules jdk.attach AttachDemo.java
java --add-modules jdk.attach AttachDemo <pid> /opt/agent/agent.jaragent 侧的入口只需一个方法:
java
import java.lang.instrument.Instrumentation;
public class Agent {
public static void agentmain(String args, Instrumentation inst) {
System.out.println("attached: " + args);
// inst.addTransformer(...); inst.retransformClasses(...);
}
}打包时把 Agent-Class: Agent 写进 manifest,并确认 agent 依赖在目标 JVM 的类路径上可见。
生产动作
- 先只读后写入:排障先用
jcmd的只读命令(VM.version、Thread.print、GC.heap_info),确认是目标进程再加载 agent。 - 权限最小化:Attach 要求外部进程与目标 JVM 有相同的 OS 用户权限;容器内要能看到目标 PID,跨容器通常需要进入同一 PID/挂载命名空间。
- JDK 21 的动态 agent 警告:JEP 451 对运行时动态加载 agent 发出警告,未来版本可能默认禁止;需要时显式加
-XX:+EnableDynamicAgentLoading,并把它当成受控变更,不要静默打开。 - 控制影响面:
retransformClasses会触发安全点,类越多、字节码越复杂,停顿越明显;在低峰执行,限制单次转换的类数量。 - 可回滚:agent jar 带版本号,转换器记录目标类和变更内容;出问题先移除 transformer,再评估是否需要再次 retransform 恢复原字节码。
- 审计:记录谁在什么时间 attach 了哪个 PID、加载了哪个 jar、传了什么参数;这比事后查日志可靠。
边界
- Attach 不是远程调用:默认只支持本机,跨主机要借助 SSH、跳板机或容器命名空间;不要把它暴露成网络服务。
- 不是永久 AOP:agent 注入适合诊断和临时增强,不适合承载核心业务逻辑;逻辑应该回到代码和发布流程。
- 类重定义有限制:不能加字段/方法、不能改继承;需要结构性变更就重启或换类加载器。
- 可能拖垮目标:字节码转换异常、agent 初始化死锁、类加载器泄漏都会影响业务;agent 代码要按「随时可能失败」设计。
- Native Image 不支持:GraalVM native image 没有传统 JVM 的 Attach/Instrumentation 机制,诊断方式不同。
讲清楚
Attach 是外部进程连上目标 JVM 的本地通道,
jcmd和 Java agent 都走它。连接建立后,目标 JVM 启动 Attach Listener,外部进程发送load命令,agent 的agentmain拿到Instrumentation,就能注册转换器、重转换已加载的类。它能改方法体,不能加字段/方法或改继承;重转换会触发安全点,所以要在低峰、限量执行。Attach 没有统一的固定超时,所谓「10 秒/30 秒」不是规范,工程上用外层超时控制。生产上先只读、再最小权限、全程审计,并且把 agent 当成可失败的临时手段,而不是核心链路。