性能工具之Java分析工具BTrace入门

简介: 【5月更文挑战第25天】性能工具之Java分析工具BTrace入门

一、引言

在我们对Java应用做性能分析的时候,往往采用log进行问题定位和分析,但是如果我们的log缺乏相关的信息呢?远程调试会影响应用的正常工作,修改代码重新部署应用,实时性和灵活性难以保证,有没有不影响正常应用运行,又灵活并无侵入性的方法呢?

答案是有,它就是Java中的神器-BTrace

二、BTrace是什么?

BTrace使用Java的Attach技术,可以让我们无缝的将我们BTrace脚本挂到JVM上,通过脚本你可以获取到任何你想拿到的数据,在侵入性和安全性都非常可靠,特别是定位线上问题的神器。

三、BTrace原理

BTrace是基于动态字节码修改技术(Hotswap)向目标程序的字节码注入追踪代码。

四、安装配置

关于BTrace的安装配置使用,此处就不再重复造轮子,网上有太多的教程。

官网地址:https://github.com/btraceio/btrace

五、注意事项

生产环境可以使用,但修改的字节码不会被还原,使用Btrace时,需要确保追踪的动作是只读的(即:追踪行为不能修改目标程序的状态)和有限的行为(即:追踪行为需要在有限的时间内终止),一个追踪行为需要满足以下的限制:

  • 不能创建新的对象 不能创建新的数组
  • 不能抛出异常
  • 不能捕获异常
  • 不能对实例或静态方法调用-只有从BTraceUtils中的public static方法中或在当前脚本中声明的方法,可以被BTrace调用
  • 不能有外部,内部,嵌套或本地类
  • 不能有同步块或同步方法
  • 不能有循环(for,while,do..while)
  • 不能继承抽象类(父类必须是java.lang.Object)
  • 不能实现接口
  • 不能有断言语句
  • 不能有class保留字

以上的限制可以通过通过unsafe模式绕过。追踪脚本和引擎都必须设置为unsafe模式。脚本需要使用注解为@BTrace(unsafe = true),需要修改BTrace安装目录下bin中btrace脚本将-Dcom.sun.btrace.unsafe=false改为-Dcom.sun.btrace.unsafe=true

注:关于unsafe的使用,如果你的程序一旦被btrace追踪过,那么unsafe的设置会一直伴随该进程的整个生命周期。如果你修改了unsafe的设置,只有通过重启目标进程,才能获得想要的结果。所以该用法不是很好使用,如果你的应用不能随便重启,那么你在第一次使用btrace最终目标进程之前,先想好到底使用那种模式来启动引擎。

六、使用示例

1、拦截一个普通方法

control方法

@GetMapping(value = "/arg1")
    public String arg1(@RequestParam("name") String name) throws InterruptedException {
        Thread.sleep(2000);
        return "7DGroup," + name;
    }

BTrace脚本

/**
 * 拦截示例
 */
@BTrace
public class PrintArgSimple {
   

    @OnMethod(
            //类名
            clazz = "com.techstar.monitordemo.controller.UserController",
            //方法名
            method = "arg1",
            //拦截时刻:入口
            location = @Location(Kind.ENTRY))

    /**
     * 拦截类名和方法名
     */ public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, AnyType[] args) {
   
        BTraceUtils.printArray(args);
        BTraceUtils.println(pcn + "," + pmn);
        BTraceUtils.println();
    }
}

拦截结果:

192:Btrace apple$ jps -l
369 
5889 /Users/apple/Downloads/performance/apache-jmeter-4.0/bin/ApacheJMeter.jar
25922 sun.tools.jps.Jps
23011 org.jetbrains.idea.maven.server.RemoteMavenServer
25914 org.jetbrains.jps.cmdline.Launcher
25915 com.techstar.monitordemo.MonitordemoApplication
192:Btrace apple$ btrace 25915 PrintArgSimple.java 
[zuozewei, ]
com.techstar.monitordemo.controller.UserController,arg1

[zee, ]
com.techstar.monitordemo.controller.UserController,arg1

2、拦截构造函数

构造函数

@Data
public class User {
   

    private int id;
    private String name;

}

control方法

@GetMapping(value = "/arg2")
    public User arg2(User user) {
   
        return user;
    }

