您的位置:首页 > 其它

深入字节码 -- 计算方法执行时间

2016-02-22 16:57 357 查看
什么是字节码

java
程序通过
javac
编译之后生成文件
.class
就是字节码集合,正是有这样一种
中间码(字节码)
,使得
scala/groovy/clojure
等函数语言只用实现一个编译器即可运行在
JVM
上。

看看一段简单代码。

public long getExclusiveTime() {
long startTime = System.currentTimeMillis();
System.out.printf("exclusive code");
long endTime = System.currentTimeMillis();
return endTime - startTime;
}
public class com.blueware.agent.StartAgent {

编译后通过命令(
javap -c com.blueware.agent.StartAgent
)查看,具体含义请参考oracle

public com.blueware.agent.StartAgent();
Code:
0: aload_0
1: invokespecial #1  // Method java/lang/Object."<init>":()V
4: return

public long getExclusiveTime();
Code:
0: invokestatic  #2                  // Method java/lang/System.currentTimeMillis:()J
3: lstore_1
4: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc           #4                  // String exclusive code
9: iconst_0
10: anewarray     #5                  // class java/lang/Object
13: invokevirtual #6                  // Method java/io/PrintStream.printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
16: pop
17: invokestatic  #2                  // Method java/lang/System.currentTimeMillis:()J
20: lstore_3
21: lload_3
22: lload_1
23: lsub
24: lreturn
}

为什么要学习字节码

能了解技术背后的原理,更容易写出高质量代码;

字节码设计非常优秀,发展十几年只仅仅删除和增加几个指令,学懂之后长期受益高,如果懂字节码再学习
scala/groovy/clojure
会容易很多;

开发框架、监控系统、中间件、语言字节码技术都是必杀技;

字节码框架(
ASM/Javassist
)


操作字节码框架有很多,具体可以参考博文,下面对比
ASM/Javassist


选项优点缺点
ASM
速度快、代码量小、功能强大要写字节码、学习曲线高
Javassist
学习简单,不用写字节码
ASM
慢,功能少
Java Instrumentation
介绍


指的是可以用独立于应用程序之外的代理(
agent
)程序,
agent
程序通过增强字节码动态修改或者新增类,利用这样特性可以设计出更通用的监控、框架、中间件程序,在
JVM
启动参数加
–javaagent:agent_jar_path/agent.jar
即可运行(在
JDK5
及其后续版本才可以),更多关于
Instrumentation
知识请参考博文

计算方法执行时间方式

直接在代码开始和结束出打印当前时间,相减即可得到;

实现一个动态代理,或者借助
Spring/AspectJ
等框架;

上面两种实现方式都需要修改代码或者配置文件,下面我要介绍方式不仅不需要修改代码,而且效率高;

具体实现方式

1.StartAgent
类必须提供
premain
方法,代码如下:

public class StartAgent {
//代理程序入口函数
public static void premain(String args, Instrumentation inst) {
System.out.println("agent begin");
//添加字节码转换器
inst.addTransformer(new PrintTimeTransformer());
System.out.println("agent end");
}
}

2.PrintTimeTransformer
实现一个转换器,代码如下:

//字节码转化器类
public class PrintTimeTransformer implements ClassFileTransformer {

//实现字节码转化接口,一个小技巧建议实现接口方法时写@Override,方便重构
//loader:定义要转换的类加载器,如果是引导加载器,则为 null(在这个小demo暂时还用不到)
//className:完全限定类内部形式的类名称和中定义的接口名称,例如"java.lang.instrument.ClassFileTransformer"
//classBeingRedefined:如果是被重定义或重转换触发,则为重定义或重转换的类;如果是类加载,则为 null
//protectionDomain:要定义或重定义的类的保护域
//classfileBuffer:类文件格式的输入字节缓冲区(不得修改)
//一个格式良好的类文件缓冲区(转换的结果),如果未执行转换,则返回 null。
@Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException {
//简化测试demo,直接写待修改的类(com/blueware/agent/TestTime)
if (className != null && className.equals("com/blueware/agent/TestTime")) {
//读取类的字节码流
ClassReader reader = new ClassReader(classfileBuffer);
//创建操作字节流值对象,ClassWriter.COMPUTE_MAXS:表示自动计算栈大小
ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS);
//接受一个ClassVisitor子类进行字节码修改
reader.accept(new TimeClassVisitor(writer, className), 8);
//返回修改后的字节码流
return writer.toByteArray();
}
return null;
}
}

3.TimeClassVisitor
类访问器,实现字节码修改,代码如下:

//定义扫描待修改class的visitor,visitor就是访问者模式
public class TimeClassVisitor extends ClassVisitor {
private String className;

public TimeClassVisitor(ClassVisitor cv, String className) {
super(Opcodes.ASM5, cv);
this.className = className;
}

//扫描到每个方法都会进入,参数详情下一篇博文详细分析
@Override public MethodVisitor visitMethod(int access, final String name, final String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
final String key = className + name + desc;
//过来待修改类的构造函数
if (!name.equals("<init>") && mv != null) {
mv = new AdviceAdapter(Opcodes.ASM5, mv, access, name, desc) {
//方法进入时获取开始时间
@Override public void onMethodEnter() {
//相当于com.blueware.agent.TimeUtil.setStartTime("key");
this.visitLdcInsn(key);
this.visitMethodInsn(Opcodes.INVOKESTATIC, "com/blueware/agent/TimeUtil", "setStartTime", "(Ljava/lang/String;)V", false);
}

//方法退出时获取结束时间并计算执行时间
@Override public void onMethodExit(int opcode) {
//相当于com.blueware.agent.TimeUtil.setEndTime("key");
this.visitLdcInsn(key);
this.visitMethodInsn(Opcodes.INVOKESTATIC, "com/blueware/agent/TimeUtil", "setEndTime", "(Ljava/lang/String;)V", false);
//向栈中压入类名称
this.visitLdcInsn(className);
//向栈中压入方法名
this.visitLdcInsn(name);
//向栈中压入方法描述
this.visitLdcInsn(desc);
//相当于com.blueware.agent.TimeUtil.getExclusiveTime("com/blueware/agent/TestTime","testTime");
this.visitMethodInsn(Opcodes.INVOKESTATIC, "com/blueware/agent/TimeUtil", "getExclusiveTime", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V", false);
}
};
}
return mv;
}
}

4.TimeClassVisitor
记录时间帮助类,代码如下:

public class TimeUtil {
private static Map<String, Long> startTimes = new HashMap<String, Long>();
private static Map<String, Long> endTimes   = new HashMap<String, Long>();

private TimeUtil() {
}

public static long getStartTime(String key) {
return startTimes.remove(key);
}

public static void setStartTime(String key) {
startTimes.put(key, System.currentTimeMillis());
}

public static long getEndTime(String key) {
return endTimes.remove(key);
}

public static void setEndTime(String key) {
endTimes.put(key, System.currentTimeMillis());
}

public static void getExclusiveTime(String className, String methodName, String methodDesc) {
String key = className + methodName + methodDesc;
long exclusive = getEndTime(key) - getStartTime(key);
System.out.println(className.replace("/", ".") + "." + methodName + " exclusive:" + exclusive);
}
}

题记

上面的代码难免有
bug
,如果你发现代码写的有问题,请你帮忙指出,让我们一起进步,让代码变的更漂亮和健壮;

顺便打点广告,如果看后对字节码技术感兴趣,欢迎加入我们(oneapm)一起做点有意思事情,可直接联系我;

完整代码请访问github;

下一篇结合
demo
在深入研究
ClassVisitor
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: