构建自己的监测工具

不论是使用 System.out.println() 还是 hprof 或 OptimizeIt 这样的监测工具,代码监测都应当是软件开发实践的关键部分。这篇文章讨论了代码监测最常见的方式,并解释了它们的不足。本文提供了对于理想的内部监测器来说最合适的特性,并解释了为什么面向方面编程技术非常适于实现其中的一些特性。本文还介绍了 JDK 5.0 代理接口,并详细介绍了用它构建自己的面向方面的监测器的步骤。

请注意,这篇文章的示例监测器和 完整源代码 基于 Java 交互监测器(JIP)—— 一个用面向方面技术和 Java 5 代理接口构建的开放源码监测器。请参阅 参考资料 学习关于 JIP 和本文中讨论的其他工具的更多内容。

监测工具和技术

多数 Java 开发人员都是从使用 System.currentTimeMillis() 和 System.out.println() 开始测量应用程序性能的。System.currentTimeMillis() 易于使用:只要测量方法开始和结束的时间,并输出时间差即可,但是它有两个重大不足:

  • 它是个手工过程,要求开发人员确定要测量哪个代码;插入工具代码;重新编译、重新部署、运行并分析结果;然后在结束时取消工具代码;而在下次出现问题时再次重复以上所有步骤。

  • 而且它对于应用程序各部分的执行情况没有提供全面的观察。

为了解决这些问题,有些开发人员转向 hprof、JProbe 或 OptimizeIt 这样的监测器。监测器避免了与即时测量相关联的问题,因为不必修改程序就可以使用它们。它们还为程序性能提供了更全面的观察,因为它们收集每个方法调用的计时信息,而不仅仅是某个具体代码段的计时信息。不幸的是,监测工具也有不足。

监测器的局限

监测器对于 System.currentTimeMillis() 这样的手工解决方案提供了很好的替代,但是它们还远谈不上理想。有一件事,就是用hprof 运行程序,会把程序减慢 20 倍。这意味着正常情况下只需要一小时的一个 EFL(提取、转换、装入)操作,可能要花一整天才能监测!不仅等候是不方便的,而且应用程序时间范围的改变,实际上也会扭曲结果。以做许多 I/O 操作的程序为例。因为 I/O 由操作系统执行,监测不会减慢它,所以 I/O 操作看起来运行得要比实际的速度快 20 倍!所以,不能总是依靠 hprof 提供对应用程序性能的正确描述。

hprof 的另一个问题与 Java 程序装入和运行的方式有关。与 C 或 C++ 这样的静态链接语言不同,Java 程序是在运行时而不是在编译时链接的。直到第一次引用的时候,JVM 才装入类,而代码直到执行了许多次之后,才从字节码编译成机器码。如果想测量一个方法的性能,但是它的类还没有装入,那么测量就会包含类的装入时间和编译时间再加上运行时间。因为这些事只在应用程序生命开始的时候发生一次,所以如果要测量长期的应用程序性能,通常不想把这些事包含在内。

当代码在应用服务器或 servlet 引擎中运行的时候,事情会变得更加复杂。hprof 这样的监测器会监测整个应用程序、servlet 容器和所有的东西。问题是,通常不 监测 servlet 引擎,只想监测应用程序。

理想的监测器

像选择其他工具一样,选择监测器也有机会成本。hprof 易于使用,但有局限性,例如不能从监测中过滤掉类或包。商业工具提供了更多特性,但是昂贵而且有严格的许可条款。有些监测器要求通过监测器启动应用程序,这意味着要用不熟悉的工具重新构建执行环境。监测器的选择涉及妥协,所以理想的监测器看起来应当像什么呢?下面是应当追寻的特性的一个简短列表:

  • 速度:监测可能会慢得让人痛苦。但是可以使用不自动监测每个类的监测器,以便加快速度。 

  • 交互性:监测器允许的交互越多,对监测器得到的信息进行的精细调整就越多。例如,能够在运行时开启和关闭监测器,有助于避免测量类的装入、编译和解释执行(预 JIT)时间。

  • 过滤:根据类或包进行过滤,可以把注意力集中在手头的问题上,而不会被太多的信息扰乱。

  • 100% 纯 Java 代码:多数监测器都要求使用本机库,这限制了可以使用它们的平台。理想的监测器不应当要求使用本机库。

  • 开放源码:开放源码工具通常允许迅速地起步和运行,同时避免了商业许可的限制。