BTrace脚本

/**
 * 拦截构造函数
 */
@BTrace
public class PrintConstructor {
   

    @OnMethod(clazz = "com.techstar.monitordemo.domain.User", method = "<init>")
    public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, AnyType[] args) {
   
        BTraceUtils.println(pcn + "," + pmn);
        BTraceUtils.printArray(args);
        BTraceUtils.println();
    }
}

拦截结果

192:Btrace apple$ btrace 34119 PrintConstructor.java 
com.techstar.monitordemo.domain.User,<init>
[1, zuozewei, ]

3、拦截同名函数,以参数区分

control方法

@GetMapping(value = "/same1")
    public String same(@RequestParam("name") String name) {
   
        return "7DGroup," + name;
    }

    @GetMapping(value = "/same2")
    public String same(@RequestParam("id") int id, @RequestParam("name") String name) {
   
        return "7DGroup," + name + "," + id;
    }

BTrace脚本

/**
 * 拦截同名函数,通过输入的参数区分
 */

@BTrace
public class PrintSame {
   

    @OnMethod(clazz = "com.techstar.monitordemo.controller.UserController", method = "same")
    public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, String name) {
   
        BTraceUtils.println(pcn + "," + pmn + "," + name);
        BTraceUtils.println();
    }
}

拦截结果

192:Btrace apple$ jps -l
369 
5889 /Users/apple/Downloads/performance/apache-jmeter-4.0/bin/ApacheJMeter.jar
34281 sun.tools.jps.Jps
34220 org.jetbrains.jps.cmdline.Launcher
34221 com.techstar.monitordemo.MonitordemoApplication
192:Btrace apple$ btrace 34221 PrintSame.java 
com.techstar.monitordemo.controller.UserController,same,zuozewei

com.techstar.monitordemo.controller.UserController,same,zuozewei

com.techstar.monitordemo.controller.UserController,same,zuozewei

4、拦截方法返回值

BTrace脚本

/**
 * 拦截返回值
 */
@BTrace
public class PrintReturn {
   

    @OnMethod(clazz = "com.techstar.monitordemo.controller.UserController", method = "arg1",
            //拦截时刻:返回值
            location = @Location(Kind.RETURN))
    public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, @Return AnyType result) {
   
        BTraceUtils.println(pcn + "," + pmn + "," + result);
        BTraceUtils.println();
    }
}

拦截结果

192:Btrace apple$ jps -l
34528 org.jetbrains.jps.cmdline.Launcher
34529 com.techstar.monitordemo.MonitordemoApplication
369 
5889 /Users/apple/Downloads/performance/apache-jmeter-4.0/bin/ApacheJMeter.jar
34533 sun.tools.jps.Jps
192:Btrace apple$ btrace 34529 PrintReturn.java 
com.techstar.monitordemo.controller.UserController,arg1,7DGroup,zuozewei

5、异常分析

有时候开发人员对异常处理不合理,导致某些重要异常人为被吃掉,并且没有日志或者日志不详细,导致性能分析定位问题困难,我们可以使用BTrace来处理

control方法

@GetMapping(value = "/exception")
    public String exception() {
        try {
            System.out.println("start...");
            System.out.println(1 / 0); //模拟异常
            System.out.println("end...");
        } catch (Exception e) {}
        return "successful...";
    }

BTrace脚本

/**
 * 有时候,有些异常被人为吃掉,日志又没有打印,这个时候可以用该类定位问题
 * This example demonstrates printing stack trace
 * of an exception and thread local variables. This
 * trace script prints exception stack trace whenever
 * java.lang.Throwable's constructor returns. This way
 * you can trace all exceptions that may be caught and
 * "eaten" silently by the traced program. Note that the
 * assumption is that the exceptions are thrown soon after
 * creation [like in "throw new FooException();"] rather
 * that be stored and thrown later.
 */
@BTrace
public class PrintOnThrow {
   
    // store current exception in a thread local
    // variable (@TLS annotation). Note that we can't
    // store it in a global variable!
    @TLS
    static Throwable currentException;

    // introduce probe into every constructor of java.lang.Throwable
    // class and store "this" in the thread local variable.
    @OnMethod(clazz = "java.lang.Throwable", method = "<init>")
    public static void onthrow(@Self Throwable self) {
   
        currentException = self;
    }

