服务热线:13616026886

技术文档 欢迎使用技术文档,我们为你提供从新手到专业开发者的所有资源,你也可以通过它日益精进

位置:首页 > 技术文档 > JAVA > 新手入门 > 开发工具 > 查看文档

java 5 特性 instrumentation 实践

    instrumentation 是 java 5 提供的新特性。使用 instrumentation,开发者可以构建一个代理,用来监测运行在 jvm 上的程序。监测一般是通过在执行某个类文件之前,对该类文件的字节码进行适当修改进行的。下文将通过一个具体的例子,来展示 java.lang.instrument 包的工作原理,并且实现了一个测量函数运行时间的代理。

简介

    不使用instrumentation 来测量函数运行时间的传统方法是:在函数调用之前记录当前系统时间,在函数调用完成之后再次记录当前系统时间(为了简化描述,本文不考虑虚拟机进程映射到本地操作系统进程时造成的计时误差,详见use the jvm profiler interface for accurate timing)。最后将两次数据的差值作为本次函数运行时间返回。这种方法的弱点在于:

  • 用于性能测量的语句直接夹杂在逻辑代码中
  • 用于性能测量的逻辑是重复的,没有做到代码重用。

使用 instrumentation 提供的功能,结合 apache 开源项目 bcel,本文将实现一个用于测量函数运行时间的代理。通过代理技术,用于性能测量的语句与业务逻辑完全分离,同时该代理可以用于测量任意类的任意方法的运行时间,大大提高了代码的重用性。

greeting 代理

在实现函数运行时间测量代理之前,我们先通过实现一个简单的 greeting 代理,介绍一下 java 5 中 instrumentation 的原理。每个代理的实现类必须实现 classfiletransformer 接口。这个接口提供了一个

public byte[] transform(
    classloader loader, 
    string classname, 
    class cbr, 
    java.security.protectiondomain pd, 
    byte[] classfilebuffer) throws illegalclassformatexception
            

方法。通过这个方法,代理可以得到虚拟机载入的类的字节码(通过 classfilebuffer 参数)。代理的各种功能一般是通过操作这一串字节码得以实现的。同时还需要提供一个公共的静态方法:
static void premain(string agentargs, instrumentation inst)

。一般会在这个方法中创建一个代理对象,通过参数 inst 的 addtransformer() 方法,将创建的代理对象再传递给虚拟机。这个方法是一个入口方法,有点类似于一般类的 main 方法。图1展示了代理工作的原理


图1 代理工作原理
图1 代理工作原理

可以看到,多个代理可以同时执行。这多个代理的 premain 方法将按照代理指定的顺序被依次调用。

下面的代码片断,演示了 greeting 代理的 transform 方法。在该方法中我们对 agent 的行为进行了简单的定制――输出需要该代理监测的类名。


列表1 输出 hello, someclass

    
public byte[] transform(classloader loader,
              string classname,
              class cbr, java.security.protectiondomain pd,
              byte[] classfilebuffer) 
    throws illegalclassformatexception
  {
    system.out.println("hello,\t", classname);
    return null; 
  }
               

transform 函数的最后,返回 null 值,表示不需要进行类字节码的转化。定制完代理的行为之后,创建一个 greeting 代理的实例,将该实例传递给虚拟机。


列表2 将 greeting 代理的实例传递给虚拟机

public static void premain(string options, instrumentation ins) {
    if (options != null) {
system.out.printf("i've been called with options: \"%s\"\n", options);
    }
    else 
      system.out.println("  i've been called with no options.");
    ins.addtransformer(new greeting());
              }

options 参数是通过命令行传递进来的,类似于调用 main 函数时传递的参数。被传递进来的命令行参数是一个完整的字符串,不同于 main 方法,该字符串的解析完全由代理自己负责。列表 3 展示了如何使用命令行调用代理:


列表 3 通过命令行参数调用代理

    
            java -javaagent:greeting.jar="hello, sample" sample

这条命令表示,用参数”hello, sample”调用 greeting 代理,以检测 sample 类的运行情况。运行该命令之后的结果如下图:


