如何跟踪 Oracle Solaris 上运行的 Java 应用程序

作者:Amit Hurvitz

如何结合使用 JSDT 和 BTrace 来动态跟踪 Oracle Solaris 环境中运行的 Java 应用程序,无需更改源代码,也不会影响性能。


2012 年 4 月发布

Oracle Solaris 的 DTrace 特性以其观察运行 Oracle Solaris 10 或更高版本(或采纳 DTrace 的任何其他操作系统)的计算机中所发生的几乎任何事情的广泛能力而闻名。DTrace 可以跟踪 Java 应用程序,但过去在跟踪 Java 代码方面往往存在一些局限。

希望将类似本文的技术文章发送到您的收件箱吗?请订阅系统社区新闻快讯 — 仅包含面向系统管理员和开发人员的技术内容。

对于 Java,有许多优秀的跟踪工具,如包含在 JDK 发布中的 jvisualvm 就非常方便,它提供了丰富的功能。这些工具大多数还是缺乏 DTrace 框架集极度的动态性、非侵入性和广泛的功能于一身的优良品质。

Java 静态定义跟踪 (JSDT) 步针对 C/C++ 代码的用户级静态定义跟踪 (USDT) 的后尘,允许程序员在代码中静态添加探测器。这些探测器在未激活时不影响应用程序性能,激活后旨在将影响降至最低。这打开了广阔的 DTrace 观察范围及其最佳聚合功能之门。

但还是存在潜在的障碍:需要将这些探测器添加到代码(在 JSDT 的情况中,是 Java 代码)。但 Java 引入了一些原生语言中没有的新功能。例如,在程序运行期间可以重新定义程序类。另外,从 Java Platform Standard Edition 6 (Java SE 6) 开始,Attach API 支持将代理附加到任何运行中的 Java SE 6 或更高版本的 JVM 并在附加到的 JVM 内部动态启动和执行代理。所有这些结合在一起,理论上我们可以在 Java 代码中动态插装 JSDT 探测器!这正是本文的主题。

目标

图 1 显示了说明这一思想的示例。在本示例中,我们想探索一些行为,无需停止应用程序、更改代码或重新编译应用程序。

图 1:探索 Java 应用程序的行为

图 1. Java 应用程序行为探索示例

当前要求、局限和注意事项

Java Hotspot VM 1.7 支持 JSDT。使用 1.7.0_04 版以避免无法创建第一个提供程序的问题。我在 Oracle Solaris 11 上测试了本文所述的过程,在 Oracle Solaris 10 上应一切工作正常;但我没有检查支持 DTrace 的其他操作系统,也没有检查其他 JVM。

BTrace 客户端向目标应用程序发起连接有时会失败。如果失败,只需重试。

BTrace 当前不清理(“取消插装”)插装的类;它只是停用探测器。此行为可防止重复插装相同的探测器和相同的类。我希望不久会实现清理。

JSDT 基本知识

定义 JSDT 探测器很轻松,只不过需要进行简单的初始化。第一步是为每个提供程序 (extends com.sun.tracing.Provider) 定义 Java 接口。接口方法同时将是对应的 DTrace 探测器名称,如清单 1 所示。

public interface MyProvider extends com.sun.tracing.Provider {
  void startProbe();
  void workProbe(int intData, String stringData);
  void endProbe();
  }
  
// Use a static factory to create a provider
import com.sun.tracing.ProviderFactory;

public static MyProvider provider;

  ProviderFactory factory = ProviderFactory.getDefaultFactory();
  
  provider = factory.createProvider(MyProvider.class);
  
  // Call the provider methods from inside your code to trigger
  // the corresponding DTrace probes.
  Provider.startProbe();
  ...
  Provider.workProbe(i, str);
   ...
  Provider.endProbe();

清单 1. 定义 Java 接口

如何动态插装 JSDT 探测器

我们看到了如何通过在源代码中添加探测器来静态定义探测器。现在我们来尝试在不更改源代码的情况下完成同一任务。要使用 Attach API 和 Java 代码插装功能,可以利用非常棒的 BTrace 软件包,该软件包提供了一组丰富的动态跟踪功能以便在跟踪 Java 应用程序时设法遵循 DTrace 标准。BTrace 本身具有极高的价值,但这里我们只是“顺带”将其与我们的 JSDT 探测器一起使用来对一个运行中的应用程序进行插装。