    @OnMethod(clazz = "java.lang.Throwable", method = "<init>")
    public static void onthrow1(@Self Throwable self, String s) {
   
        currentException = self;
    }

    @OnMethod(clazz = "java.lang.Throwable", method = "<init>")
    public static void onthrow1(@Self Throwable self, String s, Throwable cause) {
   
        currentException = self;
    }

    @OnMethod(clazz = "java.lang.Throwable", method = "<init>")
    public static void onthrow2(@Self Throwable self, Throwable cause) {
   
        currentException = self;
    }

    // when any constructor of java.lang.Throwable returns
    // print the currentException's stack trace.
    @OnMethod(clazz = "java.lang.Throwable", method = "<init>", location = @Location(Kind.RETURN))
    public static void onthrowreturn() {
   
        if (currentException != null) {
   
            Threads.jstack(currentException);
            BTraceUtils.println("=====================");
            currentException = null;
        }
    }
}

拦截结果

192:Btrace apple$ jps -l
369 
5889 /Users/apple/Downloads/performance/apache-jmeter-4.0/bin/ApacheJMeter.jar
34727 sun.tools.jps.Jps
34666 org.jetbrains.jps.cmdline.Launcher
34667 com.techstar.monitordemo.MonitordemoApplication
192:Btrace apple$ btrace 34667 PrintOnThrow.java 
java.lang.ClassNotFoundException: org.apache.catalina.webresources.WarResourceSet
    java.net.URLClassLoader.findClass(URLClassLoader.java:381)
    java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    java.lang.ClassLoader.loadClass(ClassLoader.java:411)
    sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
    java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    org.apache.catalina.webresources.StandardRoot.isPackedWarFile(StandardRoot.java:656)
    org.apache.catalina.webresources.CachedResource.validateResource(CachedResource.java:109)
    org.apache.catalina.webresources.Cache.getResource(Cache.java:69)
    org.apache.catalina.webresources.StandardRoot.getResource(StandardRoot.java:216)
    org.apache.catalina.webresources.StandardRoot.getResource(StandardRoot.java:206)
    org.apache.catalina.mapper.Mapper.internalMapWrapper(Mapper.java:1027)
    org.apache.catalina.mapper.Mapper.internalMap(Mapper.java:842)
    org.apache.catalina.mapper.Mapper.map(Mapper.java:698)
    org.apache.catalina.connector.CoyoteAdapter.postParseRequest(CoyoteAdapter.java:679)
    org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:336)
    org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:800)
    org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
    org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:800)
    org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1471)
    org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
    java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    java.lang.Thread.run(Thread.java:748)
=====================
...

6、定位某个超过阈值的函数

BTrace脚本

**
 * 探测某个包路径下的方法执行时间是否超过某个阈值的程序,如果超过了该阀值,则打印当前线程的栈信息。
 */

import com.sun.btrace.BTraceUtils;
import com.sun.btrace.annotations.*;

import static com.sun.btrace.BTraceUtils.*;

@BTrace
public class PrintDurationTracer {
   
    @OnMethod(clazz = "/com\\.techstar\\.monitordemo\\..*/", method = "/.*/", location = @Location(Kind.RETURN))
    public static void trace(@ProbeClassName String pcn, @ProbeMethodName String pmn, @Duration long duration) {
   
        //duration的单位是纳秒
        if (duration > 1000 * 1000 * 2) {
   
            BTraceUtils.println(Strings.strcat(Strings.strcat(pcn, "."), pmn));
            BTraceUtils.print(" 耗时:");
            BTraceUtils.print(duration);
            BTraceUtils.println("纳秒,堆栈信息如下");
            jstack();
        }
    }
}

拦截结果

192:Btrace apple$ btrace 39644 PrintDurationTracer.java 
com.techstar.monitordemo.controller.Adder.execute 耗时:1715294657纳秒,堆栈信息如下
com.techstar.monitordemo.controller.Adder.execute(Adder.java:13)
com.techstar.monitordemo.controller.Main.main(Main.java:10)
com.techstar.monitordemo.controller.Adder.execute 耗时:893795666纳秒,堆栈信息如下
com.techstar.monitordemo.controller.Adder.execute(Adder.java:13)
com.techstar.monitordemo.controller.Main.main(Main.java:10)
com.techstar.monitordemo.controller.Adder.execute 耗时:1331363658纳秒,堆栈信息如下
com.techstar.monitordemo.controller.Adder.execute(Adder.java:13)