图2 运行代理 greeting 的结果
greeting

代理需要被打包到一个符合特定标准的 jar 文件中运行。该 jar 文件的 manifest.mf 文件需要包括一些特殊的项以定义代理类等信息。(请查阅 java 5 规约,获取详细信息)在列表 4 中,我们指定了 greeting 代理的代理类是 greeting.class。


列表4 greeting 代理的 manifest.mf 文件

    
manifest-version: 1.0
premain-class: greeting
            

资源 greeting.jar 文件将包含 greeting 代理的源代码和类文件,以及使用说明。

 

 

timing 代理

在介绍完代理的基本原理之后,下文将实现一个用于测量函数运行时间的代理―― timing。传统的函数运行时间测量代码片断为:


列表 5 传统的测量函数运行时间代码片断

    
public void main(string[] args) {
long timeb = system. currenttimemillis();             (1)
methodx();
system.out.print(getcurrentthreadcputime() - timeb); (2)
}
private static void methodx()
{
        // originial code
}
            

使用了代理之后,语句 (1)(2) 可以被动态的添加到类字节码中,得到等同于如下代码片断的字节码。


列表 6 与经过代理转换的字节码相对应的类文件

    
public void main(string[] args) {
methodx();
}   
private static void methodx_original ()
{
        // originial code
}
private static void methodx()
{
        long timeb = getcurrentthreadcputime();
        methodx_original();
        long period = system. currenttimemillis() - timeb;        
}
            

列表 7 给出了timing 代理的完整代码,其中 addtimer 方法利用 bcel 的强大功能,动态的修改了虚拟机传递进来的类字节码。该段代码参考 developerworks 站点文章java 编程的动态性,第 7 部分: 用 bcel 设计字节码。对于 bcel 项目的详细介绍,本文不再复述,请参阅bcel项目的主页。


