java调试体系之Java 调试接口(JDI)

更高级的,当目标虚拟机已处于运行状态时,可以采用调试器 attach 到目标虚拟机的链接方式:

  • 目标虚拟机必须以 -agentlib:jdwp=transport=xxx,server=y 参数启动,并根据传输方式生成监听地址;(其中,xxx 是传输方式,可以是 dt_socket 和 share_memory)

  • 调试器启动,调用 VirtualMachineManager 的 attachingConnectors() 方法获取所有的依附型链接器实例;

  • 根据目标虚拟机采用的传输方式选择一个依附型链接器,调用其 attach() 方法依附到目标虚拟机上;

  • 完成链接后,返回目标虚拟机的实例。

被动链接表示调试器将被动地等待或者监听由目标虚拟机发起的链接,同样也举两个被动链接的例子:

目标虚拟机 attach 到已运行的调试器上的链接方式:

  • 调试器通过 VirtualMachineManager 的 listeningConnectors() 方法获取所有的监听型链接器实例;

  • 为每种传输类型分别选定一个链接器,然后调用链接器的 startListening() 方法让链接器进入监听状态;

  • 通过 accept() 方法通知链接器开始等待正确的入站链接,该方法将返回调试器正在监听的地址描述符;

  • 终端用户以 -agentlib:jdwp=transport=xxx,address=yyy 参数启动目标虚拟机(其中,yyy 是调试器的监听地址);

  • 目标虚拟机会自动地 attach 到调试器上建立链接,然后返回目标虚拟机的实例。

即时(Just-In-Time)链接方式:

  • 以 -agentlib:jdwp=launch=cmdline,onuncaught=y,transport=xxx,server=y 参数启动目标虚拟机;

  • 虚拟机将抛出一个未捕获的异常,同时生成特定于 xxx 传输方式的监听地址,用于确立一次链接;

  • 目标虚拟机启动调试器,并告知调试器传输方式和监听地址;

  • 启动后,调试器调用 VirtualMachineManager 的 attachingConnectors() 方法获取所有依附型链接器实例;

  • 根据指定的 xxx 传输方式,选择一个链接器;

  • 调用链接器的 attach 方法依附到对应地址的目标虚拟机上;

  • 完成链接后,返回目标虚拟机的实例。

Connector.Argument 是 Connector 的内嵌接口,表示链接器的一个参数,不同类型的链接器支持不同的链接器参数,LaunchingConnector 支持 home,main,suspend 等,AttachingConnector 和 ListeningConnector 支持 timeout,hostname,port 等参数,见 表 2。

表 2. 常见的链接器参数

| Connector 类型 | 参数名称 | 说明 |

| — | — | — |

| LaunchingConnector | home | 表示 java.home 的值,指向 JRE |

| main | 表示所要执行的 Java 类的类名 |

| options | 表示使用的 Java 命令行参数 |

| suspend | 表示是否在启动目标虚拟机后挂起虚拟机 |

| AttachingConnector

ListeningConnector | hostname | 表示被链接一端的地址 |

| port | 表示被链接一端的端口 |

| timeout | 表示等待链接的时间 |

下面将举一个简单例子,描述如何设置 main 链接参数,并启动目标虚拟机。首先,调用链接器的 defaultArguments() 获取该链接器所支持的一组默认参数,见 清单 5。

清单 5. 获取链接器的默认参数

1

2

Map<String,Connector.Argument> defaultArguments

    = connector.defaultArguments();

默认参数存储在一个 Key-Value 对的 Map 中,Key 是该链接器参数的唯一标识符(对终端用户不可见),Value 是对应的 Connector.Argument 实例(包括具体参数的信息和默认值)。返回的 Map 不能再新增或者删除元素,只能修改已有元素的值。

然后,从返回的 Map 中获取标识符为 main 的链接器参数,如 清单 6。

清单 6. 返回链接器的 main 参数

1

Connector.Argument mainArgument = defaultArguments.get(“main”);

最后,将 main 参数值设置为 com.ibm.jdi.test.HelloWorld,以修改后的参数启动目标虚拟机,见 清单 7。

清单 7. 设置 main 参数的值并启动虚拟机

1

2

mainArgument.setValue(“com.ibm.jdi.test.HelloWorld”);

VirtualMachine targetVM = connector.launch(defaultArguments);

JDI 事件请求和处理模块


JDI 事件分类