BTrace 基本知识

BTrace 是一个基于高效的 ASM 字节码框架构建的工具。BTrace 允许您通过使用特殊的 Java 批注动态插装运行中的 Java 应用程序类。这些批注作为指令指示在目标应用程序中何处应插入跟踪代码,例如:

@OnMethod(clazz="java.lang.Thread",method="start",location=@Location(Kind.Return))

BTrace 在目标 JVM 中创建和调用一个代理(通过 Attach API),然后使用一个客户端与该代理通信以执行插装并获取输出的跟踪数据(如果需要)。

在其默认的“safe”模式下,BTrace 对注入的代码施加某些限制以避免对目标 JVM 产生潜在、非预期的副作用。主要限制之一是不能调用 BTrace 库方法之外的其他方法。(BTrace 在其 utils 软件包中提供了一组丰富的方法。)在本例中,将使用“unsafe”模式,这样我们就可以定义和调用 DTrace 提供程序类方法。

清单 2 是一种“Hello World”BTrace 脚本,它插装 Thread.start() 方法项并打印一条消息。该脚本不调用 BTrace 外部的任何方法,因此可以在 safe 模式下编译。但稍后我们将发出对 JSDT 的调用以提供类方法,那时将不得不使用 unsafe 模式。

// import all BTrace annotations
import com.sun.btrace.annotations.*;
// import statics from BTraceUtils class
import static com.sun.btrace.BTraceUtils.*;

// @BTrace annotation indicates that this is a BTrace program
@BTrace
class HelloWorld {
// @OnMethod annotation indicates where to probe.
// In this example, we are interested in the entry
// into the Thread.start() method.
  @OnMethod(
    clazz="java.lang.Thread",
    method="start"
  )
  void func() {
    sharedMethod(msg);
  }
  void sharedMethod(String msg) {
    // println is defined in BTraceUtils
    println(msg);
  }
}

清单 2. 示例 BTrace 脚本

准备好 BTrace 环境之后,就可以运行该 BTrace 脚本以监视任何运行中的 Java 进程:

# btrace <target-java-pid> HelloWorld.java

有关 BTrace 的详细信息,请参阅 BTrace 项目,特别是查看 BTrace 用户指南

BTrace 脚本编译和目标应用程序运行时均需要 DTrace 提供程序类。提供程序需要在首次调用提供程序方法之前初始化。最好的办法是在类插装过程中初始化提供程序。这可以通过静态初始化来完成。我们定义的 BTrace 类中包含的静态初始化器不能正常工作,但是导入一个含静态初始化的工厂类可以工作,至少可以延迟工作。含静态初始化的提供程序工厂类可能如清单 3 所示。

import com.sun.tracing.*;

public class MyProviderFactory {

  private static ProviderFactory factory;
  public static MyProvider provider;
  
  static {
    factory = ProviderFactory.getDefaultFactory();
    provider = factory.createProvider(MyProvider.class);
  }
  public static void probeName1();
  public static void probeName2(String s, int i);
}

清单 3. 含静态初始化的提供程序工厂类

静态代码最晚在首次调用探测器方法之前初始化。

从 BTrace 脚本触发探测器

为了从 BTrace 代码(从 BTrace 1.2 起)触发 JSDT 探测器,需要进行一些设置和调整:

  1. 通过编辑 <Btrace-install-dir>/bin/btrace 脚本并将 -Dcom.sun.btrace.unsafe=false 更改为 -Dcom.sun.btrace.unsafe=true,转到 BTrace unsafe 模式。
  2. 通过在 <Btrace-install-dir>/bin/btrace 处的 Java 调用命令中向 -cp 链添加提供程序的 JAR 文件,将提供程序添加到 BTrace 编译类路径。如果在 BTrace 脚本中还使用任何其他类(例如想要引用的目标应用程序类),还需要添加它们。
  3. 目标 Java 应用程序将使用引导类加载器加载提供程序。因此,我们需要让引导类加载器能够找到提供程序。除非使用引导类加载器执行其他任务,可以选择以下方式添加到引导类路径:
    • 最简单的方式(不需要向目标应用程序添加标志)是在活动 JRE (jre/classes)(如 /home/ahurvitz/java/jdk1.7.0_04/jre/classes)下的 classes 目录中添加提供程序类。如果没有 classes 目录,则创建它。要验证目标应用程序类路径默认情况下是否在引导类路径中包含此目录,可以运行 JDK jinfo 命令,如下所示:

      # jinfo -sysprops <target-java-pid> | grep "sun.boot.class.path"
      
    • 另一种方式需要向目标应用程序添加 Java 选项(标志),这不如上面一种方便。为此,向目标应用程序添加 -Dsun.boot.class.path=<current-boot-class-path>:<providers-jar> 标志。使用 sun.boot.class.path 属性的值替换 <current-boot-class-path>,该值可以通过运行以下命令获得:

      # jinfo -sysprops <target-java-pid> | grep "sun.boot.class.path" 
      