7、追踪方法执行时间

BTrace脚本

/**
 * 追踪某个方法的执行时间,实现原理同AOP一样。
 */
@BTrace
public class PrintExecuteTimeTracer {
   
    @TLS
    static long beginTime;

    @OnMethod(clazz = "com.techstar.monitordemo.controller.Adder", method = "execute")
    public static void traceExecuteBegin() {
   
        beginTime = timeMillis();
    }

    @OnMethod(clazz = "com.techstar.monitordemo.controller.Adder", method = "execute", location = @Location(Kind.RETURN))
    public static void traceExecute(int arg1, int arg2, @Return int result) {
   
        BTraceUtils.println(strcat(strcat("Adder.execute 耗时:", str(timeMillis() - beginTime)), "ms"));
        BTraceUtils.println(strcat("返回结果为:", str(result)));
    }
}

拦截结果

192:Btrace apple$ btrace 40863 PrintExecuteTimeTracer.java 
Adder.execute 耗时:803ms
返回结果为:797
Adder.execute 耗时:1266ms
返回结果为:1261
Adder.execute 耗时:788ms
返回结果为:784
Adder.execute 耗时:1524ms
返回结果为:1521
Adder.execute 耗时:1775ms

8、性能分析

压测的时候经常发现某一个服务变慢了,但是由于这个服务有很多的业务逻辑和方法构成,这个时候就不好定位到底慢在哪个地方。BTrace可以解决这个问题,只需要大概定位问题可能存在的地方,通过包路径模糊匹配,就可以找到问题。

BTrace脚本

/**
 *
 * Description:
 * This script demonstrates new capabilities built into BTrace 1.2
 * Shortened syntax - when omitting "public" identifier in the class
 * definition one can safely omit all other modifiers when declaring methods
 * and variables
 * Extended syntax for @ProbeMethodName annotation - you can use
 * parameter to request a fully qualified method name instead of
 * the short one
 * Profiling support - you can use {@linkplain Profiler} instance to gather
 * performance data with the smallest overhead possible
 */
@BTrace
class Profiling {
   
    @Property
    Profiler profiler = BTraceUtils.Profiling.newProfiler();

    @OnMethod(clazz = "/com\\.techstar\\..*/", method = "/.*/")
    void entry(@ProbeMethodName(fqn = true) String probeMethod) {
   
        BTraceUtils.Profiling.recordEntry(profiler, probeMethod);
    }

    @OnMethod(clazz = "/com\\.techstar\\..*/", method = "/.*/", location = @Location(value = Kind.RETURN))
    void exit(@ProbeMethodName(fqn = true) String probeMethod, @Duration long duration) {
   
        BTraceUtils.Profiling.recordExit(profiler, probeMethod, duration);
    }

    @OnTimer(5000)
    void timer() {
   
        BTraceUtils.Profiling.printSnapshot("Performance profile", profiler);
    }

9、死锁排查

我们怀疑程序是否有死锁,可以通过以下的脚步扫描追踪,非常简单方便。

/**
 * This BTrace program demonstrates deadlocks
 * built-in function. This example prints
 * deadlocks (if any) once every 4 seconds.
 */
@BTrace
public class PrintDeadlock {
   
    @OnTimer(4000)
    public static void print() {
   
        deadlocks();
    }
}

七、小结

BTrace是一个事后工具,所谓的事后工具就是在服务已经上线或者压测后,但是发现有问题的时候,可以使用BTrace动态跟踪分析。

