Java5之后,增加了一个包java.lang.instrument,这个包的东西很少,两个接口,ClassFileTransformer和Instrumentation,一个类ClassDefinition,还有两个Exception:IllegalClassFormatException和UnmodifiableClassException;
先剧透一下整个包最重要的接口Instrumentation提供的两个功能,总体的功能就是Instrument了,具体来讲两个,从字节码级别为类增加Instrument所需的代码,第二个是获取对象的大小。
先看一下这个包的描述吧,下面是JavaDoc的翻译:
这个包提供允许一个Java程序语言的代理测量运行在JVM中的程序的多个指标的服务。
这个包提供允许一个Java程序语言的代理测量运行在JVM中的程序的多个指标的服务。监测的机制是修改类的方法的字节码。
一个代理就是部署一个Jar文件。Jar文件的manifest文件(每个jar包根目录下有个META-INF文件夹,此文件夹下的MANIFEST.MF文件就是这里的manifest文件)需要指定这个代理的类,指定之后,这个类将被加载以启动代理。因为支持命令行接口,所以一个代理可以通过在命令行中指定一个选项来移动。具体的实现也可能支持在虚拟机启动之后的某个时间开启代理。例如,一个具体的实现可能提供一个机制,使一个工具附加到一个运行的应用中,然后,初始化这个工具的代理,加载到正在运行的应用中。关于类的加载如何被初始化的细节,依赖于具体的实现。
在用命令行接口的实现中,一个代理可以通过增加下面的Option到命令行中启动:
-javaagent:jarpath[=options]
Jarpath是代理jar文件的路径。Option是代理的选项。上边的命令可以在同一个命令行中使用多次,因此,可以创建多个代理。多个代理可以使用同一jarpath。一个代理Jar文件必须符合Jar文件规范。
一个代理jar文件的manifest必须包含Premain-Class属性。这个属性的值是代理类的名字。代理类必须实现一个public static的premain
方法,与应用程序中的main方法作为程序入口的原理类似。JVM初始化之后,每一个premain方法将被以代理被指定时的顺序调用,然后应用程序真正的main方法将被调用。每一个premain方法必须以启动的顺序返回结果。(premain=pre-main)
Premain方法有两个重载的方法。JVM首先尝试调用代理类中下面的方法:
public static voidpremain(String agentArgs, Instrumentation inst);
如果代理类没有实现这个方法,jvm将尝试调用
public static voidpremain(String agentArgs);
代理类可能还有一个agentmain方法,当代理类在JVM启动之后被启动时使用。当代理类使用命令行选项被启动时,agentmain方法不被调用。
Agent类通过系统的类加载器加载(ClassLoader.getSystemClassLoader
)。这个类加载器通常跟用来加载包含应用程序的main方法的类加载器是一个。Premain方法将运行在跟应用程序main方法同样的安全机制和类加载下。代理的premain方法做什么是没有限制的。任何应用程序main方法可以做的,包括创建线程,在premain中都是合法的。
每一个代理经由agentArgs参数传给它的代理选项。代理选项以一个字符串被被传递,任何附加的解析操作应该是代理自己执行。
如果代理不能被解析(例如,因为代理类不能加载,或者因为代理类没有一个合适的premain方法),JVM将会abort。如果一个premain方法跑出一个未捕获的错误,JVM将会abort。
一个代理具体的实现可能提供一个在JVM启动之后启动的机制。关于代理被初始化的细节是具体实现特定的,但是通常应用程序已经启动,应用程序的main方法已经被调用。在代理的实现支持在JVM启动之后启动的情况下,需要遵循下面的规则:
1. 代理的Jar文件中的manifest文件必须包含Agent-Class属性。这个属性的值是代理类的名字。
2. 代理类必须实现一个public static agentmain方法
3. 系统类加载器( ClassLoader.getSystemClassLoader
)必须支持增加一个代理Jar文件到系统类路径。
代理的Jar文件被追加到系统类路径中。这个类加载器是通常用来加载包含应用程序main方法的类的类加载器。代理类被加载之后,JVM尝试调用agentmain方法。JVM首先尝试调用代理类中的下面这个方法:
public static voidagentmain(String agentArgs, Instrumentation inst);
如果代理类中没有实现这个方法,然后JVM尝试调用:
public static voidagentmain(String agentArgs);
这个代理类可能也有一个premain方法,当代理在命令行选项中启动时使用;当这个代理类在JVM启动之后启动时,premain方法不会被调用。
代理通过agentArgs参数传给代理选项。代理选项以字符串的形式传给代理内部的方法,任何附加的解析操作应该代理自己操作。
Agentmain方法应该做一些启动代理之前必要的初始化操作。当启动完成时,这个方法应该返回。如果代理不能被启动(例如,因为代理类不能被加载,或者因为代理类不包含一致的agentmain方法),JVM将不会被abort。如果agentmain方法抛出一个未捕获的错误,这个错误将被忽略。
注:著名的Java运行时分析工具Profiler有在监测Java程序时有两种模式,一种是用Profiler启动应用,一种是应用启动,然后附加到Profiler中,就是分别用的上边的两种方式。
下面的manifest属性在一个代理的jar文件中被定义:
Premain-Class
当一个代理在JVM启动时被指定时,这个属性指定代理使用的类。也就是,这个类包含premain方法。当一个代理在JVM启动时被指定时,这个属性是必须的。如果没有配置这个属性,JVM将会abort。注意:这是一个雷的名字,不是一个文件名活路径。
Agent-Class
如果一个代理的实现支持在JVM启动之后的某个时间启动,这个属性指定一个代理类。也就是说,这个类要包含agentmain方法。这个属性是必须的,如果不配置的话,代理将不会被启动。注意:这是一个类的名字,不是一个文件名字,也不是文件路径。
Boot-Class-Path
这个代理的实现中用到的第三方jar包都放到这里边。
Can-Redefine-Classes
Boolean(true或false)。Agent是否可以重定义类。可选的,默认是false。
Can-Retransform-Classes
Boolean(true或false)。Agent是否可以将类变回原形。可选的,默认是false。
Can-Set-Native-Method-Prefix
Boolean(true或false)。Agent是否可以设置本地方法的前缀。可选的,默认是false。
一个代理Jar文件中的manifest文件可能同时包含Premain-Class和Agent-Class属性。当代理在命令行中用-javaagent选项被启动,使用Premain-Class属性指定的代理类的名字,然后Agent-Class属性的配置被忽略。相似地,如果代理在JVM启动之后的某个时间启动,使用Agent-Class属性指定的类的名字,Premain-Class属性被忽略。
下面再看一下Instrumentation这个最主要的接口的一些介绍。
这是一个接口,提供了监测java程序语言代码的服务。Instrumentation将附加的功能的字节码添加到被监控的方法中,来收集应用的运行的一些数据。因为这个改变只有增加东西,监测工具可以不用修改应用的状态和表现。这样的工具包括监控代理,分析工具,代码覆盖分析器和事件日志器等。
有两种方式获得一个Instrumentation接口的实例:
1. 当JVM启动时,指定一个代理类。在这种情况下,一个Instrumentation实例传到代理类的premain方法中。
2. JVM提供可以让代理类在JVM启动后启动的机制。在这种情况下,一个Instrumentation实例传给代理的agentmain方法。
这两种机制在上边的package specification中有详尽的描述。
一旦一个代理获取到一个Instrumentation实例,代理可以再之后的任何时刻调用实例的方法。
主要的方法就是下面几个:
addTransformer:指定一个ClassFileTransformer对象。可以先看下下面ClassFileTransformer接口的介绍。
getAllLoadedClasses:返回当前JVM中加载的所有的类的数组(牛逼)
getInitiatedClasses:返回指定的类加载器中的所有的类的数据(牛逼,too)
getObjectSize:返回一个对象近似的大小
redefineClasses:用给定的类的字节码数组替换指定的类的字节码文件,也就是重新定义指定的类
retransformClasses:指定一系列的Class对象,被指定的类都会重新变回去(去掉附加的字节码)
剩下的几个都是对上边manifest文件中配置的属性的一下操作和运行时接口。
到这,看下Instrumentation接口的主要功能,从Instrumentation接口提供的这几个方法,可以看出Instrumentation的主要功能,就是修改类的字节码文件,更确切的说是修改类的字节码文件,增加自定义的功能的字节码,生成一个新的字节码文件。这是这个接口存在的最主要的意义。
先看下Doc上的简介,一个代理提供一个为了改变类文件而存在的接口的实现。这个改变发生在这个类被JVM定义之前。
这个接口只有一个方法transform,将类的字节码传给这个方法,然后返回一个增加了自定义的功能的字节码的字节数组。
这个接口存在的意义,主要是解耦Instrumentation类和改变类文件的操作。
这个类只在Instrumentation#redefineClasses方法中被调用,这个类有两个域,
Class mClass;
Byte[] mClassFile;
mClass是要重新定义的类的Class对象
mClassFile是新的类的字节码数组
下面举一个简单的例子,来感受一下,来自网络,功能是打印每一个方法的执行时间。
packagecom.highgo.test.instrumentation;
importjava.lang.instrument.ClassFileTransformer;
importjava.lang.instrument.Instrumentation;
public class PerfMonAgent {
private static Instrumentation inst = null;
public static void premain(String agentArgs, Instrumentation_inst) {
System.out.println("PerfMonAgent.premain()was called. I am premain!");
// Initialize the static variables we use to trackinformation.
inst = _inst;
// Set up the class-file transformer.
ClassFileTransformer trans = new PerfMonXformer();
System.out.println("Adding a PerfMonXformerinstance to the JVM.");
inst.addTransformer(trans);
}
public static void agentmain(String agentArgs,Instrumentation _inst) {
System.out.println("PerfMonAgent.premain()was called. I am agentmain!");
// Initialize the static variables we use to trackinformation.
inst = _inst;
// Set up the class-file transformer.
ClassFileTransformer trans = new PerfMonXformer();
System.out.println("Adding a PerfMonXformerinstance to the JVM.");
inst.addTransformer(trans);
}
}
packagecom.highgo.test.instrumentation;
importjava.lang.instrument.ClassFileTransformer;
importjava.lang.instrument.IllegalClassFormatException;
importjava.security.ProtectionDomain;
importjavassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtBehavior;
import javassist.CtClass;
importjavassist.NotFoundException;
importjavassist.expr.ExprEditor;
importjavassist.expr.MethodCall;
public class PerfMonXformerimplements ClassFileTransformer {
public byte[] transform(ClassLoader loader, StringclassName, Class> classBeingRedefined,
ProtectionDomain protectionDomain, byte[]classfileBuffer) throws IllegalClassFormatException {
byte[] transformed = null;
System.out.println("Transforming " +className);
ClassPool pool = ClassPool.getDefault();
CtClass cl = null;
try {
cl = pool.makeClass(newjava.io.ByteArrayInputStream(classfileBuffer));
if (cl.isInterface() == false) {
CtBehavior[] methods =cl.getDeclaredBehaviors();
for(int i = 0; i < methods.length; i++) {
if(methods[i].isEmpty() == false) {
doMethod(methods[i]);
}
}
transformed = cl.toBytecode();
}
} catch (Exception e) {
System.err.println("Could notinstrument " + className + ", exception : " + e.getMessage());
} finally {
if (cl != null) {
cl.detach();
}
}
return transformed;
}
private void doMethod(CtBehavior method) throwsNotFoundException, CannotCompileException {
// method.insertBefore("long stime =System.nanoTime();");
//method.insertAfter("System.out.println(\"leave"+method.getName()+" andtime:\"+(System.nanoTime()-stime));");
method.instrument(new ExprEditor() {
public void edit(MethodCall m) throwsCannotCompileException {
m.replace("{ long stime =System.nanoTime(); $_ = $proceed($$); System.out.println(\""
+m.getClassName() + "." + m.getMethodName() +":\"+(System.nanoTime()-stime));}");
}
});
}
}
Manifest-Version: 1.0
Premain-Class:com.highgo.test.instrumentation.PerfMonAgent
Boot-Class-Path: javassist.jar
Boot-Class-Path这样写要将javassist.jar文件放到jar包的根目录
jar cvfm inst.jar MANIFEST.MFjavassist.jar com
使用自己写的MANIFEST.MF文件,
将javassist.jar和com文件夹下的类打成jar包
这样这个jar包就作为一个提供打印方法时间的服务而存在了
随便编写一个类
public class App {
public static void main(String[] args) {
new App().test();
}
public void test() {
System.out.println("HelloWorld!!");
}
}
运行此类
java -javaagent:../inst.jarclasspath .;../inst.jar App
可以看到,将运行这个类的过程中用到的所有的类的方法的方法名+时间都打印出来了。
这里使用第二种方式,也就是先启动JVM,再启动agent类的方式,做一个例子。
在使用JProfiler的quick attch时,我们的使用方法是这样的:运行我们的应用程序,切换到这个界面,我们的应用就会出现在下面这个地方,ID是OS分配给这个进程的进程ID,后边是进程的名字,其实attch用到的只是进程ID号;选中这个进程,就到了分析界面了。
我们下面的例子也是按照这个顺序:
1. 编写Agent,打包成服务
2. 编写一个应用程序,运行
3. 编写一个工具,将Agent的jar包attach到Agent所在的JVM(具体实现是将应用程序的ID传递给工具)
packagecom.highgo.test.instrumentation;
importjava.lang.instrument.ClassFileTransformer;
importjava.lang.instrument.Instrumentation;
public classPerfMonAgent {
private static Instrumentation inst = null;
public static void premain(String agentArgs,Instrumentation _inst) {
System.out.println("PerfMonAgent.premain()was called. I am premain!");
// Initialize the static variables we useto track information.
inst = _inst;
// Set up the class-file transformer.
ClassFileTransformer trans = newPerfMonXformer();
System.out.println("Adding aPerfMonXformer instance to the JVM.");
inst.addTransformer(trans);
}
public static void agentmain(StringagentArgs, Instrumentation _inst) {
System.out.println("PerfMonAgent.premain()was called. I am agentmain!");
inst = _inst;
Class>[] classes =inst.getAllLoadedClasses();
for (Class> cls : classes) {
System.out.println(cls.getName());
}
System.out.println("classeslength:"+classes.length);
System.out.println("PerfMonAgent.premain()end!");
}
这里把第二种方式用到的agentmain方法直接加到第一种方式使用的类中了。
Manifest-Version: 1.0
Premain-Class:com.highgo.test.instrumentation.PerfMonAgent
Agent-Class:com.highgo.test.instrumentation.PerfMonAgent
Boot-Class-Path: javassist.jar
只是增加了Agent-Class的配置。
jar cvfm inst.jar MANIFEST.MFjavassist.jar com
还是上边用到的命令
public class TargetVM {
public static void main(String[] args) throws InterruptedException {
while (true) {
Thread.sleep(1000);
}
}
}
就一个线程,一直运行着。
用jps命令可以方便的查看进程的ID,名字是应用程序main方法所在的类的名字。可以很容易的分辨出来。
public class AttachUtil {
public static void main(String[] args) throws AttachNotSupportedException, IOException, AgentLoadException,AgentInitializationException {
String id = "520384";
VirtualMachine vm = VirtualMachine.attach(id);
vm.loadAgent("inst.jar");
}
}
这里用到了JDK的tools.jar工具包,不了解这个Jar包的朋友可以再本博客搜一下关于这个jar包的详细介绍。Eclipse默认加的jar包没有这个,可以从jdk的安装目录里找到,加到项目的sspath里就行了;如果是用javac去编译的话,只要将这个类加入到系统的classpath里就可以了,这个工具类就也可以编译,也可以运行了。
将ID修改成jps查看到的那个ID号,这个工具类就可以创建一个ID号所在的进程的VirtualMachine对象,这个对象就代表那个JVM。VirtualMachine#loadAgent方法可以加载一个Agent。
当AttachUtil方法运行到vm.loadAgent("inst.jar");这行代码的时候,就可以看到Eclipse的控制台会打印出PerfMonAgent# agentmain方法执行的打印内容,应用程序所在的JVM中加载的所有的类的名字。
从上边Instrumentation的method summary可以看出,Instrumentation接口的另一个功能就是获取一个对象的大小。
这里我们直接分析一下classmexer的源码
Classmexer的Agent使用的是第一种方式,所以看他的MANIFEST.MF文件如下:
Manifest-Version: 1.0
Created-By: 1.6.0 (SunMicrosystems Inc.)
Premain-Class:com.javamex.classmexer.Agent
这个项目只有两个类,Agent和MemoryUtil
Agent的主要目的是获取一个Instrumentation的实例。内容如下:
packagecom.javamex.classmexer;
import java.lang.instrument.Instrumentation;
public class Agent
{
private static volatile Instrumentationinstrumentation;
public static void premain(String args,Instrumentation instr)
{
instrumentation = instr;
}
protected static Instrumentation getInstrumentation()
{
Instrumentation instr = instrumentation;
if (instr == null) {
throw newIllegalStateException("Agent not initted");
}
return instr;
}
}
packagecom.javamex.classmexer;
importjava.lang.instrument.Instrumentation;
importjava.lang.reflect.Field;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.Stack;
public class MemoryUtil
{
public static enum VisibilityFilter
{
ALL, PRIVATE_ONLY, NON_PUBLIC, NONE;
}
public static long memoryUsageOf(Object obj)
{
returnAgent.getInstrumentation().getObjectSize(obj);
}
public static long deepMemoryUsageOf(Objectobj)
{
return deepMemoryUsageOf(obj,VisibilityFilter.NON_PUBLIC);
}
public static long deepMemoryUsageOf(Objectobj, VisibilityFilter referenceFilter)
{
returndeepMemoryUsageOf0(Agent.getInstrumentation(), new HashSet(), obj,referenceFilter);
}
public static longdeepMemoryUsageOfAll(Collection extends Object> objs)
{
return deepMemoryUsageOfAll(objs,VisibilityFilter.NON_PUBLIC);
}
public static longdeepMemoryUsageOfAll(Collection extends Object> objs, VisibilityFilterreferenceFilter)
{
Instrumentation instr =Agent.getInstrumentation();
long total = 0L;
Set counted = newHashSet(objs.size() * 4);
for (Object o : objs) {
total += deepMemoryUsageOf0(instr,counted, o, referenceFilter);
}
return total;
}
private static longdeepMemoryUsageOf0(Instrumentation instrumentation, Set counted,Object obj, VisibilityFilter filter)
throws SecurityException
{
Stack
可以看出,最主要的方法是deepMemoryUsageOf0(Instrumentation instrumentation,Set
这个方法的主要的工作是对一个对象的域进行深度遍历,获取最底层的每个对象的大小,然后加起来。
到这里,Instrumentation的所有的功能就完事了,其实就是两个,一个获取对象的大小,一个是作为一个服务,在JVM中的类被定义之前修改类的字节码,跟Spring的AOP不仅形似而且神似。
上边的两个Example的代码分别来自于:
http://blog.csdn.net/ykdsg/article/details/12080071
http://blog.csdn.net/pwlazy/article/details/5109742