JDI 的 com.sun.jdi.event 包定义了 18 种事件类型,如 表 3 所示。其中,与 Class 相关的有 ClassPrepareEvent 和 ClassUnloadEvent;与 Method 相关的有 MethodEntryEvent 和 MethodExitEvent;与 Field 相关的有 AccessWatchpointEvent 和 ModificationWatchpointEvent;与虚拟机相关的有 VMDeathEvent,VMDisconnectEvent 和 VMStartEvent 等。

表 3. JDI 中的事件类型

| 事件类型 | 描述 |

| — | — |

| ClassPrepareEvent | 装载某个指定的类所引发的事件 |

| ClassUnloadEvent | 卸载某个指定的类所引发的事件 |

| BreakingpointEvent | 设置断点所引发的事件 |

| ExceptionEvent | 目标虚拟机运行中抛出指定异常所引发的事件 |

| MethodEntryEvent | 进入某个指定方法体时引发的事件 |

| MethodExitEvent | 某个指定方法执行完成后引发的事件 |

| MonitorContendedEnteredEvent | 线程已经进入某个指定 Monitor 资源所引发的事件 |

| MonitorContendedEnterEvent | 线程将要进入某个指定 Monitor 资源所引发的事件 |

| MonitorWaitedEvent | 线程完成对某个指定 Monitor 资源等待所引发的事件 |

| MonitorWaitEvent | 线程开始等待对某个指定 Monitor 资源所引发的事件 |

| StepEvent | 目标应用程序执行下一条指令或者代码行所引发的事件 |

| AccessWatchpointEvent | 查看类的某个指定 Field 所引发的事件 |

| ModificationWatchpointEvent | 修改类的某个指定 Field 值所引发的事件 |

| ThreadDeathEvent | 某个指定线程运行完成所引发的事件 |

| ThreadStartEvent | 某个指定线程开始运行所引发的事件 |

| VMDeathEvent | 目标虚拟机停止运行所以的事件 |

| VMDisconnectEvent | 目标虚拟机与调试器断开链接所引发的事件 |

| VMStartEvent | 目标虚拟机初始化时所引发的事件 |

不同的事件需要被分类地添加到不同的事件集合(EventSet)中,事件集是事件发送的最小单位。事件集一旦创建出来,便不可再被修改。JDI 定义了一些规则,用以规定应该如何将事件分别加入到不同的事件集中:

  • 每个 VMStartEvent 事件应该分别加入到单独的一个事件集中;

  • 每个 VMDisconnectEvent 事件应该分别加入到单独的一个事件集中;

  • 所有的 VMDeathEvent 事件应该加入到同一个事件集中;

  • 同一线程的 ThreadStartEvent 事件应该加入到同一事件集中;

  • 同一线程的 ThreadDeathEvent 事件应该加入到同一事件集中;

  • 同一类型的 ClassPrepareEvent 事件应该加入到同一个事件集中;

  • 同一类型的 ClassUnloadEvent 事件应该加入到同一个事件集中;

  • 同一 Field 的 AccessWatchpointEvent 事件应该加入到同一个事件集中;

  • 同一 Field 的 ModificationWatchpointEvent 事件应该加入到同一个事件集中;

  • 同一异常的 ExceptionEvent 事件应该加入到同一个事件集中;

  • 同一方法的 MethodExitEvents 事件应该加入到同一个事件集中;

  • 同一 Monitor 的 MonitorContendedEnterEvent 事件应该加入到用一个事件集中;

  • 同一 Monitor 的 MonitorContendedEnteredEvent 事件应该加入到用一个事件集中;

  • 同一 Monitor 的 MonitorWaitEvent 事件应该加入到同一个事件集中

  • 同一 Monitor 上的 MonitorWaitedEvent 事件应该加入到同一个事件集中

  • 在同一线程执行过程中,具有相同行号信息的 BreakpointEvent、StepEvent 和 MethodEntryEvent 事件应该加入到同一个事件集合中。

生成的事件集将被依次地加入到目标虚拟机的事件队列(EventQueue)中。然后,EventQueue 将这些事件集以“先进先出”策略依次地发送到调试器端。EventQueue 负责管理来自目标虚拟机的事件,一个被调试的目标虚拟机上有且仅有一个 EventQueue 实例。特别地,随着一次事件集的发送,目标虚拟机上可能会有一部分的线程因此而被挂起。如果一直不恢复这些线程,有可能会导致目标虚拟机挂机。因此,在处理好一个事件集中的事件后,建议调用事件集的 resume() 方法,恢复所有可能被挂起的线程。

JDI 事件请求

