java调试体系之Java 调试接口(JDI),字节跳动历年校招Java面试真题解析

| — | — | — |

| 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 事件处理框架

一个 JDI 的简单实例


下面给出一个简单例子,说明如何实现 JDI 的部分接口来提供一个简易的调试客户端。首先是被调试的 Java 类,这里给出一个简单的 Hello World 程序,main 方法第一行声明一个“Hello World!”的字符串变量,第二行打印出这个字符串的内容,见 清单 8。

清单 8. HelloWorld 类文件

1

2

3

4

5

6

7

8

package com.ibm.jdi.test;

 

public class HelloWorld {

    public static void main(String[] args) {

        String str = "Hello world!";

        System.out.println(str);

    }

}

接着是一个简单的调试器实现 SimpleDebugger,清单 9 列出了实现该调试器所需要导入的类库和变量。简单起见,所有的变量都声明为静态全局变量。这些变量分别代表了目标虚拟机镜像,目标虚拟机所在的进程,目标虚拟机的事件请求管理器和事件对列。变量 vmExit 标志目标虚拟机是否中止。

清单 9. SimpleDebugger 导入的类和声明的全局变量

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

package com.ibm.jdi.test;

 

import java.util.List;

import java.util.Map;

import com.sun.jdi.Bootstrap;

import com.sun.jdi.LocalVariable;

import com.sun.jdi.Location;

import com.sun.jdi.ReferenceType;

import com.sun.jdi.StackFrame;

import com.sun.jdi.StringReference;

import com.sun.jdi.ThreadReference;

import com.sun.jdi.Value;

import com.sun.jdi.VirtualMachine;

import com.sun.jdi.connect.Connector;

import com.sun.jdi.connect.LaunchingConnector;

import com.sun.jdi.connect.Connector.Argument;

import com.sun.jdi.event.BreakpointEvent;

import com.sun.jdi.event.ClassPrepareEvent;

import com.sun.jdi.event.Event;

import com.sun.jdi.event.EventIterator;

import com.sun.jdi.event.EventQueue;

import com.sun.jdi.event.EventSet;

import com.sun.jdi.event.VMDisconnectEvent;

import com.sun.jdi.event.VMStartEvent;

import com.sun.jdi.request.BreakpointRequest;

import com.sun.jdi.request.ClassPrepareRequest;

import com.sun.jdi.request.EventRequest;

import com.sun.jdi.request.EventRequestManager;

 

public class SimpleDebugger {

    static VirtualMachine vm;

    static Process process;

    static EventRequestManager eventRequestManager;

    static EventQueue eventQueue;

    static EventSet eventSet;

    static boolean vmExit = false;

随后是 SimpleDebugger 的 main() 方法,见 清单 10。首先从 VirtualMachineManager 获取默认的 LaunchingConnector,然后从该 Connector 取得默认的参数。接着,设置 main 和 suspend 参数,使得目标虚拟机运行 com.ibm.jdi.test.HelloWorld 类,并随后进入挂起状态。下一步,调用 LaunchingConnector.launch() 启动目标虚拟机,返回目标虚拟机的镜像实例,并且获取运行目标虚拟机的进程( Process)。

然后,创建一个 ClassPrepareRequest 事件请求。当 com.ibm.jdi.test.HelloWorld 被装载时,目标虚拟机将发送对应的 ClassPrepareEvent 事件。事件处理完成后,通过 process 的 destroy() 方法销毁目标虚拟机进程,结束调试工作。

清单 10. SimpleDebugger 的 main() 方法

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

public static void main(String[] args) throws Exception{

    LaunchingConnector launchingConnector

        = Bootstrap.virtualMachineManager().defaultConnector();

     

    // Get arguments of the launching connector

    Map<String, Connector.Argument> defaultArguments

        = launchingConnector.defaultArguments();

    Connector.Argument mainArg = defaultArguments.get("main");

    Connector.Argument suspendArg = defaultArguments.get("suspend");

    // Set class of main method

    mainArg.setValue("com.ibm.jdi.test.HelloWorld");

    suspendArg.setValue("true");

    vm = launchingConnector.launch(defaultArguments);

 

    process = vm.process()

 

    // Register ClassPrepareRequest

    eventRequestManager = vm.eventRequestManager();

    ClassPrepareRequest classPrepareRequest

        = eventRequestManager.createClassPrepareRequest();

    classPrepareRequest.addClassFilter("com.ibm.jdi.test.HelloWorld");

    classPrepareRequest.addCountFilter(1);

    classPrepareRequest.setSuspendPolicy(EventRequest.SUSPEND_ALL);

    classPrepareRequest.enable();

 

    // Enter event loop

    eventLoop();

 

    process.destroy();

}

下面是 eventLoop() 函数的实现:首先获取目标虚拟机的事件队列,然后依次处理队列中的每个事件。当 vmExit(初始值为 false)标志为 true 时,结束循环。

清单 11. SimpleDebugger 的 eventLoop() 的实现

1

2

3

4

5

6

7

8

9

10

11

12

13

14

private static void eventLoop() throws Exception {

    eventQueue = vm.eventQueue();

    while (true) {

        if (vmExit == true) {

            break;

        }

        eventSet = eventQueue.remove();

        EventIterator eventIterator = eventSet.eventIterator();

        while (eventIterator.hasNext()) {

            Event event = (Event) eventIterator.next();

            execute(event);

        }

    }

}

具体事件的处理是由 execute(Event) 实现的,这里主要列举出 ClassPreparEvent 和 BreakpointEvent 事件的处理用法,请参见 清单 12。

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

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

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

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

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

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
img

最后

很多程序员,整天沉浸在业务代码的 CRUD 中,业务中没有大量数据做并发,缺少实战经验,对并发仅仅停留在了解,做不到精通,所以总是与大厂擦肩而过。

我把私藏的这套并发体系的笔记和思维脑图分享出来,理论知识与项目实战的结合,我觉得只要你肯花时间用心学完这些,一定可以快速掌握并发编程。

不管是查缺补漏还是深度学习都能有非常不错的成效,需要的话记得帮忙点个赞支持一下

整理不易,觉得有帮助的朋友可以帮忙点赞分享支持一下小编~
有帮助,可以添加V获取:vip1024b (备注Java)**
[外链图片转存中…(img-EKiwaVno-1711697873909)]

最后

很多程序员,整天沉浸在业务代码的 CRUD 中,业务中没有大量数据做并发,缺少实战经验,对并发仅仅停留在了解,做不到精通,所以总是与大厂擦肩而过。

我把私藏的这套并发体系的笔记和思维脑图分享出来,理论知识与项目实战的结合,我觉得只要你肯花时间用心学完这些,一定可以快速掌握并发编程。

不管是查缺补漏还是深度学习都能有非常不错的成效,需要的话记得帮忙点个赞支持一下

整理不易,觉得有帮助的朋友可以帮忙点赞分享支持一下小编~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值