Java 5 特性 Instrumentation 实践

简介

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

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

使用 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 参数)。代理的各种功能一般是通过操作这一串字节码得以实现的。同时还需要提供一个公共的静态方法:

public static void premain(String agentArgs, Instrumentation inst)

一般会在这个方法中创建一个代理对象,通过参数 inst 的 addTransformer() 方法,将创建的代理对象再传递给虚拟机。这个方法是一个入口方法,有点类似于一般类的 main 方法。图1展示了代理工作的原理:
Java 5 特性 Instrumentation 实践_第1张图片
可以看到,多个代理可以同时执行。多个代理的 premain 方法将按照代理指定的顺序被依次调用
下面的代码片断,演示了 Greeting 代理的 transform 方法。在该方法中我们对 agent 的行为进行了简单的定制——输出需要该代理监测的类名。

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 代理的实例,将该实例传递给虚拟机。

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 展示了如何使用命令行调用代理:

java -javaagent:Greeting.jar=“Hello, Sample” Sample

这条命令表示,用参数”Hello, Sample”调用 Greeting 代理,以检测 Sample 类的运行情况。运行该命令之后的结果如下图:
在此输入图片描述
代理需要被打包到一个符合特定标准的 jar 文件中运行。该 jar 文件的 MANIFEST.MF 文件需要包括一些特殊的项以定义代理类等信息。(请查阅 Java 5 规约,获取详细信息)在列表 4 中,我们指定了 Greeting 代理的代理类是 Greeting.class。

Manifest-Version: 1.0
Premain-Class: Greeting

Timing 代理

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

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) 可以被动态的添加到类字节码中,得到等同于如下代码片断的字节码。

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 的强大功能,动态的修改了虚拟机传递进来的类字节码。对于 BCEL 项目的详细介绍,本文不再复述,请参阅BCEL项目的主页。

import java.lang.instrument.ClassFileTransformer;
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 的结果。
在此输入图片描述

Instrumentation API

java.lang.Instrument包是在JDK5引入的,程序员通过修改方法的字节码实现动态修改类代码。在代理类的方法中的参数中,就有Instrumentation inst实例。通过该实例,我们可以调用Instrumentation提供的各种接口。比如:

调用inst.getAllLoadedClasses()得到所有已经加载过的类;
调用inst.addTransformer(new SdlTransformer(), true)增加一个可重转换转换器;
调用inst.retransformClasses(Class cls),向jvm发起重转换请求;

Java Instrutment只提供了JVM TI中非常小的一个功能子集,一个是允许在类加载之前,修改类字节(ClassFileTransformer)(JDK5中开始提供,即使随JVM启动的Agent),另外一个是在类加载之后,触发JVM重新进行类加载(JDK6中开始提供,用于JVM启动之后通过Attach去加载Agent)。这两个功能表面看起来微不足道,但实际非常强大,AspectJ AOP的动态WeavingVisual VM的性能剖析JConsole支持Attach到进程上进行监控,都是通过这种方式来做的。除了这两个功能外,JDK 6中还提供了动态增加BootstrapClassLoader/SystemClassLoader的搜索路径、对Native方法进行instrutment。

1)ClassFileTransformer:定义了类加载前的预处理类,可以在这个类中对要加载的类的字节码做一些处理,譬如进行字节码增强
2)Instrutmentation:增强器,由JVM在入口参数中传递给我们,提供了如下的功能
addTransformer/ removeTransformer:注册/删除ClassFileTransformer
retransformClasses:对于已经加载的类重新进行转换处理,即会触发重新加载类定义,需要注意的是,新加载的类不能修改旧有的类声明,譬如不能增加属性、不能修改方法声明
redefineClasses:与如上类似,但不是重新进行转换处理,而是直接把处理结果(bytecode)直接给JVM
getAllLoadedClasses:获得当前已经加载的Class,可配合retransformClasses使用
getInitiatedClasses:获得由某个特定的ClassLoader加载的类定义
getObjectSize:获得一个对象占用的空间,包括其引用的对象
appendToBootstrapClassLoaderSearch/appendToSystemClassLoaderSearch:增加BootstrapClassLoader/SystemClassLoader的搜索路径
isNativeMethodPrefixSupported/setNativeMethodPrefix:判断JVM是否支持拦截Native Method

ClassFileTransformer

byte[] transform(ClassLoader loader,String className, Class<?> classBeingRedefined,ProtectionDomain protectionDomain, byte[] classfileBuffer)throws IllegalClassFormatException

