BTrace 简要介绍

BTrace是Java的安全可靠的动态跟踪工具。 他的工作原理是通过 instrument + asm 来对正在运行的java程序中的class类进行动态增强。

说他是安全可靠的,是因为它对正在运行的程序是只读的。也就是说,他可以插入跟踪语句来检测和分析运行中的程序,不允许对其进行修改。因此他存在一些限制:

  • 不能创建对象
  • 不能创建数组
  • 不能抛出和捕获异常
  • 不能调用任何对象方法和静态方法
  • 不能给目标程序中的类静态属性和对象的属性进行赋值
  • 不能有外部、内部和嵌套类
  • 不能有同步块和同步方法
  • 不能有循环(for, while, do..while)
  • 不能继承任何的类
  • 不能实现接口
  • 不能包含assert断言语句

这些限制其实是可以使用unsafe模式绕过。通过声明 *@BTrace(unsafe = true) annotation 并且以unsafe 模式-u运行btrace

实际使用非安全模式跟踪时,发现一个问题,一个进程如果被安全模式btrace探测过一次, 后面再使用非安全模式进行探测时非安全模式不生效。

安装并使用

  • 下载Btrace并解压
  • 定义环境变量BTRACE_HOME为解压目录
  • bin目录加入到path

运行BTrace

btrace <pid> <btrace-script>
  • pid 为java进程号,可以使用jps来查询
  • btrace-script 为 btrace脚本

也可以先使用btracec对btrace脚本继续预先编译再进行执行。

visualvm-plugin

btrace还提供了一个vaisualvm上的一个插件,可以执行btrace脚本。尝试了下,可以attach到本机的jvm进程上,但是远程主机的JVM进程不行。晚上有的说通过端口转发绑定的方式可以,但是还是没有试出来。

安装

运行jvisualvm.exe, 选择工具->插件->可用插件 选择 BTrace Workbench进行在线安装。

jvisualvm_btrace.jpg

在线不能安装的,也可以通过手动安装。先从java visualvm 插件中心找到相应版本的插件更新文件地址进行下载,从更新文件中获取所需要的插件包的nbm下载地址进行下载。最后在 插件->已下载tab页->添加插件

执行btrace脚本

选择需要监控的进程,右击 trace application

jvisualvm_btrace2.png

在btrace的工作台中直接编写脚本并执行

jvisualvm_btrace3.png

BTrace 脚本

先来看简单的BTrace脚本的使用,用于跟踪方式执行时间

@BTrace
public class TimeLogger {

  @TLS private static long startTime = 0;

  @OnMethod(clazz="org.springframework.data.redis.core.DefaultValueOperations", method="get")
  public static void startExecute(){
    startTime = timeNanos();
  }

  @OnMethod(clazz="org.springframework.data.redis.core.DefaultValueOperations", method="get",
                                            location=@Location(Kind.RETURN)
  )
  public static void endExecute(@Duration long duration){
    long time = timeNanos() - startTime;
    println(strcat("execute time(nanos): ", str(time)));
  }

}

  • @BTrace 声明了这个类是BTrace脚本
  • @OnMethod 声明了关注点,必须声明在公有的静态方法上public static void
  • 静态方法为 在关注点上执行的跟踪动作

跟踪方式执行时间程序的逻辑大致是: 在org.springframework.data.redis.core.DefaultValueOperations 对象执行get方法时,先记录一下当前时间,在get方法return时获取当前时间,从而获得方法执行所消耗的时间。值得注意的是,@TLS声明的变量是 ThreadLocal的, 每个线程都会有一份这个自己的startTime 变量。

执行以上的TimeLogger来看一下:

$ btrace 13778 TimeLogger.java 
execute time(nanos): 214692000
execute time(nanos): 1215000
execute time(nanos): 3210000

执行后,当被监控的程序运行了这些检查点的方法时,btrace会在控制台对执行时间进行输出。通过重定向符>也可以将输出重定向到文件.

API

@BTrace

声明这个类是个BTrace脚本.unsafe参数表示是否不安全的模式执行.

