JVM系列文章目录
GC调优基础知识工具篇之Arthas与动态追踪技术
前言
本文基于JDK1.8,是博主个人的JVM学习记录,欢迎各位指正错误的地方。本文主要介绍Arash的使用及其底层实现——动态追踪技术。
arthas
官方文档
Arthas 是 Alibaba 开源的 Java 诊断工具, 支持 JDK 6+。
下载安装
Arthas其实就是个jar包,我们从官网下载到本地后解压后,可以通过java -jar arthas-boot.jar
启动即可。无需安装。
arthas快速入门
- 启动arthas。
- 选择要绑定Java进程,进行绑定。
常用指令
dashboard
查看当前系统的实时数据面板,这个面板5s刷新一次。大致数据如下:
输入q可以退出面板。
thread
这指令类似jstack ,主要是查看当前 JVM 的线程堆栈信息。
可以通过使用 thread –b
来进行死锁的排查死锁。
参数:
- -n 指定最忙的前 n 个线程并打印堆栈 。
- -b 找出阻塞当前线程的线程。
- -i 指定 cpu 占比统计的采样间隔,单位为毫秒。
jvm
这个就是查看jvm的相关信息。
jad
反编译指定已加载类的源码。
trace
使用 trace 命令可以跟踪统计方法耗时。
这里我启用一个spring boot 项目进行演示。
我这里启动了我项目工程的代码,所以路径打了码。
trace 要跟踪类的全限定名 要跟踪的方法名
现在我通过postmain调用接口
再回到arthas查看
我们使用monitor 每 5 秒统计一次上面那个类的mediaSave方法执行情况
monitor -c 时间(单位秒) 要监听的类的全限定名 要监听的方法名
watch
观察方法的入参出参信息。
watch 要观察到类全限定名 要观察的方法名 观察表达式
主要命令汇总
命令 | 介绍 |
---|---|
dashboard | 当前系统的实时数据面板 |
thread | 查看当前 JVM 的线程堆栈信息 |
watch | 方法执行数据观测 |
trace | 方法内部调用路径,并输出方法路径上的每个节点上耗时 |
stack | 方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下 调用进行观测 |
tt | 方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下 调用进行观测 |
monitor | 方法执行监控 |
jvm | 查看当前 JVM 信息 |
vmoption | 查看,更新 JVM 诊断相关的参数 |
sc | 查看 JVM 已加载的类信息 |
sm | 查看已加载类的方法信息 |
jad | 反编译指定已加载类的源码 |
classloader | 查看 classloader 的继承树,urls,类加载信息 |
heapdump | 类似 jmap 命令的 heap dump 功能 |
动态追踪技术
动态追踪技术实际上就是一种不需要重启服务,更改服务代码就能对代码进行问题排查的技术。我们上面介绍的arthas就是用的这种技术,arthas的其中一个核心就是Java Agent。
Java Agent
Java Agent有点类似于我们spring 里的AOP,但它是JVM字节码层面的。
Java Agent的实现有两种方式:
- premain:这个可以在main方法执行前进行一些操作。
- agentmain:是控制类运行时的行为(arthas就是用的这种)。
虽然有两种技术,但是JVM只会执行一种(也就是你只能二选一)。
现在我们来手动实验一下这两种方式:
premain
- 引入javassist 用来增强代码(可以理解为改写运行的代码,让代码“变异”)。
<dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.24.1-GA</version> </dependency>
- 我们写一个要运行的类,这个类将被用来“变异”。
/** * @author Abfeathers * @date 2021/3/19 * @Description VM参数中加入:-javaagent:./agent/target/agent-1.0-SNAPSHOT.jar * */ public class MainRun { public static void main(String[] args) { hello("world"); } private static void hello(String name) { System.out.println("hello " + name ); } }
-
编写agent
要构建一个 agent 程序,大体可分为以下步骤:-
使用字节码增强工具,编写增强代码;
-
在 manifest 中指定 Premain-Class/Agent-Class 属性;
-
使用参数加载或者使用 attach 方式改变 app 项目中的内容;
工程结构如下:
先写一个普通的java类,然后写两个方法premain和agentmainimport java.lang.instrument.Instrumentation; /** * @author Abfeathers * @date 2021/3/19 * @Description instrument 一共有两个 main 方法,一个是 premain,另一个是 agentmain * 但在一个 JVM 中,只会调用一个 * */ public class AgentApp { //在main 执行之前的修改 public static void premain(String agentOps, Instrumentation inst) { System.out.println("==============enter premain=============="); inst.addTransformer(new Agent()); } //控制类运行时的行为 public static void agentmain(String agentOps, Instrumentation inst) { System.out.println("==============enter agentmain=============="); } }
完成之后,我们就要编写核心了Transformer ,方法增强就得考它了。
我们编写Agent类,让它实现ClassFileTransformer,然后我们重写Transformer方法,在这个方法里写一些要执行的字节码增强的代码。
下面代码的逻辑大致是:a.获取 MainRun 类的字节码。 b.获取 hello 方法的字节码。 c.在方法前后,加入时间统计,首先定义变量 _begin,然后追加要写的代码。 d.最后把字节码返回。
import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain; public class Agent implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { String loadName = className.replaceAll("/", "."); //System.out.println(className); if (className.endsWith("MainRun")) { try { //javassist 完成字节码增强(打印方法的执行时间<纳秒>) CtClass ctClass = ClassPool.getDefault().get(loadName); CtMethod ctMethod = ctClass.getDeclaredMethod("hello"); ctMethod.addLocalVariable("_begin", CtClass.longType); ctMethod.insertBefore("_begin = System.nanoTime();"); ctMethod.insertAfter("System.out.println(System.nanoTime() - _begin);"); return ctClass.toBytecode(); } catch (Exception e) { e.printStackTrace(); } } return classfileBuffer; } }
-
-
打包agent
Manifest-Version: 1.0 Can-Redefine-Classes: true Can-Retransform-Classes: true premain-class: com.example.javaagent.AgentApp agentmain-class: AgentApp
最后执行指令mvn clean package
-
运行MainRun前将参数引入,我这里打包好的agent路径是
./agent/target/agent-1.0-SNAPSHOT.jar
。
-
agentmain
这种模式一般用在一些诊断工具上。使用 jdk/lib/tools.jar 中的工具类中的 Attach API,可以动态的为运行中的程序加入一些功能。它的主要运行步骤如下:
- 获取机器上运行的所有 JVM 进程 ID;
- 选择要诊断的 jvm;
- 将 jvm 使用 attach 函数链接上;
- 使用 loadAgent 函数加载 agent,动态修改字节码;
- 卸载 jvm。
Java Attach API
Attach API 不是 Java 的标准 API,而是 Sun 公司提供的一套扩展 API,用来向目标 JVM ”附着”(Attach)代理工具程序的。
有了它, 开发者可以方便的监控一个 JVM,运行一个外加的代理程序。Attach API 只有 2 个主要的类,都在 com.sun.tools.attach 包(在 jdk 的 lib 目录下 tools.jar 里面)里面:
VirtualMachine :代表一个 Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了 JVM 枚举,Attach 动作和 Detach 动作(Attach 动 作的相反行为,从 JVM 上面解除一个代理)等等 ;
VirtualMachineDescriptor: 则是一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能。 Java Attach API 是一个 API 接口,JDK 提供的,它可以将应用程序连接到另一个目标虚拟机。
JVM Attach API 功能上非常简单,主要功能如下 :
- Attach 到其中一个 JVM 上,建立通信管道
- 让目标 JVM 加载 Agent
了解之后,我们来实验一下:
我们这里直接把所有JVM进程都找到了。
匹配加载了StopWorld.class的JVM进程。
最后打印了一下JDK版本信息。
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
import java.util.Properties;
public class JvmAttach {
public static void main(String[] args)
throws Exception {
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : list) {
if (vmd.displayName().endsWith("StopWorld")) {
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
Properties props = virtualMachine.getSystemProperties();
String version = props.getProperty("java.version");
// virtualMachine.loadAgent("arthas-boot.jar ","...");
System.out.println("version:"+version);
virtualMachine.detach();
}
}
}
}
其实java agent就类似于安排间谍进行间谍活动,偷偷的在目标进程窃取信息再传输回来。当然真正的动态追踪肯定不是像我这样在目标进程里输出信息,是需要通过IO传输回监控进程的,要不然岂不是跟间谍潜伏后,在敌方一顿宣传我是间谍一样了。
借助 Btrace 手写动态追踪框架
什么是Btrace
btrace已经是个很古老的架构了,它也是一个实现动态追踪技术的架构,已经很少有人用了,因为入门门槛比较高,用起来也麻烦,没有arthas方便。
官方描述是这样的:
BTrace 是基于 Java 语言的一个安全的、可提供动态追踪服务的工具。BTrace 基于 ASM、Java Attach API、Instrument 开发,为用户提供 了很多注解。依靠这些注解,我们可以编写 BTrace 脚本。
配置Btrace
官方git地址
这里我们从官网下载btrace,在本地安装,并配置环境变量。
我这里是mac,我就提供一下我mac的配置。
#配置环境变量
vim ~/.bash_profile
#Btrace
BTRACE_HOME= /Users/abfeathers/btrace-2.1.0-bin
export BTRACE_HOME
export PATH=${PATH}:${BTRACE_HOME}/bin
#使配置文件生效
source ~/.bash_profile
构建一个简单的动态追踪脚手架
-
构建一个简单spring boot 工程,其中编写了这么一个类用于动态追踪
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** * @author Abfeathers * @date 2021/3/19 * @Description btace动态追踪脚手架测试 * */ @RestController @RequestMapping("/btrace") public class DemoController { @RequestMapping("/test") public String test(@RequestParam("name") String name){ return "hello,"+name; } @RequestMapping("/exception") public String exception(){ try { System.out.println("start......."); System.out.println(1/0); System.out.println("end........."); } catch (Exception e) { } return "success"; } }
-
再构建一个工程引入btrace依赖(也可以通过maven导入)
-
编写btrace脚本
@OnMethod 可以指定 clazz 、method、location。由此组成了在什么时机(location 决定)监控某个类/某些类(clazz 决定)下的某个方法/某些方法(method 决定)。 拦截时机由 location 决定,当然也可为同一个定位加入多个拦截时机,即可以在进入方法时拦截、方法返回时拦截、抛出异常时拦截。
-
clazz
clazz 支持,精准定位、正则表达式定位、 按 接 口 或 继 承 类 定 位 < 例 如 要 匹 配 继 承 或 实 现 了 com.kite.base 的 接 口 或 基 类 的 , 只 要 在 类 前 加 上 + 号 就 可 以 了 , 例 如 @OnMethod(clazz="+com.kite.base", method=“doSome”)>、 按注解定位<在前面加上 @ 即可,例如@OnMethod(clazz="@javax.jws.WebService", method="@javax.jws.WebMethod")> method 支持精准定位、正则表达式定位、按注解定位
-
location
-
Kind.Entry 与 Kind.Return 分别表示函数的开始和返回,不写 location 的情况下,默认为 Kind.Entry,仅获取参数值,可以用 Kind.Entry ,要获取返回值或执行时 间就要用 Kind.Return
-
Kind.Error, Kind.Throw 和 Kind.Catch, 表示异常被 throw 、异常被捕获还有异常发生但是没有被捕获的情况,在拦截函数的参数定义里注入一个 Throwable 的参数,代表异 常 3、Kind.Call 表示被监控的方法调用了哪些其他方法,Kind.Line 监测类是否执行到了设置的行数
-
package cn.abfeathers.btrace; import org.openjdk.btrace.core.BTraceUtils; import org.openjdk.btrace.core.annotations.*; import org.openjdk.btrace.core.types.AnyType; @BTrace public class TestBrace { @OnMethod( clazz = "cn.abfeathers.demo.controller.DemoController", method = "test", location = @Location(Kind.ENTRY) ) public static void checkEntry( @ProbeClassName String pcn, @ProbeMethodName String pmn, AnyType[] args ){ BTraceUtils.println("Class: "+pcn); BTraceUtils.println("Method: "+pmn); BTraceUtils.printArray(args); BTraceUtils.println("==========================="); BTraceUtils.println(); } }
-
-
使用jps指令查询一下spring boot 进程id
-
监听进程
-
访问spring boot
-
查看监听
我们在看另外一个接口的访问,我故意写了除零计算,然后catch了这个异常,正常我们访问是不会出现异常的。
现在我们换个脚本去监听package cn.abfeathers.btrace; import org.openjdk.btrace.core.BTraceUtils; import org.openjdk.btrace.core.annotations.*; /** * 类说明: */ @BTrace public class TraceException { @TLS static Throwable currentException; @OnMethod( clazz="java.lang.Throwable", method="<init>" ) public static void onthrow(@Self Throwable self) { // @Self其实就是拦截了this //new Throwable() currentException = self; } @OnMethod( clazz="java.lang.Throwable", method="<init>" ) public static void onthrow1(@Self Throwable self, String s) { //new Throwable(String msg) currentException = self; } @OnMethod( clazz="java.lang.Throwable", method="<init>" ) public static void onthrow1(@Self Throwable self, String s, Throwable cause) { //new Throwable(String msg, Throwable cause) currentException = self; } @OnMethod( clazz="java.lang.Throwable", method="<init>" ) public static void onthrow2(@Self Throwable self, Throwable cause) { //new Throwable(Throwable cause) currentException = self; } @OnMethod( clazz = "cn.abfeathers.demo.controller.DemoController", method = "exception", location=@Location(Kind.ERROR) ) public static void onthrowreturn() { if (currentException != null) { // 打印异常堆栈 BTraceUtils.Threads.jstack(currentException); BTraceUtils.println("====================="); // 打印完之后就置空 currentException = null; } } }
我们在访问,发现居然抛错了 ,还在堆栈日志中打印了错误日志
BTrace 注解
BTrace 注解可以分为:
- 类注解 @BTrace
- 方法注解如@OnMethod
- 参数注解如:@ProbeClassName
参数注解
@ProbeClassName :用于标记处理方法的参数,仅用户
@OnMethod, 该参数的值就是被跟踪的类名称
@ProbeMethodName用于表示处理方法的参数,仅用户
@OnMethod,该参数值是被跟踪方法名称
@Self当前截取方法的封闭实例参数
@Return当前截取方法的的返回值, 只对location=@Location(Kind.RETURN) 生效
@Duration当前截取方法的执行时间
@TargetInstance 当前截取方法内部调用的实例
@TargetMethodOrField当前截取方法内部被调用的方法名
方法注解
@OnMethod用于指定跟踪方法到目标类,目标方法和目标位置 格式
@Location 属性有:
- value 默认值为 Kind.ENTRY 即参数的入口位置
- where 限定探测位置 默认值为 Where.BEFORE 也可以设置为 Where.AFTER
- clazz
- method
- field
- type
- line
@Kind 注解的值有
- Kind.ENTRY-被 trace 方法参数
- Kind.RETURN-被 trace 方法返回值
- Kind.THROW -抛异常
- Kind.ARRAY_SET, Kind.ARRAY_GET -数组索引
- Kind.CATCH -捕获异常
- Kind.FIELD_SET -属性值
- Kind.LINE -行号
- Kind.NEW -类名
- Kind.ERROR -抛异常
@OnTimer 用于指定跟踪操作定时执行。value 用于指定时间间隔
@OnError当 trace 代码抛异常或者错误时,该注解的方法会被执行.如果同一个 trace 脚本中其他方法抛异常,该注解方法也会被执行。
Btrace 的限制
BTrace 最终借 Instrument 实现 class 的替换。出于安全考虑,Instrument 在使用上存在诸多的限制,这就好比给一架正在飞行的飞机换 发动机一样一样的,因此 BTrace 脚本的限制如下:
- 不允许创建对象
- 不允许创建数组
- 不允许抛异常
- 不允许 catch 异常
- 不允许随意调用其他对象或者类的方法,只允许调用 com.sun.btrace.BTraceUtils 中提供的静态方法(一些数据处理和信息输出工具)
- 不允许改变类的属性
- 不允许有成员变量和方法,只允许存在 static public void 方法
- 不允许有内部类、嵌套类
- 不允许有同步方法和同步块
- 不允许有循环
- 不允许随意继承其他类(当然,java.lang.Object 除外)
- 不允许实现接口 不允许使用 assert 不允许使用 Class 对象
从这么多限制,可以看出BTrace 要做的事:虽然修改了字节码,但是除了输出需要的信息外,对整个程序的正常运行并没有影响。
其实无论是arthas,还是btrace,底层无非就是基于 ASM、Java Attach API、Instrument 开发的创建。
下一篇:JVM调优之内存优化与GC优化