该接口只定义个一个方法transform,该方法会在加载新class类或者重新加载class类时,调用。例如,inst.addTransformer(new SdlTransformer(), true)当代码中增加了一个可重转换转换器后,每次类加载之前,就会调用transform方法。若该方法返回null,则不改变加载的class字节码,若返回一个byte[]数组,则jvm将会用返回的byte[]数组替换掉原先应该加载的字节码。

下面将transform的官方说明贴出来:

/**
 * 参数: 
 * loader - 定义要转换的类加载器;如果是引导加载器,则为 null 
 * className - 完全限定类内部形式的类名称和 The Java Virtual Machine Specification 中定义的接口名称。例如,"java/util/List"。 
 * classBeingRedefined - 如果是被重定义或重转换触发,则为重定义或重转换的类;如果是类加载,则为 null 
 * protectionDomain - 要定义或重定义的类的保护域 
 * classfileBuffer - 类文件格式的输入字节缓冲区(不得修改) 
 * 返回: 
 * 一个格式良好的类文件缓冲区(转换的结果),如果未执行转换,则返回 null。 
 * 抛出: 
 * IllegalClassFormatException - 如果输入不表示一个格式良好的类文件 
 */
byte[] transform(ClassLoader loader, 
                 String className, 
                 Class<?> classBeingRedefined, 
                 ProtectionDomain protectionDomain, 
                 byte[] classfileBuffer) 
                 throws IllegalClassFormatException

此方法的实现可以转换提供的类文件,并返回一个新的替换类文件。
有两种装换器,由 Instrumentation.addTransformer(ClassFileTransformer,boolean) 的 canRetransform 参数确定:

可重转换 转换器,将 canRetransform 设为 true 可添加这种转换器;
不可重转换转换器,将 canRetransform 设为 false 或者使用 Instrumentation.addTransformer(ClassFileTransformer) 可添加这种转换器;

在转换器使用 addTransformer 注册之后,每次定义新类和重定义类时都将调用该转换器。每次重转换类时还将调用可重转换转换器对新类定义的请求通过 ClassLoader.defineClass 或其本机等价方法进行对类重定义的请求通过 Instrumentation.redefineClasses 或其本机等价方法进行对类重转换的请求将通过 Instrumentation.retransformClasses 或其本机等价方法进行。转换器是在验证或应用类文件字节之前的请求处理过程中调用的。 当存在多个转换器时,转换将由 transform 调用链组成。也就是说,一个 transform 调用返回的 byte 数组将成为下一个调用的输入(通过 classfileBuffer 参数)

转换将按以下顺序应用:
不可重转换转换器
不可重转换本机转换器
可重转换转换器
可重转换本机转换器
对于重转换,不会调用不可重转换转换器,而是重用前一个转换的结果。对于所有其他情况,调用此方法。在每个这种调用组中,转换器将按照注册的顺序调用。本机转换器由 Java 虚拟机 Tool 接口中的 ClassFileLoadHook 事件提供。

第一个转换器的输入(通过 classfileBuffer 参数)如下:
对于新的类定义,是传递给 ClassLoader.defineClass 的 byte
对于类重定义,是 definitions.getDefinitionClassFile(),其中 definitions 是 Instrumentation.redefineClasses 的参数
对于类重转换,是传递给新类定义的 byte,或者是最后一个重定义(如果有重定义),所有不可转换转换器进行的转换都将自动重新应用并保持不变;有关细节,请参阅 Instrumentation.retransformClasses

如果实现方法确定不需要进行转换,则应返回 null。否则,它将创建一个新的 byte[] 数组,将输入 classfileBuffer 连同所有需要的转换复制到其中,并返回这个新数组。不得修改输入 classfileBuffer。

在重转换和重定义中,转换器必须支持重定义语义:如果转换器在初始定义期间更改的类在以后要重转换或重定义,那么转换器必须确保第二个输出类文件是第一个输出类文件的合法重定义文件。

如果转换器抛出异常(未捕获的异常),后续转换器仍然将被调用并加载,仍然将尝试重定义或重转换。因此,抛出异常与返回 null 的效果相同。若要使用转换器代码在生成未检验异常时防止不希望发生的行为,可以让转换器捕获 Throwable。如果转换器认为 classFileBuffer 不表示一个有效格式的类文件,则将抛出 IllegalClassFormatException;尽管这与返回 null 的效果相同,但它便于对格式毁坏进行记录或调试。

你可能感兴趣的:(Java 5 特性 Instrumentation 实践)