自己构建监测器!

用 System.currentTimeMillis() 生成计时信息的问题是它是一个手工过程。如果能够自动插入工具代码,那么它的许多不足就烟消云散了。这类问题正是面向方面解决方案最适合解决的问题。对于构建面向方面的监测器来说,Java 5 引入的代理接口非常理想,因为它提供了挂接到类装入器和在类装入时修改类的方便途径。

本文的剩余部分集中在 BYOP (构建自己的监测器)上。我将介绍代理接口,并演示如何创建简单代理。将学习基本监测方面的代码,以及为了更高级的监测对它进行修改所采取的步骤。

创建代理

不幸的是,-javaagent 这个 JVM 选项的文档只有零星记载。找不到太多关于这个主题的书(没有 Java 代理傻瓜书 或 21 天学会 Java 代理),但是可以在 参考资料 一节中发现一些好的资源,还有这里的概述。

代理背后的想法是:在 JVM 装入类时,代理可以修改类的字节码。可以用三个步骤创建代理:

  1. 实现 java.lang.instrument.ClassFileTransformer 接口: 

    public interface ClassFileTransformer {
    public byte[] transform(ClassLoader loader, String className, 
            Class<?> classBeingRedefined, ProtectionDomain protectionDomain, 
            byte[] classfileBuffer) throws IllegalClassFormatException;
    			
    }
    	



  2. 创建 “premain” 方法。这个方法在应用程序的 main() 方法之前调用,看起来像这样:

    package sample.verboseclass;
    			
    public class Main {
        public static void premain(String args, Instrumentation inst) {
            ...
        }
    }
    	



  3. 在代理的 JAR 文件中,包含一个清单条目,表示包含 premain() 方法的类: 

    Manifest-Version: 1.0
    Premain-Class: sample.verboseclass.Main
            

一个简单的代理

构建监测器的第一步是创建一个代理,在装入每个类的时候输出类的名称,与 -verbose:class JVM 选项的功能类似。如清单 1 所示,这只要求几行代码:


清单 1. 一个简单的代理
package sample.verboseclass;
public class Main {
    public static void premain(String args, Instrumentation inst) {
        inst.addTransformer(new Transformer());
    }
}
class Transformer implements ClassFileTransformer {
	
    public byte[] transform(ClassLoader l, String className, Class<?> c,
            ProtectionDomain pd, byte[] b) throws IllegalClassFormatException {
        System.out.print("Loading class: ");
        System.out.println(className);
        return b;
    }
}

如果代理被打包在叫作 vc.jar 的 JAR 文件中,就应当用 -javaagent 选项启动 JVM,如下所示:

java -javaagent:vc.jar MyApplicationClass

监测方面

有了代理的基本元素之后,下一步就是在装入应用程序的类时向其中添加简单的监测方面。幸运的是,不需要掌握修改字节码的 JVM 指令集的细节。相反,可以用 ASM 库这样的工具包(来自 ObjectWeb 论坛,请参阅 参考资料)来处理类文件格式的细节。ASM 是个 Java 字节码操纵框架,使用访客模式实现对类文件的转换,使用的方式非常像使用 SAX 事件遍历和转换 XML 文档那样。

清单 2 中的监测方面可以用来输出类名称、方法名称和 JVM 每次进入或离开一个方法的时间戳。(对于更复杂的监测器,可能还想使用精度更高的计时器,像 Java 5 的 System.nanoTime()。)


清单 2. 简单的监测方面
package sample.profiler;
public class Profile {
    public static void start(String className, String methodName) {
        System.out.println(new StringBuilder(className)
            .append('\t')
            .append(methodName)
            .append("\tstart\t")
            .append(System.currentTimeMillis()));
	}
    public static void end(String className, String methodName) {
        System.out.println(new StringBuilder(className)
            .append('\t')
            .append(methodName)
            .append("\end\t")
            .append(System.currentTimeMillis()));
    }
}

如果手工进行监测,那么下一步可能是把每个方法修改成像下面这样:

void myMethod() {
    Profile.start("MyClass", "myMethod"); 
    ...
    Profile.end("MyClass", "myMethod");
}

使用 ASM 插件

现在需要找出 Profile.start() 和 Profile.end() 调用的字节码是什么样的 —— 这正是 ASM 库发挥作用的地方。ASM 有一个用于 Eclipse 的 Bytecode Outline 插件(请参阅 参考资料),它允许查看类或方法的字节码。图 1 显示了以上方法的字节码。(也可以使用 javap 这样的反汇编器,它是 JDK 的一部分。)


