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

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

什么是字节码

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

代码语言:javascript
复制
	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

代码语言:javascript
复制
	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
}</code></pre></div></div><h3 id="173n0" name="%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E5%AD%A6%E4%B9%A0%E5%AD%97%E8%8A%82%E7%A0%81">为什么要学习字节码</h3><ul class="ul-level-0"><li>能了解技术背后的原理,更容易写出高质量代码;</li><li>字节码设计非常优秀,发展十几年只仅仅删除和增加几个指令,学懂之后长期受益高,如果懂字节码再学习<code>scala/groovy/clojure</code>会容易很多;</li><li>开发框架、监控系统、中间件、语言字节码技术都是必杀技;</li></ul><h3 id="dbglm" name="%E5%AD%97%E8%8A%82%E7%A0%81%E6%A1%86%E6%9E%B6(ASM/Javassist)">字节码框架(<code>ASM/Javassist</code>)</h3><p>操作字节码框架有很多,具体可以参考博文,下面对比<code>ASM/Javassist</code></p><div class="table-wrapper"><table><thead><tr><th style="text-align:left"><div><div class="table-header"><p>选项</p></div></div></th><th style="text-align:left"><div><div class="table-header"><p>优点</p></div></div></th><th style="text-align:left"><div><div class="table-header"><p>缺点</p></div></div></th></tr></thead><tbody><tr><td style="text-align:left"><div><div class="table-cell"><p>ASM</p></div></div></td><td style="text-align:left"><div><div class="table-cell"><p>速度快、代码量小、功能强大</p></div></div></td><td style="text-align:left"><div><div class="table-cell"><p>要写字节码、学习曲线高</p></div></div></td></tr><tr><td style="text-align:left"><div><div class="table-cell"><p>Javassist</p></div></div></td><td style="text-align:left"><div><div class="table-cell"><p>学习简单,不用写字节码</p></div></div></td><td style="text-align:left"><div><div class="table-cell"><p>比ASM慢,功能少</p></div></div></td></tr></tbody></table></div><h3 id="7v9tm" name="Java-Instrumentation%E4%BB%8B%E7%BB%8D"><code>Java Instrumentation</code>介绍</h3><p>指的是可以用独立于应用程序之外的代理(<code>agent</code>)程序,<code>agent</code>程序通过增强字节码动态修改或者新增类,利用这样特性可以设计出更通用的监控、框架、中间件程序,在<code>JVM</code>启动参数加<code>–javaagent:agent_jar_path/agent.jar</code>即可运行(在<code>JDK5</code>及其后续版本才可以),更多关于<code>Instrumentation</code>知识请参考博文</p><h3 id="b273a" name="%E8%AE%A1%E7%AE%97%E6%96%B9%E6%B3%95%E6%89%A7%E8%A1%8C%E6%97%B6%E9%97%B4%E6%96%B9%E5%BC%8F">计算方法执行时间方式</h3><ul class="ul-level-0"><li>直接在代码开始和结束出打印当前时间,相减即可得到;</li><li>实现一个动态代理,或者借助<code>Spring/AspectJ</code>等框架;</li><li>上面两种实现方式都需要修改代码或者配置文件,下面我要介绍方式不仅不需要修改代码,而且效率高;</li></ul><h3 id="8f26u" name="%E5%85%B7%E4%BD%93%E5%AE%9E%E7%8E%B0%E6%96%B9%E5%BC%8F">具体实现方式</h3><p><code>1.StartAgent</code>类必须提供<code>premain</code>方法,代码如下:</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-javascript"><code class="language-javascript" style="margin-left:0">	public class StartAgent {
    //代理程序入口函数
    public static void premain(String args, Instrumentation inst) {
        System.out.println(&#34;agent begin&#34;);
        //添加字节码转换器
        inst.addTransformer(new PrintTimeTransformer());
        System.out.println(&#34;agent end&#34;);
    }
}</code></pre></div></div><p><code>2.PrintTimeTransformer</code>实现一个转换器,代码如下:</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-javascript"><code class="language-javascript" style="margin-left:0">		//字节码转化器类
public class PrintTimeTransformer implements ClassFileTransformer {

    //实现字节码转化接口,一个小技巧建议实现接口方法时写@Override,方便重构
    //loader:定义要转换的类加载器,如果是引导加载器,则为 null(在这个小demo暂时还用不到)
    //className:完全限定类内部形式的类名称和中定义的接口名称,例如&#34;java.lang.instrument.ClassFileTransformer&#34;
    //classBeingRedefined:如果是被重定义或重转换触发,则为重定义或重转换的类;如果是类加载,则为 null
    //protectionDomain:要定义或重定义的类的保护域
    //classfileBuffer:类文件格式的输入字节缓冲区(不得修改)
    //一个格式良好的类文件缓冲区(转换的结果),如果未执行转换,则返回 null。
    @Override public byte[] transform(ClassLoader loader, String className, Class&lt;?&gt; classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer)
            throws IllegalClassFormatException {
        //简化测试demo,直接写待修改的类(com/blueware/agent/TestTime)
        if (className != null &amp;&amp; className.equals(&#34;com/blueware/agent/TestTime&#34;)) {
            //读取类的字节码流
            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;
    }
}</code></pre></div></div><p><code>3.TimeClassVisitor</code>类访问器,实现字节码修改,代码如下:</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-javascript"><code class="language-javascript" style="margin-left:0">		//定义扫描待修改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(&#34;&lt;init&gt;&#34;) &amp;&amp; mv != null) {
            mv = new AdviceAdapter(Opcodes.ASM5, mv, access, name, desc) {
                //方法进入时获取开始时间
                @Override public void onMethodEnter() {
                    //相当于com.blueware.agent.TimeUtil.setStartTime(&#34;key&#34;);
                    this.visitLdcInsn(key);
                    this.visitMethodInsn(Opcodes.INVOKESTATIC, &#34;com/blueware/agent/TimeUtil&#34;, &#34;setStartTime&#34;, &#34;(Ljava/lang/String;)V&#34;, false);
                }

                //方法退出时获取结束时间并计算执行时间
                @Override public void onMethodExit(int opcode) {
                    //相当于com.blueware.agent.TimeUtil.setEndTime(&#34;key&#34;);
                    this.visitLdcInsn(key);
                    this.visitMethodInsn(Opcodes.INVOKESTATIC, &#34;com/blueware/agent/TimeUtil&#34;, &#34;setEndTime&#34;, &#34;(Ljava/lang/String;)V&#34;, false);
                    //向栈中压入类名称
                    this.visitLdcInsn(className);
                    //向栈中压入方法名
                    this.visitLdcInsn(name);
                    //向栈中压入方法描述
                    this.visitLdcInsn(desc);
                    //相当于com.blueware.agent.TimeUtil.getExclusiveTime(&#34;com/blueware/agent/TestTime&#34;,&#34;testTime&#34;);
                    this.visitMethodInsn(Opcodes.INVOKESTATIC, &#34;com/blueware/agent/TimeUtil&#34;, &#34;getExclusiveTime&#34;, &#34;(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)J&#34;, false);
                }
            };
        }
        return mv;
    }
}</code></pre></div></div><p><code>4.TimeClassVisitor</code>记录时间帮助类,代码如下:</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-javascript"><code class="language-javascript" style="margin-left:0">  public class TimeUtil {
    private static Map&lt;String, Long&gt; startTimes = new HashMap&lt;String, Long&gt;();
    private static Map&lt;String, Long&gt; endTimes   = new HashMap&lt;String, Long&gt;();

    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(&#34;/&#34;, &#34;.&#34;) + &#34;.&#34; + methodName + &#34; exclusive:&#34; + exclusive);
        return exclusive;
    }
}</code></pre></div></div><h3 id="7kvr8" name="%E9%A2%98%E8%AE%B0">题记</h3><ul class="ul-level-0"><li>上面的代码难免有<code>bug</code>,如果你发现代码写的有问题,请你帮忙指出,让我们一起进步,让代码变的更漂亮和健壮;</li><li>顺便打点广告,如果看后对字节码技术感兴趣,欢迎加入我们oneapm,一起做点有意思事情,可直接联系我;</li><li>完整代码请访问github;</li><li>下一篇结合<code>demo</code>再深入研究<code>ClassVisitor</code></li></ul>