Java Debug Interface(JDI)调试多线程应用程序

来源:互联网 发布:360软件管家手机安卓版 编辑:程序博客网 时间:2024/06/03 00:14

        项目中遇到500多个线程并发执行,并将线程执行所生成的数据插入MySql数据库,按设想,500个线程,数据库中应有序号连续的500条记录。然而,郁闷的是数据库中的记录在第450条左右就开始不连续,部分记录缺失。500多个线程几乎是独立的,它们之间存在的资源竞争已经做好同步了,因此,由于资源而阻塞的情况排除。再者,500个线程间优先级均等同,我中间做了sleep操作,让线程sleep时间与序号成正比例关系。按理说,每个线程都有机会运行,不存在有部分线程因未被调度到没有运行的情况。

        针对上述问题,我先采用打印 Trace Log的方式来调试。关于单步调式,在线程数目很多的情况下,或许不是最好的选择。在 IDE 中通过添加断点的方式调试程序,往往会因为停在某一条线程的某个断点上而错失了其他线程的执行。另外,线程之间的调度往往无法预期,并且会因为断点影响了实际的线程执行顺序。嘿嘿,允许我这么说吧,对于多线程,尤其是很多个线程并发下的单步调式,我还真不很清楚快哭了,所以请专家见谅。日志调试的方式,可以帮助我们及时记录每个线程的启动、运行、想要watch的变量信息等等,可以说好处多多。不太好的就是我们往往无法预期哪些关键点需要记录,于是在整个程序的调试过程中,需要不断地加入 Log 调用,我已深有感受。。。这对于大尺寸的软件开发项目无疑是噩梦,而且开发效率会受到很大的影响。因此,上网查询多线程调试工具,以希望解决纠结了我N就的问题。发现目前用的较多的貌似是JDI来开发的debugger吧,于是小试牛刀了下。

   一、认识JPDA和JDI

   JPD(

Java Platform Debugger Architecture)是一套架构,开发者可以通过这套架构来开发调试用程序。目前这套架构被主流的 Java IDE(如 Eclipse、NetBeans 等)广泛地采用。更多关于JPDA的详细

介绍,可以参见JPDA官方文档以及“深入java调式体系”系列文章。Java Debuger Interface(JDI),定义了代码级别的调试接口。目前,大多数的 JDI 实现都是通过 Java 语言编写的。比如,大家再熟悉不过的 Eclipse IDE,它的调试工具相信大家都使用过。它的两个插件 org.eclipse.jdt.debug.ui 和 org.eclipse.jdt.debug 与其强大的调试功能密切相关,其中 org.eclipse.jdt.debug.ui 是 Eclipse 调试工具界面的实现,而 org.eclipse.jdt.debug 则是 JDI 的一个完整实现。

     二、使用JDI开发调式工具

       (1)需求

               我们要开发的调试工具大致满足一下的通用需求:

1、 独立于目标应用程序。

2、应该足够简单,并且能在通过少量的代码修改就能完成集中配置,这样是帮助开发者不需要付出太多的努力就能开始调试自己的多线程程序。

3、能够抓取足够的信息,比如说异常的信息,程序调用过程中的变量值等等。

4、所生成的 Log 应该足够清晰,能够按不同的线程来分离记录,而不是按照时间的顺序来生成每一条记录,否则会给调试带来不便。 

     (2)实现

      在IBM的技术网站上,提供了一个典型的基于JDI的调试工具的示例,可参考http://www.ibm.com/developerworks/cn/java/j-lo-jdi/#download。该示例依据前面所提到的需求,用来 Profile 一个简单的多线程程序的执行。它展示了线程运行栈快照、方法调用的入口参数值收集、异常过滤定制、类过滤配置、线程 Log 记录等功能。相关的详细信息大家可以参考该网站上的内容。

     另外,需要把安装路径下的,如com.sun.java.jdk.win32.x86_1.6.0.013\lib下的tools.jar加入当前工程中,tools.jar中提供了com.sun.jdi的库,开发时调用的接口大多都是这个包下的。

      总体来说,JDI的工作过程主要为以下操作:

  1. 绑定,分析工具和目标调试程序的虚拟机实例绑定;
  2. 事件注册,分析工具向虚拟机实例注册相关事件请求,整个分析过程采取基于事件驱动的模式。
  3. 线程运行时信息挖掘。
  4. 分类信息生成。
       下面分析下每一操作的具体过程。

      绑定

     JDI 支持四种对目标程序的绑定方式,分别为:

  1. 分析器启动目标程序虚拟机实例
  2. 分析器绑定到已运行的目标程序虚拟机实例
  3. 目标程序虚拟机实例绑定到已运行的分析器
  4. 目标程序虚拟机实例启动分析器

     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;}}throw new Error("No launching connector");}

