深入字节码 -- 计算方法执行时间(ONE APM基础原理窥探)

来源:互联网 发布:matlab 2017b mac 编辑:程序博客网 时间:2024/05/01 23:43

市面上有听云、oneapm等性能分析工具,通过对使用的APK反编译分析,他们提供的PLUGIN都做了很重要的一件事情,就是在class文件进行了代码打点,

也就是在实际的代码上做了他们自己的一些代码以便于用于性能分析。

详细的技术参考:http://www.tuicool.com/articles/7zYR3aU


什么是字节码?

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;)J", 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.get(key);        }        public static void setStartTime(String key) {            startTimes.put(key, System.currentTimeMillis());        }        public static long getEndTime(String key) {            return endTimes.get(key);        }        public static void setEndTime(String key) {            endTimes.put(key, System.currentTimeMillis());        }        public static long 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);            return exclusive;        }    }

题记

  • 上面的代码难免有 bug ,如果你发现代码写的有问题,请你帮忙指出,让我们一起进步,让代码变的更漂亮和健壮;
  • 顺便打点广告,如果看后对字节码技术感兴趣,欢迎加入我们oneapm,一起做点有意思事情,可直接联系我;
  • 完整代码请访问 github ;
  • 下一篇结合 demo 再深入研究 ClassVisitor

    OneAPM 为您提供端到端的Java 应用性能解决方案,我们支持所有常见的 Java 框架及应用服务器,助您快速发现系统瓶颈,定位异常根本原因。分钟级部署,即刻体验,Java 监控从来没有如此简单。想阅读更多技术文章,请访问OneAPM 官方技术博客,还可以扫码关注下方的Java程序性能优化公众号。


0 0
原创粉丝点击