利用Java Debug Interface(JDI)调试多线程应用措施
副标题#e#
多线程情况下的措施调试是让开拓者头痛的问题。在 IDE 中通过添加断点的 方法调试措施,往往会因为停在某一条线程的某个断点上而错失了其他线程的执 行,线程之间的调治往往无法预期,而且会因为断点影响了实际的线程执行顺序 。因此,在调试多线程措施时,开拓者往往会选择打印 Trace Log 的方法来帮 助调试。
利用 Log 来辅佐调试的问题在于,开拓者往往无法预期哪些要害点需要记录 ,于是在整个措施的调试进程中,需要不绝的插手 Log 挪用,编译生成可执行 措施并陈设,这对付大尺寸的软件开拓项目无疑是恶梦,会直接影响到开拓效率 。
有没有一种步伐,可以独立于措施代码,能在运行期间绑定到措施上并获取 措施运行进程傍边的要害信息呢?更重要的,这种要领应该是可定制的,开拓者 可以通过少量的尽力,就可以到达特定的调试目标。谜底是必定的。通过利用 java Debug Interface(JDI),开拓者可以快速开拓定制出合用于本身的线程 Profiling 东西。这样的东西独立于主措施,而且可高度定制。在接下来的文章 中,我们将先容如何实现该东西。
认识 JPDA 和 JDI
从 J2SE 1.3 开始,Java 开始提供了一套叫做 Java Platform Debugger Architecture(JPDA)的架构,开拓者可以通过这套架构来开拓调试用措施。这 套架构被主流的 Java IDE(如 Eclipse、NetBeans 等)遍及地回收。
详细来说,JPDA 不只仅是一套 API 的组合,也不可是一个详细的东西。这 套架构提供了从方针措施、调试两边的信息协议,到供开拓者利用的布局挪用, 都一一做出了界说。在 J2SE 5.0 中,它由三个部门构成:
Java Virtual Machine Tools Interface(JVMTI),是一套初级此外 native 接口。它界说了 Java 虚拟机所必须为调试提供的处事接口。JVMTI 在 Java 5.0 之前的前身是 JVMDI(Jave Virtual Machine Debug Interface)。
Java Debug Wire Protocol(JDWP),界说了调试两边信息和请求的文本格 式。
Java Debuger Interface(JDI),界说了代码级此外调试接口。
从开拓者的角度来看,调试东西的开拓既可以基于 JVMTI 也可以基于 JDI。 JVMTI 是 native 接口,利用起来相对巨大,而且需要 C 语言的基本,因此, 在本文中,我们将先容如何利用 JDI 这种最上层的方法来开拓 Java 调试措施 。
需求阐明
在接下的部门,我们将先容如何利用 JDI 来开拓一个用来调试多线程措施的 东西。在开始前,让我们先列出这个东西需要满意的成果:
独立于方针应用措施的。
应该足够简朴,而且能在通过少量的代码修改就能完成会合设置,这样是帮 助开拓者不需要支付太多的尽力就能开始调试本身的多线程措施。
可以或许抓取足够的信息,好比说异常的信息,措施挪用进程中的变量值等等。
所生成的 Log 应该足够清晰,可以或许按差异的线程来疏散记录,而不是凭据时 间的顺序来生成每一笔记录,不然会给调试带来未便。
实现
在文章最后的 示例代码 中,我们展示了一个典范的基于 JDI 的调试东西逻 辑,而且用它来 Profile 一个简朴的多线程措施的执行。按照前面所提到的需 求,代码展示了线程运行栈快照、要领挪用的进口参数值收集、异常过滤定制、 类过滤设置、线程 Log 记录等成果。详细来说:
独立于方针措施
阐明东西可以通过如下方法启动:
java Trace options class args
支持的 options 参数:
-output 文件名:东西生成的 Log 的路径
class 是方针措施的进口类,args 为方针措施的输入参数
#p#副标题#e#
简捷设置
异常过滤设置:
您可以在 ExceptionConfig.properties 属性文件中设置所需记录异常范例 。在 Demo 代码中设置了对付 NullPointerException 和 UserDefinedException 两种异常,阐明东西将追踪这两种异常环境。
ExceptionName = exceptions.UserDefinedException;java.lang.NullPointerException
类过滤设置:
您可以在 ClassExcludeConfig.properties 属性文件中设置被过滤的类模式 ,阐明东西将不会处理惩罚被过滤类的任何事件。
ExcludedClassPattern=java.*;javax.*;sun.*;com.sun.*;com.ibm.*
运行
在方针的主措施的生命周期中,阐明器完成以下操纵:
绑定,阐明东西和方针调试措施的虚拟机实例绑定;
事件注册,阐明东西向虚拟机实例注册相关事件请求,整个阐明进程采纳基 于事件驱动的模式。
线程运行时信息挖掘。
分类信息生成。
以上四点操纵满意了需求:通过回收绑定机制实现调试措施和东西措施的独 立,阐明东西和方针措施以监听端口、共享内存等方法举办通信,无须方针措施 举办任何代码修改即可实现调试。回收基于事件的机制可以辅佐开拓者依据实际 需要会合注册和处理惩罚事件。作为基本框架,阐明东西注册了支持异常、执行流程 等事件,并提供了异常时运行栈快照,要领收支参数记录等成果实现信息抓取。 支持单线程为单元的 Log 记录,将开拓者从无序不行预测的多线程执行中挣脱 出来,对换试措施提供辅佐。
下面将具体叙述实现步调:
绑定
JDI 支持四种对方针措施的绑定方法,别离为:
阐明器启动方针措施虚拟机实例
阐明器绑定到已运行的方针措施虚拟机实例
方针措施虚拟机实例绑定到已运行的阐明器
方针措施虚拟机实例启动阐明器
#p#分页标题#e#
JDI 支持一个阐明器绑定多个方针措施,但一个方针措施只能绑定一个阐明 器。为支持以上绑定,JDI 对应有 LaunchingConnector,AttachingConnector 和 ListeningConnector,详细类先容可以参照 文档。
本文回收第一种绑定方法叙述如何开拓定制的多线程阐明器,其它绑定方法 可以参照 文档。
绑定进程分为三个步调:
获取毗连实例
清单 1. 获取毗连实例
LaunchingConnector findLaunchingConnector() {
List connectors = Bootstrap.virtualMachineManager ().allConnectors();
Iterator iter = connectors.iterator();
while (iter.hasNext()) {
Connector connector = (Connector) iter.next();
if ("com.sun.jdi.CommandLineLaunch".equals(connector.name ())) {
return (LaunchingConnector) connector;
}
}
}
Bootstrap.virtualMachineManager().allConnectors() 返回所有已知的 Connector 工具实例。选择返回 com.sun.jdi.CommandLineLaunch 毗连实例, 暗示利用第一种绑定方法。
配置毗连参数
清单 2. 配置毗连参数
/**参数:
* connector为清单1.中获取的Connector毗连实例
* mainArgs为方针措施main函数地址的类
**/
Map connectorArguments(LaunchingConnector connector, String mainArgs) {
Map arguments = connector.defaultArguments();
Connector.Argument mainArg = (Connector.Argument) arguments.get("main");
if (mainArg == null) {
throw new Error("Bad launching connector");
}
mainArg.setValue(mainArgs);
return arguments;
}
每个毗连实例都有对应的默认参数,启动毗连之前需要配置必需的参数,对 于 CommandLineLaunch 毗连实例需要配置主措施启动方针措施虚拟机实例所需 的参数。
启动毗连,获取方针措施虚拟机实例
清单 3. 启动毗连
/**参数:
* mainArgs为方针措施main函数地址的类
**/
VirtualMachine launchTarget(String mainArgs) {
//findLaunchingConnector:获取毗连
LaunchingConnector connector = findLaunchingConnector();
//connectorArguments:配置毗连参数
Map arguments = connectorArguments(connector, mainArgs);
try {
return connector.launch(arguments);//启动毗连
} catch (IOException exc) {
throw new Error("Unable to launch target VM: " + exc);
} catch (IllegalConnectorArgumentsException exc) {
throw new Error("Internal error: " + exc);
} catch (VMStartException exc) {
throw new Error("Target VM failed to initialize: " + exc.getMessage());
}
}
清单 1 和清单 2 别离获取毗连实例和启动所需的变量,通过挪用 connector.launch(arguments) 启动毗连,实现了阐明器和方针措施的绑定。
注册事件
阐明器和方针措施之间回收基于事件的模式举办通信。阐明器向虚拟机实例 注册所存眷的事件。事件产生时,虚拟机将相关事件信息放入事件行列中,回收 出产者 – 消费者 的模式与阐明器同步。
注册事件
EventRequestManager 打点事件请求,它支持建设、删除和查询事件请求。 EventRequest 支持三种挂起计策:
EventRequest.SUSPEND_ALL : 事件产生时,挂起所有线程
EventRequest.SUSPEND_EVENT_THREAD : 事件产生时,挂起事件源线程
EventRequest.SUSPEND_NONE : 事件产生时,不挂起任何线程
JDI 支持多种范例的 EventRequest,如 ExceptionRequest, MethodEntryRequest,MethodExitRequest,ThreadStartRequest 等,可以参考 文档。
清单 4. 注册事件
#p#分页标题#e#
EventRequestManager mgr = vm.eventRequestManager();
// 注册异常事件
ExceptionRequest excReq = mgr.createExceptionRequest(null, true, true);
excReq.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
excReq.enable();
// 注册进要领事件
MethodEntryRequest menr = mgr.createMethodEntryRequest();
menr.setSuspendPolicy(EventRequest.SUSPEND_NONE);
menr.enable();
// 注册出要领事件
MethodExitRequest mexr = mgr.createMethodExitRequest();
mexr.setSuspendPolicy(EventRequest.SUSPEND_NONE);
mexr.enable();
// 注册线程启动事件
ThreadStartRequest tsr = mgr.createThreadStartRequest();
tsr.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
tsr.enable();
// 注册线程竣事事件
ThreadDeathRequest tdr = mgr.createThreadDeathRequest();
tdr.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
tdr.enable();
阐明器从事件行列中获取事件
EventQueue 用来打点方针虚拟机实例的事件,事件会被插手 EventQueue 中 。阐明器挪用 EventQueue.remove(),假如事件行列中存在事件,则返回不行修 改的 EventSet 实例,不然阐明器会被挂起直到有新的事件产生。处理惩罚完 EventSet 中的事件后,挪用其 resume() 要领叫醒 EventSet 中所有事件产生 时大概挂起的线程。
清单 5. 获取事件
public void run() {
EventQueue queue = vm.eventQueue();
while (connected) {
try {
EventSet eventSet = queue.remove();
EventIterator it = eventSet.eventIterator();
while (it.hasNext()) {
handleEvent(it.nextEvent());
}
eventSet.resume();
} catch (InterruptedException exc) {// Ignore
} catch (VMDisconnectedException discExc) {
handleDisconnectedException();
break;
}
}
}
获取多线程执行信息
执行流程和变量信息是调试措施最重要的两方面。无论是通过 IDE 配置断点 的调试方法,照旧通过在措施中记 Log 的调试方法,它们的主要目标是向开拓 者提供以上两方面信息。本文阐明器以单个线程为单元,来记录线程运行信息:
执行流程。阐明器以要领作为最小颗粒度单元。阐明器凭据实际的线程执行 顺序记录要领收支。
变量值。对付单个要领而言,其措施逻辑牢靠,要领的输入值抉择了要领内 部执行流程。阐明器将在要领进口和出口别离记录该要领浸染域内可见变量,便 于开拓者调试。
执行栈信息记录。当异常产生时,执行栈中完好地生存了挪用帧信息。阐明 器获取线程栈中的所有帧,并记录每个帧记录的信息,个中包括可见变量值、帧 挪用名称等信息。StackFrame 中变量信息的获取也是 JDI 所提供的非凡本领之 一。
与 IDE 配置断点的要领对比,提供的数据信息量相当,但阐明器提供执行流 程信息越发的清晰;与在措施中记录 Log 的方法对比,阐明器在执行流程和信 息量两方面都胜出。
以下将具体先容上面三方面信息抓取:
线程执行流程
线程执行流程可分别:线程启动→ run() →进入要领→ … →退出要领→ 线程竣事。通过向虚拟机实例注册 ThreadStartRequest,MethodEntryRequest ,MethodExitRequest 和 ThreadDeathRequest 事件的方法记录执行进程。事件 注册具体见清单 4,清单 6 列出阐明器对付以上事件的处理惩罚要领。
清单 6. 获取执行流程
void threadStartEvent(ThreadStartEvent event) {
println("Thread " + event.thread().name() + " Start");
}
void methodEntryEvent(MethodEntryEvent event) {
println("Enter Method:" + event.method().name() + " -- "
+ event.method().declaringType().name());
// 进入要领记录可见变量值
this.printVisiableVariables();
}
void methodExitEvent(MethodExitEvent event) {
println("Exit Method:" + event.method().name() + " -- "
+ event.method().declaringType().name());
// 退出要领记录可见变量值
this.printVisiableVariables();
}
void threadDeathEvent(ThreadDeathEvent event) {
println("Thread " + event.thread().name() + " Dead");
}
可见变量信息抓取
清单 7. 可见变量信息抓取
#p#分页标题#e#
private void printVisiableVariables()
{
try{
this.thread.suspend();
if(this.thread.frameCount()>0) {
//获取当前要领地址的帧
StackFrame frame = this.thread.frame(0);
List<LocalVariable> lvs = frame.visibleVariables();
for (LocalVariable lv : lvs) {
println("Name:" + lv.name() + "\t" + "Type:"
+ lv.typeName() + "\t" + "Value:"
+ frame.getValue(lv));
}
}
} catch(Exception e){//ignore}
finally{this.thread.resume();}
}
通过 this.thread.frame(0) 获取当前要领对应的帧,挪用 frame.visibleVariables() 取出当前要领帧的所有可见变量。
异常时线程栈快照
清单 8. 异常事件线程栈快照
private void printStackSnapShot() {
try {
this.thread.suspend();
//获取线程栈
List<StackFrame> frames = this.thread.frames ();
//获取线程栈信息
for (StackFrame frame : frames) {
if (frame.thisObject() != null) {
//获取当前工具应该的所有字段信息
List<Field> fields = frame.thisObject ().referenceType().allFields();
for (Field field : fields) {
println(field.name() + "\t" + field.typeName()+ "\t"
+ frame.thisObject().getValue(field));
}
}
//获取帧的可见变量信息
List<LocalVariable> lvs = frame.visibleVariables();
for (LocalVariable lv : lvs) {
println(lv.name() + "\t" + lv.typeName() + "\t"
+ frame.getValue(lv));
}
}
} catch (Exception e) {}
finally { this.thread.resume();}
}
通过 this.thread.frames() 获取异常产生时线程栈中所有帧信息,挪用 frame.thisObject() 获取对 this 指针的引用,进而获取工具字段信息;对付 帧信息的抓取与清单 7 雷同。
分类信息生成 Log
以单线程为记录单位是阐明器的特点,下面将从阐明器 Log 实现布局、方针 措施所模仿的场景及阐明功效三方面临示例代码举办先容。
阐明器 Log 实现布局
Trace 为阐明器进口类,它认真建设绑定毗连,生成方针措施虚拟机实例; EventThread 认真从虚拟机实例的事件行列中获取事件,交由对应的 ThreadTrace 处理惩罚,它同时维护着一张 ThreadReference 和 ThreadTrace 一一 对应干系的映射表;ThreadTrace 认真阐明 ThreadReference 信息,并将功效 记录在 logRecord 的缓存中,每个 ThreadTrace 实现了单个线程信息的追踪, 详见图 1。
图 1. 阐明器类图 :
方针措施
方针措施由两个焦点类构成:MainThread 和 CounterThread。MainThread 是措施的主类,它认真启动两个 CounterThread 线程实例并抛出两类异常:用 户自界说异常 UserDefinedException 和运行时异常 NullPointerException; CounterThread 是一个简朴的计数线程。整个方针措施模仿的是多线程和异常的 情况。
阐明功效
Log 依照方针措施的挪用条理举办缩进,清晰地揭示每个线程的执行逻辑和 变量信息,详见清单 9。为了利便领略,我们在 log 中插手了注释。
清单 9. Log
-- VM Started --
====== main ======
Enter Method:main//
Enter Method:<init>//MainThread 结构函数
a int 0
b int 0
c int 0
Exit Method:<init>
Enter Method:makeABusinessException//makeABusinessException 要领挪用
a int 0
b int 1
c int 2
Enter Method:<init>//UserDefinedException 结构函数
...
Exit Method:<init>
//UserDefinedException 异常产生,抓取线程栈中所有帧信息
exceptions.UserDefinedException(id=62) catch: MainThread:30
Frame(MainThread:44)
a int 0
b int 1
c int 2
i int 0
d int 4
Frame(MainThread:23)
e int 4
g int 5
mt MainThread instance of MainThread(id=59)
i int 0
// NullPointerException 异常产生,抓取线程栈信 息
java.lang.NullPointerException(id=70) catch: MainThread:30
...
// 以下是两个 CounterThread 线程的结构
Enter Method:<init>
name java.lang.String null
index int 0
Exit Method:<init>
Enter Method:<init>
name java.lang.String null
index int 0
Exit Method:<init>
Exit Method:main
====== main end ======
====== Thread-1 ======
Enter Method:run//run 要领挪用
name java.lang.String "thread1"
index int 0
// 以下是 3 次 updateIndex 要领挪用
Enter Method:updateIndex
name java.lang.String "thread1"
index int 0
Exit Method:updateIndex
Enter Method:updateIndex
name java.lang.String "thread1"
index int 2
Exit Method:updateIndex
Enter Method:updateIndex
name java.lang.String "thread1"
index int 4
Exit Method:updateIndex
Exit Method:run
====== Thread-1 end ======
====== Thread-2 ======
Enter Method:run//run 要领挪用
name java.lang.String "thread2"
index int 0
// 以下是 3 次 updateIndex 要领挪用
Enter Method:updateIndex
name java.lang.String "thread2"
index int 1
Exit Method:updateIndex
Enter Method:updateIndex
name java.lang.String "thread2"
index int 3
Exit Method:updateIndex
Enter Method:updateIndex
name java.lang.String "thread2"
index int 5
Exit Method:updateIndex
Exit Method:run
====== Thread-2 end ======
结语
当开拓多线程措施时,至少有两个来由让你选择 JDI 来协助调试:
线程执行的时序变得越来越不行预测,在 IDE 中通过添加断点来调试的要领 已经不能正确地反应措施运行状况。
措施局限大,每一次 trace 语句的添加城市造成措施的再编译,而这样的编 译需要花上许多时间。
#p#分页标题#e#
因此,利用 JDI 开拓本身的调试措施,有时会为开拓者节减更多的时间。通 过本文的先容和示例代码的解读,读者可以着手开拓本身的多线程调试措施了。
本文配套源码