下载所需软件、安装和运行:分步示例

在下面的示例中,将使用清单 4 中一个极其简单的 Java 程序作为要跟踪的目标程序。假设要针对进出 makeOneIteration() 方法(传递 counter 对象作为参数)的每个条目触发 DTrace 探测器。

package tracetarget;

public class TraceTarget {

    private String strProp;
    private int intProp;
    
    public static void main(String[] args) {
        TraceTarget me = new TraceTarget();
        String runTimeId = java.lang.management.ManagementFactory.getRuntimeMXBean().getName();
        System.out.println(runTimeId);
        me.work();
    }
    
    public TraceTarget() {
        strProp = "I am a tracing target";
        intProp = 17;
    }

    public int getIntProp() {
        return intProp;
    }

    public String getStrProp() {
        return strProp;
    }

    private void makeOneIteration(Counter c) {
        c.count();
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
        }
    }

    public void work() {
        Counter counter = new Counter();
        while (true) {
            makeOneIteration(counter);
        }
    }
}


package tracetarget;

public class Counter {
    private int counter;

    Counter() {
        counter = 0;
    }

    public int getCounter() {
        return counter;
    }

    public void count() {
        counter++;
    }
}

清单 4. 跟踪目标的代码

编译该程序后,运行它。程序友好地输出其进程 ID (pid) 以用于后续步骤。此 pid 为 <target-java-pid>,我们将在运行 btrace 命令时引用它。

