背景
本文记录的是 JDK 8 生产环境下的实践:线上出现问题时,在不改代码、不重启的前提下,用 BTrace attach 到运行中的 JVM,按条件过滤慢方法并持续输出。
概念与相关文章的关系见 Java 领域知识关系图。
何时需要 BTrace(生产动机)
| 动机 | 说明 |
|---|---|
| 不能停机 | 滚动重启成本高或 SLA 不允许 |
| 保留故障现场 | 重启后偶现问题、特定请求参数、堆内对象态、当时流量下的慢调用往往 难以复现;需要在 故障仍在发生时 对当前 JVM 取样 |
| 观测而非热修 | BTrace 用于 慢调用过滤、参数、耗时、调用路径 等可观测数据,不是用 agent 临时改业务逻辑修 Bug(机制上 agent 能改字节码,但 BTrace 产品侧用脚本校验限制为安全探针) |
车联网协议解析 G4 因写 MySQL 偏慢导致 OOM、后迁移 InfluxDB 的完整案例见 物联网平台协议解析服务生产环境 OOM:MySQL 迁移 InfluxDB(职业叙事;本文偏工具用法)。
临时排查可配合 生产环境诊断工具选型(Arthas、JFR 等);BTrace 适合 脚本化、持续输出 的采集。
安装
BTrace 不是单个原生二进制,而是 Java 工具发行包(压缩包 + shell 脚本 + JAR),运行时依赖本机 JDK。
从 GitHub Releases 下载 btrace-bin.tar.gz 或 .zip,解压后大致结构:
btrace/
├── bin/
│ ├── btrace # 客户端:attach 并下发脚本
│ ├── btracec # 编译 BTrace 脚本
│ └── btracer # 启动时带 agent 运行应用
└── libs/ # client、agent、boot 等 JAR
JDK 8 时代常见做法:
tar -xzf btrace-2.x.tar.gz
export BTRACE_HOME=/path/to/btrace
export PATH=$BTRACE_HOME/bin:$PATH
btrace -h
文档中的 /bin/btrace <PID> ... 即上述 bin/btrace 脚本(内部用 java 启动 client JAR)。
较新安装方式(JBang、SDKMan、Docker 等)见文末「其它 JDK 版本」。
运行依赖与架构
BTrace 全程依赖 Java / JVM,且涉及 两个进程、两个 JVM:
| 角色 | 是什么 | 依赖 |
|---|---|---|
| 客户端 | 执行 btrace 命令的 Java 进程 |
本机 JDK,跑 client JAR |
| 业务 JVM | 正在运行的应用 | 被 attach 的 HotSpot JVM |
| Agent | 加载进业务 JVM 的 btrace-agent.jar |
在业务进程内执行织入与探针 |
不是像 tcpdump 那样不依赖 Java 的原生工具。客户端与目标 JVM 通常在同一台机器、可用同一套 JDK 安装;版本不必完全一致,但需在目标 JDK 上实测 Agent 兼容性。
整体链路:
客户端 JVM(btrace 命令)
│ ① Attach API:loadAgent(btrace-agent.jar) ──► 业务 JVM(Agent 启动)
│ ② Socket:下发脚本/探针定义,回收 println 输出
▼
业务 JVM 内:Agent 用 ASM + Instrumentation 织入业务类;探针在业务线程里执行
注意:
- 通过 Attach 进入业务 JVM 的是 Agent JAR,不是把脚本编译成 jar 再 attach 进去。
- Agent 是加载进业务 JVM 的一整套代码(含 Socket 服务等,可能起监听线程),不等于业务 JVM 里的「一个线程」;探针跑在业务方法调用栈上(如
method0返回前)。 - Socket 是 两个进程之间 的通信,不是两个 JVM 内部随便传数据。
概念厘清
本节汇总 attach、Agent、ClassLoader、线程与开发热替换等易混点(与 java-asm、Attach API 一致)。
loadAgent 之后:不是 main,是 agentmain
| 普通 JAR | Agent JAR(btrace-agent.jar) |
|---|---|
manifest Main-Class → public static void main |
manifest Agent-Class → public static void agentmain(String, Instrumentation) |
java -jar app.jar |
Attach API loadAgent 或启动时 -javaagent(premain) |
loadAgent 由 业务 JVM 进程内 的 HotSpot 完成加载并调用 agentmain,不会 执行 agent 的 main,也 不会 在此时加载你的 BTrace 脚本。
两阶段:先基础设施,后脚本
阶段 1 loadAgent(btrace-agent.jar)
→ agentmain:Instrumentation、Socket 服务等就绪
阶段 2 客户端编译 script.java(在客户端 JVM)
→ Socket 下发探针定义
→ Agent:ASM + retransform,织入业务类
attach 的是 agent JAR;脚本 不以 jar 形式 attach,而是织进目标类字节码。
同一 OS 进程,不同 ClassLoader
| 维度 | 说明 |
|---|---|
| 进程 | 只有一个业务 JVM;btrace 命令是 另一个客户端进程 |
| Agent 类 | 多由 JVM agent 加载路径 加载,一般不当作业务 AppClassLoader 的普通 classpath |
| 业务类 | 仍由 AppClassLoader(等)定义;retransform 改的是这些类的方法 字节码 |
| 探针执行 | 在 业务线程 调用栈上(如 method0 返回前),不是「每个方法一个 agent 线程」 |
命名空间与「子可见父、兄弟互不可见」见 java classloader §不同 ClassLoader。
Agent 能力 vs BTrace 限制
| 层面 | 能力 |
|---|---|
JVM + Instrumentation + ASM |
理论上可大幅改写已加载类(含改返回值等) |
| BTrace 产品 | 脚本编译/校验:禁止 new、循环、抛异常等;API 限于 BTraceUtils → 以观测为主 |
| 恶意 agent | 同进程、同用户权限下风险大;生产需管控 attach 与 agent 来源 |
Hook 点除方法入口/返回外,还有 CALL、LINE、异常相关等(见下文 @Location);机制上 可在这些位置插字节码,BTrace 则限制探针体为安全子集。
与开发期热替换的区别
| BTrace attach | IDE HotSwap / DCEVM | |
|---|---|---|
| 目的 | 观测 已运行进程 | 修改 自己的业务代码并立刻跑 |
| 路径 | Attach + retransform 探针 |
JPDA → JVMTI / 增强 redefine |
| 与 Spring DevTools | 无关 | DevTools 是上下文 Restart,不是 JVM HotSwap |
打印慢调用
编写 BTrace 脚本
BTrace 脚本常用 .java 扩展名,语法大体是 Java 的子集,但不是标准 Java 源文件,而是带 BTrace 注解的探针脚本:
- 必须有
@BTrace,探针方法用@OnMethod、@Location等注解声明 hook 点 - 常用
import static com.sun.btrace.BTraceUtils.*中的println、timestamp等 API - 由 BTrace 编译器在 客户端 JVM 中编译(
btrace <PID> script.java或事先btracec),不能用javac当普通业务类编译运行 - 不进业务应用的 classpath;探针逻辑由 Agent 织入目标 JVM 的类中,而不是在脚本的
main里执行
| 方面 | 标准 Java | BTrace 脚本 |
|---|---|---|
| 编译 | javac 生成可独立运行的 class |
BTrace 编译器 + 校验 |
| 运行 | main、实例方法、可 new 对象 |
无独立 main;探针注入目标方法 |
| 限制 | 无特殊限制 | 不能 new、不能循环、不能抛异常、须 static 等 |
| 注解 | JDK / 第三方注解 | com.sun.btrace.annotations.* |
示例脚本:
// MethodDuration_redis.java
import com.sun.btrace.annotations.*;
import static com.sun.btrace.BTraceUtils.*;
@BTrace
public class MethodDuration_redis {
private static int i = 0;
@OnMethod(clazz = "com.wiloon.package0.Class0", method = "method0",
location = @Location(Kind.RETURN))
public static void printMethodRunTime(@ProbeClassName String probeClassName,
@Duration long duration) {
long d = duration / 1000000;
if (d > 9) { // 大于 9 毫秒的调用
i++;
println("index: " + i + ", timestamp:" + timestamp("HH:mm:ss")
+ ", " + probeClassName + ", duration: " + d + " ms");
}
}
}
找到 Java 进程并执行 BTrace
# 打印 Java 进程
jcmd -l
# 执行 btrace,Ctrl-C 退出
/bin/btrace <PID> MethodDuration_redis.java
Attach 原理
BTrace 的 attach 走 JDK 官方的动态 attach + Java Agent + Instrumentation 字节码改写,不是重启进程。可概括为:
Client(Attach API + 脚本编译)+ Agent([ASM](./asm.md) + Instrumentation)+ Socket
执行流程(Attach 模式)
-
启动客户端 JVM
执行
btrace <PID> script.java会拉起独立的 Java 进程(客户端),用本机 JDK 运行 BTrace client。 -
Attach API 加载 Agent
客户端使用 Attach API(
com.sun.tools.attach.VirtualMachine):VirtualMachine vm = VirtualMachine.attach(pid); vm.loadAgent("/path/to/btrace-agent.jar", "port=2020,..."); vm.detach();attach(pid)通过操作系统本地机制连接目标 JVM(Linux 上常见为/tmp/.java_pid<pid>一类 socket)。通常要求同一台机器、同一用户。loadAgent把 btrace-agent.jar 载入业务 JVM,调用agentmain(非main),获得Instrumentation。不是 载入脚本 jar。Agent 框架见 java-asm §Agent 框架、ClassFileTransformer。 -
Agent 在业务 JVM 内就绪
Agent 在业务进程内启动(可能包含监听 Socket 的线程),等待客户端连接。
-
客户端编译并下发脚本
- 在 客户端 JVM 读取
script.java,经 BTrace 编译器编译、校验 - 经 Socket 把探针定义发给业务 JVM 内的 Agent(不是 attach 脚本 jar)
- 也可用
btracec预编译后再 attach
- 在 客户端 JVM 读取
-
Agent 在业务 JVM 内织入探针
- 解析 hook 点(
@OnMethod、@Location等) - 用 ASM 生成探针字节码,做 verification
- 注册
ClassFileTransformer,对目标类retransformClasses,在方法入口/返回前插入探针 - 探针在 业务线程 中执行;
println等经 Socket 回到客户端终端
这与 IDE 的 HotSwap(JPDA / 有限 redefine)不同:是 Instrumentation 的 retransform,由 ASM 在已加载类上插探针字节码。
- 解析 hook 点(
-
结束
Ctrl-C 退出客户端;探针是否完全移除取决于 detach 行为,生产上仍按「可能需重启业务 JVM」对待。
常见误解
| 误解 | 实际情况 |
|---|---|
| attach 的是脚本编译出的 jar | attach 的是 btrace-agent.jar;脚本以探针形式 织进业务类 |
| Agent 是业务 JVM 里的一个线程 | Agent 是整份 agent 代码;探针跑在业务方法调用栈上 |
| 两个 JVM 长期对等通信 | Attach 一次性加载 Agent;之后主要通过 Socket 传脚本与输出 |
| BTrace 是单个原生二进制 | 是 脚本 + JAR,客户端与目标都依赖 JVM |
loadAgent 会执行 agent 的 main |
执行 agentmain,manifest 为 Agent-Class |
| loadAgent 时脚本已进业务 JVM | 否;脚本在 agent 就绪后经 Socket 下发再织入 |
| Agent 与业务是不同 JVM 进程 | 同一业务进程;客户端 btrace 是另一进程 |
| JVM 禁止 agent 改业务类 | 不禁止;BTrace 校验脚本 限制为观测型探针 |
| 开发 HotSwap 与 BTrace 同一路 | 否;见 §概念厘清 |
两种启动方式
| 方式 | 命令 | Agent 何时加载 |
|---|---|---|
| Attach 模式 | btrace <PID> script.java |
进程已运行,动态 loadAgent |
| 启动时 Agent | btracer ... 或 -javaagent:btrace.jar |
JVM 启动时 premain |
JDK 8 生产排查常用 Attach 模式。
脚本限制的原因
探针字节码会进入业务方法的热路径。若允许在探针里创建对象、抛异常、加锁或写循环,可能改变 GC/锁行为,甚至导致 JVM 校验失败。因此 BTrace 强制脚本几乎全是 static、无堆分配、无异常的子集。
BTrace 简介
BTrace 是 Sun 推出的 Java 动态、安全追踪工具:不重启即可监控运行情况,获取方法参数、返回值、堆栈等,侵入性较低。
由于脚本逻辑会织入运行中的代码,使用上有较多限制:
- 不能创建对象
- 不能使用数组
- 不能抛出或捕获异常
- 不能使用循环
- 不能使用
synchronized - 属性和方法必须使用
static修饰
不当使用可能导致 JVM 崩溃,上生产前应在本地充分验证脚本。移除探针后,通常需重启才能完全恢复被改写的类(取决于 detach 行为,生产上仍按「可能需重启」对待)。
典型用途
- 接口变慢,分析方法耗时
- Map 大量插入时观察扩容
- 分析谁调用了
System.gc()及调用栈 - 方法抛异常时查看运行时参数
第一个例子
被跟踪的 Java 类:
package com.metty.rpc.common;
import java.util.Random;
public class BtraceCase {
public static Random random = new Random();
public int size;
public static void main(String[] args) throws Exception {
new BtraceCase().run();
}
public void run() throws Exception {
while (true) {
add(random.nextInt(10), random.nextInt(10));
}
}
public int add(int a, int b) throws Exception {
Thread.sleep(random.nextInt(10) * 100);
return a + b;
}
}
通过 jps 获取 PID,执行 btrace <pid> Debug.java 即可在每次 add 调用时看到参数、返回值与耗时。
注解说明
@OnMethod
定义要分析的方法入口。class、method 指定方式示例:
- 全限定名:
clazz="com.metty.rpc.common.BtraceCase",method="add" - 正则:
clazz="/javax\.swing\..*/",method="/.*/" - 接口:
clazz="+com.ctrip.demo.Filter",method="doFilter" - 注解:
clazz="@javax.jws.WebService",method="@javax.jws.WebMethod" - 构造方法:
method="<init>"
@Location
定义拦截位置,默认为 Kind.ENTRY:
Kind.ENTRY:进入方法时执行脚本Kind.RETURN:方法返回前执行;只有此时能拿到@Return和@DurationKind.CALL:分析方法内对其它方法的调用;细粒度耗时常配合Where.AFTERKind.LINE:是否执行到指定行号Kind.ERROR/Kind.THROW/Kind.CATCH:异常相关跟踪
使用 BTrace 定位问题
- 找出耗时超过阈值的 Filter:
@Duration为纳秒,需除以1_000_000得到毫秒;可再用@Location(Kind.CALL)细查 - 分析谁调用了
System.gc():在调用栈中定位类与方法 - 统计方法调用次数:
@OnTimer可定时执行脚本中的方法 - 查看实例属性:通过 BTrace 提供的反射能力读取字段
相关博客(本专题)
| 主题 | 文章 |
|---|---|
| 知识关系总图 | Java 领域知识关系图 |
Attach、loadAgent、jcmd |
Java Attach API |
| ASM、Instrumentation、retransform | Java ASM 与运行时字节码织入 |
| ClassLoader、Agent 与业务可见性 | java classloader |
| HotSpot、JVMTI、JPDA | HotSpot 简介、JVMTI、JAVA 调试与 JPDA |
| 开发热替换(对比用) | 开发期热替换、Spring Boot DevTools |
| jcmd、工具选型 | jcmd、生产环境诊断工具选型 |
参考
- BTrace 项目
- 生产环境诊断工具选型(其它 JDK 版本下的工具对照)
- rowkey: btrace 介绍
- calvin1978: btrace
- json-liu btrace gitbook
- greys-anatomy(Arthas 前身之一)
其它 JDK 版本
以下内容超出 JDK 8 当时的生产环境,供升级 JDK 后参考。
安装方式补充
除解压发行包外,还可:
# SDKMan
sdk install btrace
# JBang(从 Maven 拉 client/agent,无需手动下 tar.gz)
curl -Ls https://sh.jbang.dev | bash -s - app setup
jbang catalog add --name btraceio https://raw.githubusercontent.com/btraceio/jbang-catalog/main/jbang-catalog.json
jbang btrace@btraceio <PID> script.java
JDK 11 及以后
-
JFR 自 JDK 11 起免费,许多场景可用
jcmd <pid> JFR.start做 GC、锁、分配等分析,见 Java Flight Recorder。 -
BTrace 2.x 仍维护,支持 oneliner,例如:
btrace -n 'com.myapp.*::* @return if duration>100ms { print method, duration }' <PID>
JDK 21 及以后
运行时动态加载 agent 可能需目标 JVM 增加:
-XX:+EnableDynamicAgentLoading
若存在 -XX:+DisableAttachMechanism 则需移除。BTrace、Arthas、async-profiler 的 attach 模式均受影响。
更高版本 JDK 与工具选型
JDK 已升级到 Java 26 时,BTrace 仍可用于「慢调用过滤 + 持续采集」等脚本化场景,但通常不再是默认首选:临时排查多用 Arthas(trace / watch),CPU/GC 多用 JFR 与 async-profiler。
详见 Java 生产环境诊断工具选型。