Event 是 JDI 中所有事件接口的父接口,它只定义了一个 request() 方法,用以返回由调试器发出的针对该事件的事件请求(EventRequest)。事件请求是由调试器向目标虚拟机发出的,目的是请求目标虚拟机在发生指定的事件后通知调试器。只有当调试器发出的请求与目标虚拟机上发生的事件契合时,这些事件才会被分发到各个事件集,进而等待发送至调试器端。在 JDI 中,每一种事件类型都对应着一种事件请求类型。一次事件请求可能对应有多个事件实例,但不是每个事件实例都存在与之对应的事件请求。例如,对于某些事件(如 VMDeathEvent,VMDisconnectEvent 等),即使没有对应的事件请求,这些事件也必定会被发送给调试器端。

另外,事件请求还支持过滤功能。通过给 EventRequest 实例添加过滤器(Filter),可以进一步筛选出调试器真正感兴趣的事件实例。事件请求支持多重过滤,通过 EventRequest 的 add*Filter() 方法可以添加多个过滤器。多个过滤器将共同作用,最终只有满足所有过滤条件的事件实例才会被发给调试器。常用的过滤器有:

  • 线程过滤器:用以过滤出指定线程中发生的事件;

  • 类型过滤器:用以过滤出指定类型中发生的事件;

  • 实例过滤器:用以过滤出指定实例中发生的事件;

  • 计数过滤器:用以过滤出发生一定次数的事件;

过滤器提供了一些附加的限制条件,减少了最终加入到事件队列的事件数量,从而提高了调试性能。除了过滤功能,还可以通过它的 setSuspendPolicy(int) 设置是否需要在事件发生后挂起目标虚拟机。

事件请求是由事件请求管理器(EventRequestManager)进行统一管理的,包括对请求的创建和删除。一个目标虚拟机中有且仅有一个 EventRequestManager 实例。通常,一个事件请求实例有两种状态:激活态和非激活态。非激活态的事件请求将不起任何作用,即使目标虚拟机上有满足此请求的事件发生,目标虚拟机将不做停留,继续执行下一条指令。由 EventRequestManager 新建的事件请求都是非激活的,需要调用 setEnable(true) 方法激活该请求,而通过 setEnable(false) 则可废除该请求,使其转化为非激活态。

JDI 事件处理

下面将介绍 JDI 中调试器与目标虚拟机事件交互的方式。首先,调试器调用目标虚拟机的 eventQueue() 和 eventRequestManager() 分别获取唯一的 EventQueue 实例和 EventRequestManager 实例。然后,通过 EventRequestManager 的 createXxxRequest() 创建需要的事件请求,并添加过滤器和设置挂起策略。接着,调试器将从 EventQueue 获取来自目标虚拟机的事件实例。

一个事件实例中包含着事件发生时目标虚拟机的一些状态信息。以 BreakpointEvent 为例:

调用 BreakpointEvent 的 thread() 可以获取产生事件的线程镜像(ThreadReference),调用 ThreadReference 的 frame(int) 可获得当前代码行所在的堆栈(StackFrame),调用 StackFrame 的 visibleVariables() 可获取当前堆栈中的所有本地变量(LocaleVariable)。通过调用 BreakpointEvent 的 location() 可获得断点所在的代码行号(Location),调用 Location 的 method() 可获得当前代码行所归属的方法。通过以上调用,调试器便可获得了目标虚拟机上线程、对象、变量等镜像信息。

另外,根据从事件实例中获取的以上信息,调试器还可以进一步控制目标虚拟机。例如,可以调用 ObjectReference 的 getValue() 和 setValue() 访问和修改对象中封装的 Field 或者 LocalVariable 等,进而影响虚拟机的行为。更多的 JDI 的事件处理的详情,请参见图 2。

图 2. JDI 事件处理框架(查看大图

图 2. JDI 事件处理框架

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

文章中涉及到的知识点我都已经整理成了资料,录制了视频供大家下载学习,诚意满满,希望可以帮助在这个行业发展的朋友,在论坛博客等地方少花些时间找资料,把有限的时间,真正花在学习上,所以我把这些资料,分享出来。相信对于已经工作和遇到技术瓶颈的朋友们,在这份资料中一定都有你需要的内容。

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!
303032d36.jpg" alt=“img” style=“zoom: 33%;” />

最后

文章中涉及到的知识点我都已经整理成了资料,录制了视频供大家下载学习,诚意满满,希望可以帮助在这个行业发展的朋友,在论坛博客等地方少花些时间找资料,把有限的时间,真正花在学习上,所以我把这些资料,分享出来。相信对于已经工作和遇到技术瓶颈的朋友们,在这份资料中一定都有你需要的内容。

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值