现在有了要跟踪的程序,我们开始跟踪。(假设尚未进行任何安装。)

  1. http://kenai.com/projects/btrace/downloads/directory/releases/current 下载 BTrace 1.2.1 版的二进制文件 (btrace-bin.tar.gz)。该文件是针对 Linux 和 Mac 操作系统的,但也适用于 Oracle Solaris。
  2. 对该文件进行解压缩,例如:

    # gunzip < btrace-bin.tar.gz | tar xf -
    
  3. JAVA_HOME 环境变量设置为正确的 JDK(JDK 7 update 4 或更高版本)。JAVA_HOME 用于 BTrace 二进制文件。
  4. 在路径中添加 BTrace bin 目录。
  5. 现在定义 DTrace 提供程序,编译提供程序类并将其存档为一个 JAR 文件。在本示例中,MyProvider 是提供程序名称,startMethod()finishMethod() 是探测器名称。
    package jsdttest;
    
    import com.sun.tracing.*;
    
    public interface MyProvider extends Provider {
    
      public void startMethod(String methodName);
      public void startMethod(String methodName, String str, int i);
      public void finishMethod(int result);
    }
    
  6. 定义提供程序工厂,如清单 5 所示。

    package jsdttest;
    
    import com.sun.tracing.*;
    
    public class MyProviderFactory {
    
      private static ProviderFactory factory;
      public static MyProvider provider;
    
      static {
        factory = ProviderFactory.getDefaultFactory();
        provider = factory.createProvider(MyProvider.class);
      }
      public static void probeName1() {}
      public static void probeName2(String s, int i);
    }
    

    清单 5. 定义提供程序工厂

  7. 编写一个 BTrace 脚本来触发提供程序初始化并根据需要触发探测器。清单 6 Trace.java 是一个示例。
    import com.sun.btrace.annotations.*; 
    // import statics from BTraceUtils class 
    import static com.sun.btrace.BTraceUtils.*;
    import com.sun.tracing.*;
    import com.sun.btrace.AnyType;
    import jsdttest.MyProvider; 
    import jsdttest.DummyProvider; 
    import jsdttest.MyProviderFactory; 
    import tracetarget.TraceTarget;
    import tracetarget.Callee;
    
    // @BTrace annotation indicates that this is a BTrace program
    
    @BTrace class Trace {
    
    // @OnMethod annotation indicates where to probe.
    // In this example, we are interested in entry 
    // into the Thread.start() method. 
    @OnMethod(
        clazz="tracetarget.TraceTarget",
        method="/.*/"
    )
    
        void mEnrty(@Self Object self, @ProbeClassName String probeClass, @ProbeMethodName 
     String probeMethod, AnyType[] args) {
    	MyProvider provider = MyProviderFactory.provider;
            provider.startMethod(probeMethod);
            provider.startMethod(probeMethod, ((TraceTarget)self).getStrProp(), ((Callee)args[0]).getCounter());
        }
    
    @OnMethod(
        clazz="tracetarget.TraceTarget",
        method="/.*/",
        location=@Location(Kind.RETURN)
    )
        void mReturn(@ProbeClassName String probeClass, @ProbeMethodName String probeMethod) {
    	MyProvider provider = MyProviderFactory.provider;
            provider.finishMethod(19);
        }
    }
    

    清单 6. Trace.java 脚本

  8. 通过编辑 <Btrace-install-dir>/bin/btrace 配置 BTrace,如下所示。(建议在编辑该文件之前保存副本。)
    1. 将 unsafe 模式从 false 更改为 true

      ${JAVA_HOME}/bin/java ... -Dcom.sun.btrace.unsafe=true ...
      
    2. 向引导类路径添加提供程序的 JAR 和 BTrace 脚本中使用的任何其他类(例如,引用的对象)。在下例中,我们还将添加 TraceTarget 类,因为在 BTrace 脚本中引用了它的对象。

      ${JAVA_HOME}/bin/java ... -cp ${BTRACE_HOME}/build/btrace-client.jar:${TOOLS_JAR}:/usr/share/lib/java/dtrace.jar:
      /home/ahurvitz/NetBeansProjects/jsdtTest/dist/jsdtTest.jar:/home/ahurvitz/NetBeansProjects/TraceTarget/dist/
      TraceTarget.
      jar ...
      
  9. 运行 BTrace 跟踪目标应用程序:

    # btrace <target-java-pid> Trace.java
    
  10. 使用 dtrace -l | grep <provider-name> 检查 DTrace 探测器,例如:

    # dtrace -l | grep MyProvider
    
  11. 编写一个小 DTrace 脚本来全部进行测试,如清单 7 所示,并调用脚本 my_provider.d

    #!/usr/sbin/dtrace -Cs
    
    BEGIN
    {
        start_timestamp = timestamp;
    }
    
    MyProvider$target:::startMethod
    {
        @starts[pid] = count();
        printf("started method, arg0 = %s\n", copyinstr(arg0));
        printf("arg1 = %s\n", arg1 ? copyinstr(arg1) : "null");
        printf("arg2 = %d\n", arg2);
    }
    
    MyProvider$target:::finishMethod
    {
        @ends[pid] = count();
        printf("finished method, arg0: %d\n", arg0);
    }
    
    tick-5sec
    {
        printf("stats:\n******\n");
        printa(@starts);
        printa(@ends);
    }
    

    清单 7. 测试脚本

  12. 运行 DTrace 测试脚本:

    # dtrace my_provider.d -p <target-java-pid>
    

总结

DTrace、JSDT 和 BTrace 提供了一种在 Java 应用程序中动态插装 DTrace 探测器的轻松方法,它带来了对 Oracle Solaris 10 及更高版本上运行的 Java 应用程序进行即席分析的众多机会。

另请参见

关于作者

Amit Hurvitz 在 Oracle Hardware Engineering(前身为 Sun Microsystems)的 ISV 工程小组工作了 10 年。此前,他从事 C 编译器优化工作并且是 C++ 和 Java Platform Enterprise Edition 开发人员。

修订版 1.0,2012 年 4 月 17 日

通过 FacebookTwitterOracle 博客关注我们。