2、设置连接参数 

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;}

3、启动连接,获取目标程序虚拟机实例 

VirtualMachine launchTarget(String mainArgs) {mainArgs = mainArgs.trim();LaunchingConnector connector = findLaunchingConnector();//获取连接实例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、  注册事件

EventRequestManager 管理事件请求,它支持创建、删除和查询事件请求。EventRequest 支持三种挂起策略:

  • EventRequest.SUSPEND_ALL : 事件发生时,挂起所有线程
  • EventRequest.SUSPEND_EVENT_THREAD : 事件发生时,挂起事件源线程
  • EventRequest.SUSPEND_NONE : 事件发生时,不挂起任何线程

    JDI 支持多种类型的 EventRequest,如 ExceptionRequest,MethodEntryRequest,MethodExitRequest,ThreadStartRequest 等,可以参考 文档。

void setEventRequests() {EventRequestManager mgr = vm.eventRequestManager();// want all exceptions 注册异常事件ExceptionRequest excReq = mgr.createExceptionRequest(null, true, true);// suspend so we can stepexcReq.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);//事件发生时,挂起事件源线程excReq.enable();               // 注册进方法事件MethodEntryRequest menr = mgr.createMethodEntryRequest();for (int i = 0; i < excludes.length; ++i) {menr.addClassExclusionFilter(excludes[i]);}menr.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);menr.enable();// 注册出方法事件MethodExitRequest mexr = mgr.createMethodExitRequest();for (int i = 0; i < excludes.length; ++i) {mexr.addClassExclusionFilter(excludes[i]);}mexr.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);mexr.enable();// 注册线程启动事件ThreadStartRequest tsr = mgr.createThreadStartRequest();// Make sure we sync on thread deathtsr.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);tsr.enable();               // 注册线程结束事件ThreadDeathRequest tdr = mgr.createThreadDeathRequest();// Make sure we sync on thread deathtdr.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);tdr.enable();}

2、分析器从事件队列中获取事件

         EventQueue 用来管理目标虚拟机实例的事件,事件会被加入 EventQueue 中。分析器调用 EventQueue.remove(),如果事件队列中存在事件,则返回不可修改的 EventSet 实例,否则分析器会被挂起直到有新的事件发生。处理完 EventSet 中的事件后,调用其 resume() 方法唤醒 EventSet 中所有事件发生时可能挂起的线程。   

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 的调试方式,它们的主要目的是向开发者提供以上两方面信息。本文分析器以单个线程为单位,来记录线程运行信息:

  1. 执行流程。分析器以方法作为最小颗粒度单位。分析器按照实际的线程执行顺序记录方法进出。
  2. 变量值。对于单个方法而言,其程序逻辑固定,方法的输入值决定了方法内部执行流程。分析器将在方法入口和出口分别记录该方法作用域内可见变量,便于开发者调试。
  3. 执行栈信息记录。当异常发生时,执行栈中完好地保存了调用帧信息。分析器获取线程栈中的所有帧,并记录每个帧记录的信息,其中包含可见变量值、帧调用名称等信息。StackFrame 中变量信息的获取也是 JDI 所提供的特殊能力之一。关于帧栈(StackFrame)的详情,请参考:sun的官网http://docs.oracle.com/javase/6/docs/jdk/api/jpda/jdi/com/sun/jdi/StackFrame.html执行流程。
     线程执行流程

     线程执行流程可划分:线程启动→ run() →进入方法→ ... →退出方法→线程结束。通过向虚拟机实例注册 ThreadStartRequest,MethodEntryRequest,MethodExitRequest 和 ThreadDeathRequest 事件的方式记录执行过程。

// Forward event for thread specific processingprivate void methodEntryEvent(MethodEntryEvent event) {threadTrace(event.thread()).methodEntryEvent(event);}// Forward event for thread specific processingprivate void methodExitEvent(MethodExitEvent event) {threadTrace(event.thread()).methodExitEvent(event);}void threadDeathEvent(ThreadDeathEvent event) {ThreadTrace trace = (ThreadTrace) traceMap.get(event.thread());if (trace != null) { // only want threads we care abouttrace.threadDeathEvent(event); // Forward event}}       //获取执行流程1——线程启动void threadStartEvent(ThreadStartEvent event) {threadTrace(event.thread()).threadStartEvent(event);}
可见变量信息抓取 

以下代码中抓取的是name和iValue两个变量。
private void printVisiableVariables(){try{this.thread.suspend();if(this.thread.frameCount()>0){//retrieve current method frame  获取当前方法所在的帧StackFrame frame = this.thread.frame(0);Field field2=frame.thisObject().referenceType().fieldByName("name");increaseIndent();println(field2.name() + "\t"+ field2.typeName()+ "\t" + frame.thisObject().getValue(field2));decreaseIndent();Field field1=frame.thisObject().referenceType().fieldByName("iValue");increaseIndent();println(field1.name() + "\t"+ field1.typeName()+ "\t" + frame.thisObject().getValue(field1));decreaseIndent();}}catch(Exception e){//ignore}finally{this.thread.resume();}}
异常时线程栈快照 
                //异常事件线程栈快照private void printStackSnapShot() {if (isMainThreadOrCreatedFromMain(this.thread)) {try {this.thread.suspend();println("Thread Status:" + this.thread.status());println("FrameCount in thread:" + this.thread.frameCount());//获取线程栈List<StackFrame> frames = this.thread.frames();//获取线程栈信息for (StackFrame frame : frames) {println("Frame(" + frame.location()+ ")");if (frame.thisObject() != null) {increaseIndent();//获取当前对象应该的所有字段信息println("");//获取帧的可见变量信息List<Field> fields = frame.thisObject().referenceType().allFields();for (Field field : fields) {println(field.name() + "\t"+ field.typeName()+ "\t" + frame.thisObject().getValue(field));}decreaseIndent();}List<LocalVariable> lvs = frame.visibleVariables();increaseIndent();println("");for (LocalVariable lv : lvs) {println(lv.name() + "\t" + lv.typeName() + "\t" + frame.getValue(lv));}decreaseIndent();}} catch (Exception e) {// ignore the exception} finally {this.thread.resume();}}}
分类信息生成log

以单线程为记录单元是分析器的特点,下面将从分析器 Log 实现结构、目标程序所模拟的场景及分析结果三方面对示例代码进行介绍。

  1. 分析器 Log 实现结构

    Trace 为分析器入口类,它负责创建绑定连接,生成目标程序虚拟机实例;EventThread 负责从虚拟机实例的事件队列中获取事件,交由对应的 ThreadTrace 处理,它同时维护着一张 ThreadReference 和 ThreadTrace 一一对应关系的映射表;ThreadTrace 负责分析 ThreadReference 信息,并将结果记录在 logRecord 的缓存中,每个 ThreadTrace 实现了单个线程信息的追踪,详见图 1。

2、目标程序

     由两个核心类组成:MainThread 和 CounterThread。MainThread 是程序的主类,它负责启动两个 CounterThread 线程实例并抛出两类异常:用户自定义异常 UserDefinedException 和运行时异常 NullPointerException;CounterThread 是一个简单的计数线程。整个目标程序模拟的是多线程和异常的环境。

3、分析结果

   Log 依照目标程序的调用层次进行缩进,清晰地展现每个线程的执行逻辑和变量信息,详见如下图。


结语

 JDI确实在多线程调试中起到了作用。问题是项目程序代码量大,设置了很多过滤,即ExcludeClass,还是没法用JDI。。。未完待续吧。

参考:

深入 Java 调试体系,第 4 部分: Java 调试接口(JDI)

使用 Java Debug Interface(JDI)调试多线程应用程序

sun官方文档




	
				
		
原创粉丝点击