图 1. 用 ASM 插件查看字节码
用 ASM 插件查看字节码  

ASM 插件甚至还生成了能够用来生成对应字节码的 ASM 代码,如图 2 所示:


图 2. ASM 插件生成的代码
ASM 插件生成的代码  

可以把图 2 中高亮的代码复制到代理中,调用 Profile.start() 方法的通用化版本,如清单 3 所示:


清单 3. 插入对监测器的调用的 ASM 代码
visitLdcInsn(className);
visitLdcInsn(methodName);
visitMethodInsn(INVOKESTATIC, 
	"sample/profiler/Profile", 
	"start", 
	"(Ljava/lang/String;Ljava/lang/String;)V");

为了插入开始和结束调用,请继承 ASM 的 MethodAdapter,如清单 4 所示:


清单 4. 插入对监测器的调用的 ASM 代码
package sample.profiler;
import org.objectweb.asm.MethodAdapter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
public class PerfMethodAdapter extends MethodAdapter {
    private String className, methodName;
	
    public PerfMethodAdapter(MethodVisitor visitor, String className, 
            String methodName) { 
        super(visitor);
        className = className;
        methodName = methodName;
    }
    public void visitCode() {
        this.visitLdcInsn(className);
        this.visitLdcInsn(methodName);
        this.visitMethodInsn(INVOKESTATIC, 
            "sample/profiler/Profile", 
            "start", 
            "(Ljava/lang/String;Ljava/lang/String;)V");
        super.visitCode();
    }
    public void visitInsn(int inst) {
        switch (inst) {
        case Opcodes.ARETURN:
        case Opcodes.DRETURN:
        case Opcodes.FRETURN:
        case Opcodes.IRETURN:
        case Opcodes.LRETURN:
        case Opcodes.RETURN:
        case Opcodes.ATHROW:
            this.visitLdcInsn(className);
            this.visitLdcInsn(methodName);
            this.visitMethodInsn(INVOKESTATIC, 
                "sample/profiler/Profile", 
                "end", 
                "(Ljava/lang/String;Ljava/lang/String;)V");
            break;
        default:
            break;
        }
		
        super.visitInsn(inst);
    }
}

把这个功能挂接到代理的代码非常简单,也是这篇文章的 源代码下载 的一部分。

装入 ASM 类

因为代理使用 ASM,所以需要确保装入了 ASM 类,所有东西才能工作。在 Java 应用程序中有许多类路径:应用程序类路径、扩展类路径和启动类路径。令人惊讶的是,ASM JAR 没有采用其中任何一个路径;相反,要使用清单告诉 JVM 代理需要哪个 JAR 文件,如清单 5 所示。在这种情况下,JAR 文件必须与代理的 JAR 放在同一目录中。


清单 5. 监测器的清单文件
Manifest-Version: 1.0
Premain-Class: sample.profiler.Main
Boot-Class-Path: asm-2.0.jar asm-attrs-2.0.jar asm-commons-2.0.jar

运行监测器

所有东西都编译打包之后,就可以对任何 Java 应用程序运行监测器了。清单 6 中的部分输出来自对 Ant 的监测,这个 Ant 执行 build.xml 对代理进行编译:


