文章
服务器与存储开发
作者:Amit Hurvitz
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 应用程序行为探索示例
Java Hotspot VM 1.7 支持 JSDT。使用 1.7.0_04 版以避免无法创建第一个提供程序的问题。我在 Oracle Solaris 11 上测试了本文所述的过程,在 Oracle Solaris 10 上应一切工作正常;但我没有检查支持 DTrace 的其他操作系统,也没有检查其他 JVM。
BTrace 客户端向目标应用程序发起连接有时会失败。如果失败,只需重试。
BTrace 当前不清理(“取消插装”)插装的类;它只是停用探测器。此行为可防止重复插装相同的探测器和相同的类。我希望不久会实现清理。
定义 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 接口
我们看到了如何通过在源代码中添加探测器来静态定义探测器。现在我们来尝试在不更改源代码的情况下完成同一任务。要使用 Attach API 和 Java 代码插装功能,可以利用非常棒的 BTrace 软件包,该软件包提供了一组丰富的动态跟踪功能以便在跟踪 Java 应用程序时设法遵循 DTrace 标准。BTrace 本身具有极高的价值,但这里我们只是“顺带”将其与我们的 JSDT 探测器一起使用来对一个运行中的应用程序进行插装。
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 1.2 起)触发 JSDT 探测器,需要进行一些设置和调整:
<Btrace-install-dir>/bin/btrace 脚本并将 -Dcom.sun.btrace.unsafe=false 更改为 -Dcom.sun.btrace.unsafe=true,转到 BTrace unsafe 模式。<Btrace-install-dir>/bin/btrace 处的 Java 调用命令中向 -cp 链添加提供程序的 JAR 文件,将提供程序添加到 BTrace 编译类路径。如果在 BTrace 脚本中还使用任何其他类(例如想要引用的目标应用程序类),还需要添加它们。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"
-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 命令时引用它。
现在有了要跟踪的程序,我们开始跟踪。(假设尚未进行任何安装。)
btrace-bin.tar.gz)。该文件是针对 Linux 和 Mac 操作系统的,但也适用于 Oracle Solaris。# gunzip < btrace-bin.tar.gz | tar xf -
JAVA_HOME 环境变量设置为正确的 JDK(JDK 7 update 4 或更高版本)。JAVA_HOME 用于 BTrace 二进制文件。bin 目录。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);
}
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. 定义提供程序工厂
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 脚本
<Btrace-install-dir>/bin/btrace 配置 BTrace,如下所示。(建议在编辑该文件之前保存副本。) false 更改为 true:
${JAVA_HOME}/bin/java ... -Dcom.sun.btrace.unsafe=true ...
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 ...
# btrace <target-java-pid> Trace.java
dtrace -l | grep <provider-name> 检查 DTrace 探测器,例如: # dtrace -l | grep MyProvider
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. 测试脚本
# 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 日 |