方法annotation

  • @OnMethod: 声明 探查点(probe point)
  • clazz: 全路径类名,支持正则表达式,格式为/正则表达式/
  • +类名 匹配子类
  • @前缀 匹配anotation声明的类
  • method: 方法名,支持正则表达式,格式为/正则表达式/, anotation使用@
  • location: 用@Location来表明在什么时候时候去执行脚本
  • Kind.ENTRY 进入方法时
  • Kind.RETURN 方法返回时
  • Kind.THROW 抛出异常时
  • Kind.ARRAY_SET 设置数组元素时
  • Kind.ARRAY_GET 获取数组元素时
  • Kind.NEWARRAY 创建新数组时
  • Kind.NEW 创建新对象时
  • Kind.CALL 调用方法时
  • Kind.CATCH 捕获异常时
  • Kind.FIELD_SET 获取对象属性时
  • Kind.FIELD_SET 设置对象属性时
  • Kind.ERROR 方法由于发生未被捕获的异常结束时
  • Kind.SYNC_ENTRY 进入同步块时
  • Kind.SYNC_EXIT 离开同步块时
  • type 方法类型, 不含方法名、参数民、异常声明。
  • @OnTimer 定时器,间隔出发动作。
  • 参数: 间隔时间,单位毫秒
  • @OnError BTrace代码发生异常时回调
  • @OnExit BTrace代码调用exit来结束探测时回调该注释的方法
  • @OnEvent 接受客户端事件时会回调。目前,当客户端命令上执行Ctrl-C (SIGINT)时会发送一个时间到服务器端,从而触发@OnEvent注释的方法。
  • @OnLowMemory 内存低于设置的阈值时回调方法
  • pool 内存池名称
  • threshold 阈值大小
  • @OnProbe 支持使用xml格式来声明探测点点和探测动作。

未声明注解的方法参数

未声明注解的方法参数的映射,根据探测点类型locaiton的不同而不同:

  • Kind.ENTRY 方法参数
  • Kind.RETURN 方法返回值
  • Kind.THROW 被抛出的异常
  • Kind.ARRAY_SET 数值下标
  • Kind.ARRAY_GET 数组下标
  • Kind.CATCH 被捕获的异常
  • Kind.FIELD_SET 被设置属性的值
  • Kind.NEW 创建的对象的类型
  • Kind.ERROR 被抛出的异常

字段注解

  • Export 将字段保罗给jstat访问
  • Property 将字段暴露注册为MBean 属性,可以通过JMX进行查看
  • TLS 将字段声明为TheadLocal字段,每个线程拥有自己独立的字段
参数annotation介绍
  • @Self 声明探测的当前对象this
  • @Return 方法返回对象
  • @ProbeClassName 当前探测点所在的类名
  • @ProbeMethodName 当前探测点所在的方法名
  • fqn 是否获取全路径方法名称fully qualified name (FQN)
  • @Duration 执行时间,单位纳秒,一般同 Kind.RETURN 和 Kind.ERROR 配合使用
  • @TargetInstance 配合 Kind.CALL使用,声明了被调用方法所在的对象
  • @TargetMethodOrField Kind.CALL使用,声明了被调用方法所在的对象的方法

BTrace原理分析

BTrace 主要使用了 Instrumentation + ASM技术来实现对正在运行进程的探测。

基本实现逻辑

虚拟机其实提供了一个hook,那就是Instrumentation,可以将独立于应用程序的代理程序agent程序随着着应用程序一起启动或者attach挂载到正在运行中的应用程序。而在代理程序中可以对class进行修改或者重新定义。

btrace的agent程序在btrace-agent.jar,通过挂在vm = VirtualMachine.attach(pid); vm.loadAgent 进行挂载到正在运行中的java进程。 源码如下:

btrace-client.jar/com.sun.btrace.client.Main

    /**
     * Attach the BTrace client to the given Java process.
     * Loads BTrace agent on the target process if not loaded
     * already.
     */
    public void attach(String pid, String sysCp, String bootCp) throws IOException {

            String agentPath = "/btrace-agent.jar";
            String tmp = Client.class.getClassLoader().getResource("com/sun/btrace").toString();
            tmp = tmp.substring(0, tmp.indexOf("!"));
            tmp = tmp.substring("jar:".length(), tmp.lastIndexOf("/"));
            agentPath = tmp + agentPath;
            agentPath = new File(new URI(agentPath)).getAbsolutePath();
            attach(pid, agentPath, sysCp, bootCp);

    }


    /**
     * Attach the BTrace client to the given Java process.
     * Loads BTrace agent on the target process if not loaded
     * already. Accepts the full path of the btrace agent jar.
     * Also, accepts system classpath and boot classpath optionally.
     */
    public void attach(String pid, String agentPath, String sysCp, String bootCp) throws IOException {
        try {
            VirtualMachine vm = null;
             byte[] code = client.compile(fileName, classPath, includePath);   // 编译btrace脚本
            vm = VirtualMachine.attach(pid);

           // 此处 省略了诺干代码..
            vm.loadAgent(agentPath, agentArgs); // 挂在 agent 包后,会运行 com.sun.btrace.agent.Main.main 开启server socket后台线程,监听客户端的连接
            client.submit(fileName, code, btraceArgs, createCommandListener(client)); // 将btrace脚本编译后的字节码提交给 正在运行的agent的程序
           
    }

btrace-agent.jar/com.sun.btrace.agent.Main


Thread agentThread = new Thread(new Runnable() {
            @Override
            public void run() {
                BTraceRuntime.enter();
                try {
                    startServer();
                } finally {
                    BTraceRuntime.leave();
                }
            }
        });
        BTraceRuntime.initUnsafe();
        BTraceRuntime.enter();
        try {
            agentThread.setDaemon(true);
            if (isDebug()) debugPrint("starting agent thread");
            agentThread.start();
        } finally {
            BTraceRuntime.leave();
        }

        public static final int BTRACE_DEFAULT_PORT = 2020;

    //-- Internals only below this point
    private static void startServer() {
        int port = BTRACE_DEFAULT_PORT;
        String p = argMap.get("port");
        if (p != null) {
            try {
                port = Integer.parseInt(p);
            } catch (NumberFormatException exp) {
                error("invalid port assuming default..");
            }
        }
        ServerSocket ss;
        try {
            if (isDebug()) debugPrint("starting server at " + port);
            System.setProperty("btrace.port", String.valueOf(port));
            if (scriptOutputFile != null && scriptOutputFile.length() > 0) {
                System.setProperty("btrace.output", scriptOutputFile);
            }
            ss = new ServerSocket(port);
        } catch (IOException ioexp) {
            ioexp.printStackTrace();
            return;
        }

        while (true) {
            try {
                if (isDebug()) debugPrint("waiting for clients");
                Socket sock = ss.accept();
                if (isDebug()) debugPrint("client accepted " + sock);
                Client client = new RemoteClient(inst, sock);
                handleNewClient(client).get();
            } catch (RuntimeException | IOException | ExecutionException re) {
                if (isDebug()) debugPrint(re);
            } catch (InterruptedException e) {
                return;
            }
        }
    }

Instrumentation.ClassFileTransformer:定义了类加载前的预处理类,可以在这个类中对要加载的类的字节码做一些处理
Instrumentation.retransformClasses 对于已经加载的类触发重新加载类定义, 进行ClassFileTransformer转换处理

com.sun.btrace.agent.Main 在会启动一个socket后台线程与btrace client 进行交互. 客户端会将BTRACE 程序的提交到agent去,再通过字节码操作对class进行操作。

在被探测点上增加探测点动作是通过 在Instrumentation代理程序中通过修改字节码来谈价探测点动作。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,295评论 18 399
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • 原文地址:BTrace用户手册<译> BTrace(https://btrace.dev.java.net/) 是...
    好好学习天天引体向上阅读 1,120评论 0 51
  • 又做梦了 醉得一塌糊涂 不知廉耻的在街边小解 不解的望着路人的嘲笑 一台洒水车又有什么错 我狠命的踢着人行道边的树...
    冷冬年阅读 211评论 0 3
  • 【原创诗歌】 梦一样的年华 哪怕是成长也如火如荼 的热烈 岁月透明的手指穿过 青春的海洋 燃烧成漫天的红霞 季风被...
    淡淡青莲阅读 378评论 19 23