背景
JDK 8 生产排查里,BTrace、jcmd 等工具都需要在 不重启业务 JVM 的前提下与之交互。底层机制是 JDK 自带的 Attach API(com.sun.tools.attach,jdk.attach 模块):一个本地进程通过 PID 连上目标 HotSpot JVM,再在这条 attach 通道上发送不同种类的请求。
Attach 不是只有 loadAgent 一种用法。jcmd 的 Thread.print、JFR.start 等走 JVM 内置 Diagnostic Command,一般 不需要 再加载外部 agent JAR;BTrace 等才典型地 loadAgent。
本文说明 Attach 通道、公开 API、与 jcmd / BTrace 的关系。概念总图见 Java 领域知识关系图。容器、新版 JDK 注意点见文末。
是什么
Attach 让已启动的 HotSpot JVM 在运行中被另一进程「挂上」:
- 连接入口:
VirtualMachine.attach(pid)(及AttachProvider发现 JVM) - 通道上可发多种 attach 操作(见下节),不限于
loadAgent - 其中一类操作会加载外部 Agent JAR,调用
agentmain(启动时-javaagent则走premain)
不是远程 RPC:必须在本机(或共享进程命名空间,见文末),且一般要求与目标 JVM 相同的有效用户/组。
版本历史
Attach API 自 Java 6(JDK 6)起提供。 com.sun.tools.attach 包(如 VirtualMachine、loadAgent)官方标注 Since 1.6,用于在目标 JVM 已运行时动态 attach 并加载 Agent。
| 版本 | 相关内容 |
|---|---|
| JDK 6 | Attach API(VirtualMachine.attach、loadAgent 等) |
| JDK 7 | jcmd 与 Diagnostic Command 框架(JEP 137),在 attach 通道上触发内置诊断命令 |
| JDK 9 | API 收入 jdk.attach 模块;模块化项目引用时常需 requires jdk.attach |
注意:动态 attach 从 JDK 6 就有;jcmd / 内置 Diagnostic Command 是 JDK 7 起叠在 attach 之上的能力,不等于 Attach API 本身到 JDK 7 才出现。JDK 8 上的 BTrace、jcmd 均建立在此基础之上。
早期实现主要面向 HotSpot;目标 JVM 若带 -XX:+DisableAttachMechanism 则无法 attach。
一条通道,两类典型用法
Attach 通道(.java_pid socket 等)
├─ 执行内置 Diagnostic Command ← jcmd:Thread.print、JFR.start、GC.heap_dump …
└─ loadAgent 外部 JAR ← BTrace、Arthas、async-profiler …
| 能力 | JVM 内置? | 典型工具 | 是否 loadAgent |
|---|---|---|---|
线程栈 Thread.print |
是 | jcmd |
否 |
堆 dump GC.heap_dump |
是 | jcmd |
否 |
JFR JFR.start 等 |
是(JDK 8 需解锁商业特性) | jcmd |
否 |
| 自定义方法探针 | 否 | BTrace | 是 |
| 交互式诊断命令 | 否 | Arthas | 是 |
| CPU/alloc 采样(async-profiler) | 否 | async-profiler | 是 |
内置诊断不是「挂了一个诊断 agent.jar」,而是 HotSpot 启动时就有 AttachListener 与 Diagnostic Command(DCmd)框架(JEP 137),jcmd attach 上去后直接触发这些命令。
公开 Attach API
应用开发者能稳定依赖的主要是 jdk.attach 包:
import com.sun.tools.attach.VirtualMachine;
VirtualMachine vm = VirtualMachine.attach(pid);
try {
vm.loadAgent("/path/to/agent.jar", "key=value");
} finally {
vm.detach();
}
| 方法 | 作用 |
|---|---|
VirtualMachine.attach(pid) |
按操作系统 PID 连接目标 JVM |
loadAgent(jar, args) |
加载外部 Agent JAR,调用 agentmain |
loadAgentLibrary / loadAgentPath |
加载本地库 Agent(JVMTI) |
detach() |
断开 attach 连接(不一定会卸载已加载的 Agent) |
AttachProvider / VirtualMachine.list() |
发现本机可 attach 的 JVM |
没有对外公开的 runDiagnostic("Thread.print") 之类 API。jcmd 发 Diagnostic Command 用的是 JDK 内部 类 sun.tools.attach.HotSpotVirtualMachine(非标准 API,第三方不宜依赖)。
jcmd 如何通过 Attach 调用 Diagnostic Command
jcmd 列进程、发 Thread.print / JFR.start 等,底层都走 attach,但 不走 loadAgent。
调用链(简化):
jcmd 客户端
│ VirtualMachine.attach(pid) ← 公开 Attach API
│ HotSpotVirtualMachine.executeJCmd("Thread.print") ← JDK 内部
│ native execute("jcmd", "Thread.print")
▼
目标 JVM:AttachListener 收到操作类型 "jcmd"
│ DCmd::parse_and_execute("Thread.print", ...) ← HotSpot 内置 DCmd 框架
▼
结果经 attach 通道返回终端
JDK 内部等价逻辑(sun.tools.attach.HotSpotVirtualMachine,仅供理解):
public InputStream executeJCmd(String command) throws IOException {
return executeCommand("jcmd", command);
}
同一 attach 通道上还有其它操作码,例如 "load"(对应 loadAgent)、"properties"(读系统属性)等;Diagnostic Command 与 loadAgent 是不同 attach 操作,不是同一 API 的两种配置。
jcmd -l 则通过 AttachProvider 扫描 attach 标记发现 JVM,同样不是 loadAgent,也与 ps 扫 /proc 不同(见 jcmd)。
premain 与 agentmain
| 方式 | 何时加载 | 入口 | 典型场景 |
|---|---|---|---|
-javaagent:agent.jar |
JVM 启动 | premain |
启动即埋点、AOP LTW |
Attach loadAgent |
JVM 已运行 | agentmain |
BTrace、Arthas、async-profiler |
Agent JAR 的 MANIFEST.MF 需包含 Agent-Class(agentmain)或 Premain-Class(premain)。
Linux 上的 attach 标记
HotSpot 在 java.io.tmpdir(Linux 上常见 /tmp)创建 attach 专用标记,常见路径为 /tmp/.java_pid<pid>。表面像文件名,实际是 UNIX domain socket,供 attach 客户端连上目标 JVM 内的 AttachListener。
该标记 专门服务于 Attach 机制(jcmd、VirtualMachine.attach、loadAgent 等),不是泛用的 Java 临时文件。
| 路径示例 | 用途 |
|---|---|
/tmp/.java_pid<pid> |
Attach API / attach 通道 |
/tmp/hsperfdata_<user>/<pid> |
性能计数器共享;jps 等常靠它列进程 |
注意:
- 目录可被
-Djava.io.tmpdir=...改变,不限于/tmp -XX:+DisableAttachMechanism时可能不创建标记,attach 与jcmd -l会失败- 容器内标记在容器自己的 tmp,宿主机
jcmd -l往往看不到(jcmd)
谁在用 Attach API
| 工具 | attach 之后做什么 |
|---|---|
| jcmd | 发内置 Diagnostic Command(线程栈、堆 dump、JFR 等) |
| BTrace | loadAgent(btrace-agent.jar),Socket 下发探针 |
| Arthas | loadAgent,提供 trace / watch 等 |
| async-profiler | loadAgent,CPU/alloc 采样 |
上层协议不同,但「连上目标 JVM」这一步共用 attach 通道。
与 BTrace 的关系(简图)
客户端进程(btrace 命令)
│ VirtualMachine.attach(pid)
│ loadAgent("btrace-agent.jar") ← attach 操作 "load"
▼
业务 JVM(agentmain → Instrumentation → ASM 织入)
│ Socket:脚本/输出(BTrace 特有)
▼
客户端终端
BTrace 脚本编译、Socket、探针织入在 Agent 之上,见 BTrace Attach 原理。
限制与常见错误
- 同一用户:否则
Unable to attach to target VM - 本机 attach:不能跨机器 attach(远程需 SSH 再执行工具)
-XX:+DisableAttachMechanism:目标 JVM 禁用 attach 时失败- 非 HotSpot / 非 Java 进程:attach 无效
- loadAgent 之后:Agent 是否可完全卸载取决于实现;生产上常按「可能需重启业务」处理
参考
其它 JDK 版本
JDK 21 及以后:动态 Agent 加载
运行时 loadAgent 可能要求目标 JVM 开启:
-XX:+EnableDynamicAgentLoading
主要影响 BTrace、Arthas、async-profiler 等 loadAgent 路径;jcmd 内置 Diagnostic Command 也依赖 attach 通道,若完全禁用 attach 同样不可用。
容器与 Kubernetes
- 推荐:
docker exec进入跑 Java 的业务容器,在同一命名空间内jcmd -l/btrace <PID> ... - 多阶段镜像:从
btrace/btrace等镜像 COPY 工具到业务 JDK 镜像,见 BTrace 安装 - Sidecar:同 Pod 需
shareProcessNamespace: true,并常需SYS_PTRACE
Attach 不提供跨容器网络 attach;可见性由进程/PID 命名空间决定。
工具选型
升级 JDK 后,列进程仍多用 jcmd -l;内置诊断与 loadAgent 系工具的分工见 生产环境诊断工具选型。