跳到正文

Attach 机制与运行时注入 ​

一句话:Attach 让外部进程通过本地 socket/管道连上目标 JVM,加载一个 agent,拿到 Instrumentation 后重定义类;它把「重启才能改」变成「运行时能改」,代价是权限、稳定性和可审计性都要额外设计。

适用 JDK 17 / JDK 21。Attach 是本地机制,默认不跨主机;远程诊断通常走 JDWP 或跳板机。

为什么需要 ​

JVM 启动后,类已经加载、JIT 已经编译,很多诊断只能在运行时做:线上方法耗时突然变高、某个类被谁加载、是否需要临时加一段监控。重启会丢现场,也会中断业务。

Attach 提供了这个入口:jcmd、jstack、jmap 这些工具都通过它连接目标 JVM;Java agent 也通过它把 Instrumentation 注入进去,实现字节码增强、类重定义和运行时诊断。

核心机制 ​

两个 JVM 怎么连上 ​

Attach 的链路可以分成四步:

  1. 外部进程调用 VirtualMachine.attach(pid)。
  2. 操作系统层建立一个本地连接。Linux 上通常是临时目录下的 Unix domain socket(如 /tmp/.java_pid<pid>),Windows 上是命名管道;具体路径和实现随平台/JDK 版本变化。
  3. 目标 JVM 的 Attach Listener 线程接收请求,解析协议命令。
  4. 外部进程发送 load 命令,目标 JVM 加载 agent jar,调用 agentmain。

Attach Listener 不是启动时就常驻的线程。首次 Attach 时,目标 JVM 通过信号/启动机制创建它,之后复用。这个设计避免了所有 JVM 都常驻一个监听线程,但也意味着第一次 Attach 有一笔启动成本。

premain 与 agentmain ​

入口触发方式时机
premain(String, Instrumentation)-javaagent:agent.jarmain 之前
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: true

agentmain 拿到 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.jar

agent 侧的入口只需一个方法:

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 当成可失败的临时手段,而不是核心链路。

把一门技术讲透到既懂也会用