  1. 比如哪些方法执行太慢,例如监控方法执行时间超过1秒的方法;
  2. 查看哪些方法调用了system.gc(),调用栈是怎样的;
  3. 查看方法的参数和属性
  4. 哪些方法发生了异常
    ...

总之,这里只是将部分经常用的列举了下抛砖引玉,还有很多没有列举,大家可以参考官方的其他Sample去玩下。

本文源码:
https://github.com/zuozewei/blog-example/tree/master/Performance-testing/03-performance-monitoring/btrace

目录
相关文章
|
7天前
|
人工智能 缓存 监控
使用LangChain4j构建Java AI智能体:让大模型学会使用工具
AI智能体是大模型技术的重要演进方向,它使模型能够主动使用工具、与环境交互,以完成复杂任务。本文详细介绍如何在Java应用中,借助LangChain4j框架构建一个具备工具使用能力的AI智能体。我们将创建一个能够进行数学计算和实时信息查询的智能体,涵盖工具定义、智能体组装、记忆管理以及Spring Boot集成等关键步骤,并展示如何通过简单的对话界面与智能体交互。
134 1
|
11天前
|
Java
java入门代码示例
本文介绍Java入门基础,包含Hello World、变量类型、条件判断、循环及方法定义等核心语法示例,帮助初学者快速掌握Java编程基本结构与逻辑。
162 0
|
13天前
|
Java Spring
如何优化Java异步任务的性能?
本文介绍了Java中四种异步任务实现方式:基础Thread、线程池、CompletableFuture及虚拟线程。涵盖多场景代码示例,展示从简单异步到复杂流程编排的演进,适用于不同版本与业务需求,助你掌握高效并发编程实践。(239字)
116 6
|
19天前
|
数据采集 存储 弹性计算
高并发Java爬虫的瓶颈分析与动态线程优化方案
高并发Java爬虫的瓶颈分析与动态线程优化方案
|
19天前
|
Java API 数据库
2025 年最新 Java 实操学习路线,从入门到高级应用详细指南
2025年Java最新实操学习路线,涵盖从环境搭建到微服务、容器化部署的全流程实战内容,助你掌握Java 21核心特性、Spring Boot 3.2开发、云原生与微服务架构,提升企业级项目开发能力,适合从入门到高级应用的学习需求。
272 0
|
28天前
|
前端开发 Java 数据库连接
帮助新手快速上手的 JAVA 学习路线最详细版涵盖从入门到进阶的 JAVA 学习路线
本Java学习路线涵盖从基础语法、面向对象、异常处理到高级框架、微服务、JVM调优等内容,适合新手入门到进阶,助力掌握企业级开发技能,快速成为合格Java开发者。
292 3
|
28天前
|
监控 Java API
2025 年全新出炉的 Java 学习路线:从入门起步到实操精通的详细指南
2025年Java学习路线与实操指南,涵盖Java 21核心特性、虚拟线程、Spring Boot 3、微服务、Spring Security、容器化部署等前沿技术,助你从入门到企业级开发进阶。
224 0
|
1月前
|
缓存 Java 开发者
Java 开发者必看!ArrayList 和 LinkedList 的性能厮杀:选错一次,代码慢成蜗牛
本文深入解析了 Java 中 ArrayList 和 LinkedList 的性能差异,揭示了它们在不同操作下的表现。通过对比随机访问、插入、删除等操作的效率,指出 ArrayList 在多数场景下更高效,而 LinkedList 仅在特定情况下表现优异。文章强调选择合适容器对程序性能的重要性,并提供了实用的选择法则。
106 3
|
2月前
|
NoSQL Java 关系型数据库
Java 从入门到进阶完整学习路线图规划与实战开发最佳实践指南
本文为Java开发者提供从入门到进阶的完整学习路线图,涵盖基础语法、面向对象、数据结构与算法、并发编程、JVM调优、主流框架(如Spring Boot)、数据库操作(MySQL、Redis)、微服务架构及云原生开发等内容,并结合实战案例与最佳实践,助力高效掌握Java核心技术。
221 0
|
2月前
|
安全 Java 编译器
new出来的对象,不一定在堆上?聊聊Java虚拟机的优化技术:逃逸分析
逃逸分析是一种静态程序分析技术,用于判断对象的可见性与生命周期。它帮助即时编译器优化内存使用、降低同步开销。根据对象是否逃逸出方法或线程,分析结果分为未逃逸、方法逃逸和线程逃逸三种。基于分析结果,编译器可进行同步锁消除、标量替换和栈上分配等优化,从而提升程序性能。尽管逃逸分析计算复杂度较高,但其在热点代码中的应用为Java虚拟机带来了显著的优化效果。
61 4