列表7 timing 代理的完整实现

    
import java.io.ioexception;
import java.io.bytearrayoutputstream;
import java.lang.instrument.classfiletransformer;
import java.lang.instrument.illegalclassformatexception;
import java.lang.instrument.instrumentation;
import org.apache.bcel.constants;
import org.apache.bcel.classfile.classparser;
import org.apache.bcel.classfile.javaclass;
import org.apache.bcel.classfile.method;
import org.apache.bcel.generic.classgen;
import org.apache.bcel.generic.constantpoolgen;
import org.apache.bcel.generic.instructionconstants;
import org.apache.bcel.generic.instructionfactory;
import org.apache.bcel.generic.instructionlist;
import org.apache.bcel.generic.methodgen;
import org.apache.bcel.generic.objecttype;
import org.apache.bcel.generic.push;
import org.apache.bcel.generic.type;
public class timing implements classfiletransformer {
    private string methodname;
    private timing(string methodname) {
        this.methodname = methodname;
        system.out.println(methodname);
    }
    public byte[] transform(classloader loader, string classname, class cbr,
            java.security.protectiondomain pd, byte[] classfilebuffer)
            throws illegalclassformatexception {
        try {
          classparser cp = new classparser(new java.io.bytearrayinputstream(
                    classfilebuffer), classname + ".java"); 
            javaclass jclas = cp.parse();
            classgen cgen = new classgen(jclas);
            method[] methods = jclas.getmethods();
            int index;
            for (index = 0; index < methods.length; index++) {
                if (methods[index].getname().equals(methodname)) {
                    break;
                }
            }
            if (index < methods.length) {
                addtimer(cgen, methods[index]);
                bytearrayoutputstream bos = new bytearrayoutputstream();
                cgen.getjavaclass().dump(bos);
                return bos.tobytearray();
            }
            system.err.println("method " + methodname + " not found in " 
                    + classname);
            system.exit(0);
        } catch (ioexception e) {
            system.err.println(e);
            system.exit(0);
        }
        return null; // no transformation required
    }
    private static void addtimer(classgen cgen, method method) {
        // set up the construction tools
        instructionfactory ifact = new instructionfactory(cgen);
        instructionlist ilist = new instructionlist();
        constantpoolgen pgen = cgen.getconstantpool();
        string cname = cgen.getclassname();
        methodgen wrapgen = new methodgen(method, cname, pgen);
        wrapgen.setinstructionlist(ilist);
        // rename a copy of the original method
        methodgen methgen = new methodgen(method, cname, pgen);
        cgen.removemethod(method);
        string iname = methgen.getname() + "_timing";
        methgen.setname(iname);
        cgen.addmethod(methgen.getmethod());
        type result = methgen.getreturntype();
        // compute the size of the calling parameters
        type[] parameters = methgen.getargumenttypes();
        int stackindex = methgen.isstatic() ? 0 : 1;
        for (int i = 0; i < parameters.length; i++) {
            stackindex += parameters[i].getsize();
        }
        // save time prior to invocation
        ilist.append(ifact.createinvoke("java.lang.system",
            "currenttimemillis", type.long, type.no_args, 
            constants.invokestatic));
        ilist.append(instructionfactory.
            createstore(type.long, stackindex));
        // call the wrapped method
        int offset = 0;
        short invoke = constants.invokestatic;
        if (!methgen.isstatic()) {
            ilist.append(instructionfactory.
                createload(type.object, 0));
            offset = 1;
            invoke = constants.invokevirtual;
        }
        for (int i = 0; i < parameters.length; i++) {
            type type = parameters[i];
            ilist.append(instructionfactory.
                createload(type, offset));
            offset += type.getsize();
        }
        ilist.append(ifact.createinvoke(cname, 
            iname, result, parameters, invoke));
        // store result for return later
        if (result != type.void) {
            ilist.append(instructionfactory.
                createstore(result, stackindex+2));
        }
        // print time required for method call
        ilist.append(ifact.createfieldaccess("java.lang.system",
            "out",  new objecttype("java.io.printstream"),
            constants.getstatic));
        ilist.append(instructionconstants.dup);
        ilist.append(instructionconstants.dup);
        string text = "call to method " + methgen.getname() +
            " took ";
        ilist.append(new push(pgen, text));
        ilist.append(ifact.createinvoke("java.io.printstream",
            "print", type.void, new type[] { type.string },
            constants.invokevirtual));
        ilist.append(ifact.createinvoke("java.lang.system", 
            "currenttimemillis", type.long, type.no_args, 
            constants.invokestatic));
        ilist.append(instructionfactory.
            createload(type.long, stackindex));
        ilist.append(instructionconstants.lsub);
        ilist.append(ifact.createinvoke("java.io.printstream",
            "print", type.void, new type[] { type.long },
            constants.invokevirtual));
        ilist.append(new push(pgen, " ms."));
        ilist.append(ifact.createinvoke("java.io.printstream",
            "println", type.void, new type[] { type.string },
            constants.invokevirtual));
        // return result from wrapped method call
        if (result != type.void) {
            ilist.append(instructionfactory.
                createload(result, stackindex+2));
        }
        ilist.append(instructionfactory.createreturn(result));
        // finalize the constructed method
        wrapgen.stripattributes(true);
        wrapgen.setmaxstack();
        wrapgen.setmaxlocals();
        cgen.addmethod(wrapgen.getmethod());
        ilist.dispose();
    }
    public static void premain(string options, instrumentation ins) {
        if (options != null) {
            ins.addtransformer(new timing(options));
        } else {
            system.out
            .println("usage: java -javaagent:timing.jar=\"class:method\""); 
            system.exit(0);
        }
    }
}
            

通过调用 timing 代理,当运行结束之后,被检测类的字节码不会改动。函数运行时间的检测,是通过运行期间,动态的插入函数,并且改变调用序列来实现的。图3给出了使用命令行 java -javaagent:timing.jar="helloworld" sample 运行代理 timing 的结果。


列表 8 通过命令行参数调用代理

    
            java -javaagent:timing.jar="helloworld" sample


图3 运行代理 timing 的结果
timing

资源 timing.jar 文件将包含 timing 代理的源代码和类文件,以及使用说明。

扫描关注微信公众号