跳转到内容

Java 5:Instrumentation实践

原文日期:2006年8月31日
原文地址:https://www.ibm.com/developerworks/cn/java/j-lo-instrumentation/index.html
转载备注:调整了少量格式

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

public static void premain(String agentArgs, Instrumentation inst)

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

Image

可以看到,多个代理可以同时执行。这多个代理的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的结果
I've been called with options: "Hello, Sample"
Hello, Sample

代理需要被打包到一个符合特定标准的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的结果
Call to method helloWorld_timing took 219 ms.

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

下载资源

相关主题