清单 6. 监测器的输出示例
org/apache/tools/ant/Main                       runBuild        start   1138565072002
org/apache/tools/ant/Project                    <init>    start   1138565072029
org/apache/tools/ant/Project$AntRefTable        <init>    start   1138565072031
org/apache/tools/ant/Project$AntRefTable        <init>    end     1138565072033
org/apache/tools/ant/types/FilterSet            <init>    start   1138565072054
org/apache/tools/ant/types/DataType             <init>    start   1138565072055
org/apache/tools/ant/ProjectComponent           <init>    start   1138565072055
org/apache/tools/ant/ProjectComponent           <init>    end     1138565072055
org/apache/tools/ant/types/DataType             <init>    end     1138565072055
org/apache/tools/ant/types/FilterSet            <init>    end     1138565072055
org/apache/tools/ant/ProjectComponent           setProject      start   1138565072055
org/apache/tools/ant/ProjectComponent           setProject      end     1138565072055
org/apache/tools/ant/types/FilterSetCollection  <init>   start   1138565072057
org/apache/tools/ant/types/FilterSetCollection  addFilterSet   start   1138565072057
org/apache/tools/ant/types/FilterSetCollection  addFilterSet   end     1138565072057
org/apache/tools/ant/types/FilterSetCollection  <init>   end     1138565072057
org/apache/tools/ant/util/FileUtils             <clinit> start   1138565072075
org/apache/tools/ant/util/FileUtils             <clinit> end     1138565072076
org/apache/tools/ant/util/FileUtils             newFileUtils   start   1138565072076
org/apache/tools/ant/util/FileUtils             <init>   start   1138565072076
org/apache/tools/ant/taskdefs/condition/Os      <clinit> start   1138565072080
org/apache/tools/ant/taskdefs/condition/Os      <clinit> end     1138565072081
org/apache/tools/ant/taskdefs/condition/Os      isFamily       start   1138565072082
org/apache/tools/ant/taskdefs/condition/Os      isOs           start   1138565072082
org/apache/tools/ant/taskdefs/condition/Os      isOs           end     1138565072082
org/apache/tools/ant/taskdefs/condition/Os      isFamily       end     1138565072082
org/apache/tools/ant/util/FileUtils             <init>   end     1138565072082
org/apache/tools/ant/util/FileUtils             newFileUtils   end     1138565072082
org/apache/tools/ant/input/DefaultInputHandler  <init>   start   1138565072084
org/apache/tools/ant/input/DefaultInputHandler  <init>   end     1138565072085
org/apache/tools/ant/Project                    <init>   end     1138565072085
org/apache/tools/ant/Project                    setCoreLoader  start   1138565072085
org/apache/tools/ant/Project                    setCoreLoader  end     1138565072085
org/apache/tools/ant/Main                       addBuildListener start 1138565072085
org/apache/tools/ant/Main                       createLogger   start   1138565072085
org/apache/tools/ant/DefaultLogger              <clinit> start   1138565072092
org/apache/tools/ant/util/StringUtils           <clinit> start   1138565072096
org/apache/tools/ant/util/StringUtils           <clinit> end     1138565072096

跟踪调用堆栈

迄今为止,已经看到了如何只用几行代码就构建了一个简单的面向方面的监测器。虽然是个好的开始,但是示例监测器没有收集线程和调用堆栈数据。调用堆栈信息对于判断方法的毛执行时间和净执行时间是必需的。另外,每个调用堆栈都与一个线程相关,所以如果想跟踪调用堆栈数据,也需要线程信息。多数监测器使用两趟式设计进行这类分析:首先收集数据,然后分析数据。我将介绍如何采用这种技术,而不是在收集数据的时候输出数据。

修改监测类

可以很容易地增强 Profile 类,让它捕获堆栈和线程信息。对于初学者来说,不用在每个方法调用的开始和结束时都输出时间信息,可以用图 3 所示的数据结构保存这些信息:


图 3. 跟踪调用堆栈和线程信息的数据结构
调用堆栈  

有许多方法可以收集关于调用堆栈的信息。其中之一是实例化一个 Exception,但是如果在每个方法的开始和结束时都做这件事,就太慢了。更简单的方法是让监测器管理它自己的内部堆栈。这很容易,因为对于每个方法都要调用 start();唯一的技巧就是当抛出异常时就解开内部调用堆栈。在调用 Profile.end() 时,通过检查预期的类和方法名称,可以探测到什么时候抛出了异常。

输出的设置也很容易。可以用 Runtime.addShutdownHook() 登记一个 Thread 来创建一个 shutdown 钩子,在关闭的时候运行,向控制台输出监测报告。

结束语

这篇文章介绍了监测目前最常用的工具和技术,并讨论了它们的一些局限性。还提供了一个理想的监测器应当具有的特性列表。最后,学习了如何用面向方面编程和 Java 5 代理接口构建出集成了一些理想特性的自己的监测器。

这篇文章的示例代码基于 Java 交互式监测器,这是一个用这里讨论的技术构建的开放源码监测器。除了示例监测器中的基本特性之外,JIP 还集成了以下特性:

  • 交互式监测
  • 排除类或包的能力
  • 只包含由特定类装入器装入的类的能力
  • 跟踪对象分配的工具
  • 代码监测之外的性能测量

JIP 是在 BSD 形式的许可下分发的。请参阅 参考资料 获得下载信息。



  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值