trace java_使用java动态字节码技术简单实现arthas的trace功能。
参考资料
用过[Arthas]的都知道,Arthas是alibaba开源的一个非常强大的Java诊断工具。
不管是线上还是线下,我们都可以用Arthas分析程序的线程状态、查看jvm的实时运行状态、打印方法的出入参和返回类型、收集方法中每个代码块耗时,
甚至可以监控类、方法的调用次数、成功次数、失败次数、平均响应时长、失败率等。
前几天学习java动态字节码技术时,突然想起这款java诊断工具的trace功能:打印方法中每个节点的调用耗时。简简单单的,正好拿来做动态字节码入门学习的demo。
程序结构
src
├── agent-package.bat
├── java
│ ├── asm
│ │ ├── MANIFEST.MF
│ │ ├── TimerAgent.java
│ │ ├── TimerAttach.java
│ │ ├── TimerMethodVisitor.java
│ │ ├── TimerTrace.java
│ │ └── TimerTransformer.java
│ └── demo
│ ├── MANIFEST.MF
│ ├── Operator.java
│ └── Test.java
├── run-agent.bat
├── target-package.bat
└── tools.jar
编写目标程序
代码
package com.gravel.demo.test.asm;
/**
* @Auther: syh
* @Date: 2020/10/12
* @Description:
*/
public class Test {
public static boolean runnable = true;
public static void main(String[] args) throws Exception {
while (runnable) {
test();
}
}
// 目标:分析这个方法中每个节点的耗时
public static void test() throws Exception {
Operator.handler();
long time_wait = (long) ((Math.random() * 1000) + 2000);
Operator.callback();
Operator.pause(time_wait);
}
}
Operator.java
/**
* @Auther: syh
* @Date: 2020/10/28
* @Description: 辅助类,同样可用于分析耗时
*/
public class Operator {
public static void handler() throws Exception {
long time_wait = (long) ((Math.random() * 10) + 20);
sleep(time_wait);
}
public static void callback() throws Exception {
long time_wait = (long) ((Math.random() * 10) + 20);
sleep(time_wait);
}
public static void pause(long time_wait) throws Exception {
sleep(time_wait);
}
public static void stop() throws Exception {
Test.runnable = false;
System.out.println("business stopped.");
}
private static void sleep(long time_wait) throws Exception {
Thread.sleep(time_wait);
}
}
MANIFEST.MF
编写MANIFEST.MF文件,指定main-class。注意:冒号后面加空格,结尾加两行空白行。
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: syh
Created-By: Apache Maven
Build-Jdk: 1.8.0_202
Main-Class: com.gravel.demo.test.asm.Target
打包
偷懒写了bat批命令,生成target.jar
@echo off & setlocal
attrib -s -h -r -a /s /d demo
rd /s /q demo
rd /q target.jar
javac -encoding utf-8 -d . ./java/demo/*.java
jar cvfm target.jar ./java/demo/MANIFEST.MF demo
rd /s /q demo
pause
java -jar target.jar
java agent探针
instrument 是 JVM 提供的一个可以修改已加载类文件的类库。而要实现代码的修改,我们需要实现一个 instrument agent。
jdk1.5时,agent有个内定方法premain。是在类加载前修改。所以无法做到修改正在运行的类。
jdk1.6后,agent新增了agentmain方法。agentmain是在虚拟机启动以后加载的。所以可以做拦截、热部署等。
讲JAVA探针技术,实际上我自己也是半吊子。所以这里用的是边分析别人例子边摸索的思路来实现我的简单的trace功能。
例子使用的是ASM字节码生成框架
MANIFEST.MF
首先一个可用的jar,关键之一是MAINFEST.MF文件是吧。
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Created-By: Apache Maven
Built-By: syh
Build-Jdk: 1.8.0_202
Agent-Class: asm.TimerAgent
Can-Retransform-Classes: true
Can-Redefine-Classes: true
Class-Path: ./tools.jar
Main-Class: asm.TimerAttach
我们从MANIFEST.MF中提取几个关键的属性
属性
说明
Agent-Class
agentmain入口类
Premain-Class
premain入口类,与agent-class至少指定一个。
Can-Retransform-Classes
对于已经加载的类重新进行转换处理,即会触发重新加载类定义。
Can-Redefine-Classes
对已经加载的类不做转换处理,而是直接把处理结果(bytecode)直接给JVM
Class-Path
asm动态字节码技术依赖tools.jar,如果没有可以从jdk的lib目录下拷贝。
Main-Class
这里并不是agent的关键属性,为了方便,我把加载虚拟机的程序和agent合并了。
代码
然后我们来看看两个入口类,首先分析一个可执行jar的入口类Main-Class。
public class TimerAttach {
public static void main(String[] args) throws Exception {
/**
* 启动jar时,需要指定两个参数:1目标程序的pid。 2 要修改的类路径及方法,格式 package.class#methodName
*/
if (args.length < 2) {
System.out.println("pid and class must be specify.");
return;
}
if (!args[1].contains("#")) {
System.out.println("methodName must be specify.");
return;
}
VirtualMachine vm = VirtualMachine.attach(args[0]);
// 这里为了方便我把 vm和agent整合在一个jar里面了, args[1]就是agentmain的入参。
vm.loadAgent("agent.jar", args[1]);
}
}
代码很简单,1:args入参校验;2:加载目标进程pid(args[0]);3:加载agent jar包(因为合并了,所以这个jar其实就是自己)。
其中vm.loadAgent(agent.jar, args[1])会调用agent-class中的agentmain方法,而args[1]就是agentmain的第一个入参。
public class TimerAgent {
public static void agentmain(String agentArgs, Instrumentation inst) {
String[] ownerAndMethod = agentArgs.split("#");
inst.addTransformer(new TimerTransformer(ownerAndMethod[1]), true);
try {
inst.retransformClasses(Class.forName(ownerAndMethod[0]));
System.out.println("agent load done.");
} catch (Exception e) {
e.printStackTrace();
System.out.println("agent load failed!");
}
}
}
在 agentmain 方法里,我们调用retransformClassess方法载入目标类,调用addTransformer方法加载TimerTransformer类实现对目标类的重新定义。
类转换器
public class TimerTransformer implements ClassFileTransformer {
private String methodName;
public TimerTransformer(String methodName) {
this.methodName = methodName;
}
@Override
public byte[] transform(ClassLoader loader, String className, Class> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classFileBuffer) {
ClassReader reader = new ClassReader(classFileBuffer);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor classVisitor = new TimerTrace(Opcodes.ASM5, classWriter, methodName);
reader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
return classWriter.toByteArray();
}
}
对被匹配到的类中的方法进行修改
public class TimerTrace extends ClassVisitor implements Opcodes {
private String owner;
private boolean isInterface;
private String methodName;
public TimerTrace(int i, ClassVisitor classVisitor, String methodName) {
super(i, classVisitor);
this.methodName = methodName;
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
owner = name;
isInterface = (access & ACC_INTERFACE) != 0;
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
// 匹配到指定methodName时,进行字节码修改
if (!isInterface && mv != null && name.equals(methodName)) {
// System.out.println(" package.className:methodName()")
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "", "()V", false);
mv.visitLdcInsn(" " + owner.replace("/", ".")
+ ":" + methodName + "() ");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append",
"(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
// 方法代码块耗时统计并打印
TimerMethodVisitor at = new TimerMethodVisitor(owner, access, name, descriptor, mv);
return at.getLocalVariablesSorter();
}
return mv;
}
public static void main(String[] args) throws IOException {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
TraceClassVisitor tv = new TraceClassVisitor(cw, new PrintWriter(System.out));
TimerTrace addFiled = new TimerTrace(Opcodes.ASM5, tv, "test");
ClassReader classReader = new ClassReader("demo.Test");
classReader.accept(addFiled, ClassReader.EXPAND_FRAMES);
File file = new File("out/production/asm-demo/demo/Test.class");
String parent = file.getParent();
File parent1 = new File(parent);
parent1.mkdirs();
file.createNewFile();
FileOutputStream fileOutputStream = new FileOutputStream(file);
fileOutputStream.write(cw.toByteArray());
}
}
要统计方法中每行代码耗时,只需要在每一行代码的前后加上当前时间戳然后相减即可。
所以我们的代码是这么写的。
public class TimerMethodVisitor extends MethodVisitor implements Opcodes {
private int start;
private int end;
private int maxStack;
private String lineContent;
public boolean instance = false;
private LocalVariablesSorter localVariablesSorter;
private AnalyzerAdapter analyzerAdapter;
public TimerMethodVisitor(String owner, int access, String name, String descriptor, MethodVisitor methodVisitor) {
super(Opcodes.ASM5, methodVisitor);
this.analyzerAdapter = new AnalyzerAdapter(owner, access, name, descriptor, this);
localVariablesSorter = new LocalVariablesSorter(access, descriptor, this.analyzerAdapter);
}
public LocalVariablesSorter getLocalVariablesSorter() {
return localVariablesSorter;
}
/**
* 进入方法后,最先执行
* 所以我们可以在这里定义一个最开始的时间戳, 然后创建一个局部变量var_end
* Long var_start = System.nanoTime();
* Long var_end;
*/
@Override
public void visitCode() {
mv.visitCode();
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;", false);
start = localVariablesSorter.newLocal(Type.LONG_TYPE);
mv.visitVarInsn(ASTORE, start);
end = localVariablesSorter.newLocal(Type.LONG_TYPE);
maxStack = 4;
}
/**
* 在每行代码后面增加以下代码
* var_end = System.nanoTime();
* System.out.println("[" + String.valueOf((var_end.doubleValue() - var_start.doubleValue()) / 1000000.0D) + "ms] " + "package.className:methodName() #lineNumber");
* var_start = var_end;
* @param lineNumber
* @param label
*/
@Override
public void visitLineNumber(int lineNumber, Label label) {
super.visitLineNumber(lineNumber, label);
if (instance) {
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;", false);
mv.visitVarInsn(ASTORE, end);
// System.out
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
// new StringBuilder();
mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "", "()V", false);
mv.visitLdcInsn(" -[");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append",
"(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitVarInsn(ALOAD, end);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Long", "doubleValue", "()D", false);
mv.visitVarInsn(ALOAD, start);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Long", "doubleValue", "()D", false);
mv.visitInsn(DSUB);
mv.visitLdcInsn(new Double(1000 * 1000));
mv.visitInsn(DDIV);
// String.valueOf((end - start)/1000000)
mv.visitMethodInsn(INVOKESTATIC, "java/lang/String", "valueOf", "(D)Ljava/lang/String;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append",
"(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitLdcInsn("ms] ");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append",
"(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
// .append("owner:methodName() #line")
mv.visitLdcInsn(this.lineContent + "#" + lineNumber);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append",
"(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
// stringBuilder.toString()
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
// println stringBuilder.toString()
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
// start = end
mv.visitVarInsn(ALOAD, end);
mv.visitVarInsn(ASTORE, start);
maxStack = Math.max(analyzerAdapter.stack.size() + 4, maxStack);
}
instance = true;
}
/**
* 拼接字节码内容
* @param opcode
* @param owner
* @param methodName
* @param descriptor
* @param isInterface
*/
@Override
public void visitMethodInsn(int opcode, String owner, String methodName, String descriptor, boolean isInterface) {
super.visitMethodInsn(opcode, owner, methodName, descriptor, isInterface);
if (!isInterface && opcode == Opcodes.INVOKESTATIC) {
this.lineContent = owner.replace("/", ".")
+ ":" + methodName + "() ";
}
}
@Override
public void visitMaxs(int maxStack, int maxLocals) {
super.visitMaxs(Math.max(maxStack, this.maxStack), maxLocals);
}
}
如果初学者不会改字节码。可以利用idea自带的asm插件做参考。
打包
这样,一个可执行的agent jar就写完了,然后打包
@echo off
attrib -s -h -r -a /s /d asm
rd /s /q asm
rd /q agent.jar
javac -XDignore.symbol.file=true -encoding utf-8 -d . ./java/asm/*.java
jar cvfm agent.jar ./java/asm/MANIFEST.MF asm
rd /s /q asm
exit
测试
运行目标程序 target.jar
java -jar target.jar
打印Test.test中每个节点耗时
java -jar agent.jar [pid] demo.Test#test
结果
打印Operator.handler方法每个节点耗时
trace java_使用java动态字节码技术简单实现arthas的trace功能。相关推荐
- 【Java 虚拟机原理】动态字节码技术 | Dalvik ART 虚拟机 | Android 字节码打包过程
文章目录 一.动态字节码技术 二.Dalvik & ART 虚拟机 三.Android 字节码打包过程 总结 一.动态字节码技术 动态字节码技术 就是在 运行时 , 动态修改 Class 字节 ...
- java修改字节码技术,Javassist修改class,ASM修改class
背景: 项目使用的Logback 1.1.11版本的类ch.qos.logback.core.rolling.helper.RollingCalendar的periodBarriersCrossed方 ...
- JAVA的字节码技术
1.什么是字节码? 字节码 byteCode JVM能够解释执行的.java程序的归宿,但是从规范上来讲和Java已没有任何关系了.一些动态语言也可以编译成字节码在JVM上运行.字节码就相当于JVM上 ...
- 学习笔记:Java虚拟机——JVM内存结构、垃圾回收、类加载与字节码技术
学习视频来源:https://www.bilibili.com/video/BV1yE411Z7AP Java类加载机制与ClassLoader详解推荐文章:https://yichun.blog.c ...
- Java字节码技术(二)字节码增强之ASM、JavaAssist、Agent、Instrumentation
文章目录 前言 从AOP说起 静态代理 动态代理 JavaProxy CGLIB 字节码增强实现AOP ASM JavaAssist 运行时类加载 Instrumentation接口 JavaAgen ...
- OpenRasp Java运行时修改字节码技术
Java运行时修改字节码技术 Java运行时动态修改字节码技术,常用的有javassist asm来实现.不过最近在分析openrasp-java这块时,程序使用的javassist来动态插桩关键类, ...
- 定义入栈java_小师妹学JVM之:java的字节码byte code简介
简介 Byte Code也叫做字节码,是连接java源代码和JVM的桥梁,源代码编译成为字节码,而字节码又被加载进JVM中运行.字节码怎么生成,怎么查看字节码,隐藏在Byte Code背后的秘密是什么 ...
- 深入学习JVM探针与字节码技术
JVM探针是自jdk1.5以来,由虚拟机提供的一套监控类加载器和符合虚拟机规范的代理接口,结合字节码指令能够让开发者实现无侵入的监控功能.如:监控生产环境中的函数调用情况或动态增加日志输出等等.虽然在 ...
- JVM 学习笔记(三)类加载与字节码技术内存模型
四.类加载与字节码技术 1.类文件结构 通过 javac 类名.java 编译 java 文件后,会生成一个 .class 的文件! 以下是字节码文件: 0000000 ca fe ba be 00 ...
最新文章
- LeetCode:Spiral Matrix I II
- 无线充电系统的功率与效率
- Linux System Programming --Chapter Eight
- OpenCV中向量是可以转化为MAT类型的~
- Win32环境下两种用于C++的线程同步类(上)
- 微信自动回复和自动抢红包实现原理(二):自动回复
- 3目标检测的准确率_吊打YOLOv3!普林斯顿大学提出:CornerNetLite,基于关键点的实时且精度高的目标检测算法,已开源!...
- 爬虫、框架scrapy
- Springboot中,如何读取配置文件中的属性
- 转: React系统的入门系统
- cart算法_决策树学习笔记(三):CART算法,决策树总结
- Wireshark教程:识别主机和用户
- 工商银行java script error windows7_Win8.1装工行网银提示"called runscript when not marked in progress"的解决方法...
- 解决VS2017安装一直卡在正在下载
- 服务器被攻击怎么办?常见处理方法
- 学习总结1-跟开涛学SpringMVC
- Python爬虫,爬取51job上有关大数据的招聘信息
- python培训中心-python培训中心
- 计算机专业考研面试题大全———持续更新中
- 从零开始的Android:Android应用程序开发概述
热门文章
- NVIDIA发布全新推理软件,开创交互式会话AI新时代!
- 【重磅快讯】T11 2019数据智能技术峰会举办,AI将成为行业颠覆者
- 语音识别现状与工程师必备技能
- axure中出现小手_你所不知道的15个Axure使用技巧
- java jndi使用_Java项目中使用JNDI连接数据库
- centos标准分区调整大小_CentOS下调整home和根分区大小的方法
- 115配额怎么增加_笔电、平板接口少怎么办,ORICO八合一多功能扩展坞助你一臂之力...
- Linux环境 安装 Redis-6.2.6 配置运行_01
- 使html表格可编辑状态,js+Html实现表格可编辑操作
- 信号扫描_图文并茂,一文读懂信号源