精华内容
下载资源
问答
  • java调试

    千次阅读 2018-02-28 22:27:15
    如何调试Java程序?大家最开始学习Java,都会觉得IDE调试好高端有木有,其实很简单了。下文会尽量简单直观的教会你在Eclipse中调试,其他的IDE调试步骤也是类似的。1.在你觉得有错的地方设置断点。在代码行数前,...

    如何调试Java程序?

    大家最开始学习Java,都会觉得IDE调试好高端有木有,其实很简单了。

    下文会尽量简单直观的教会你在Eclipse中调试,其他的IDE调试步骤也是类似的。

    1.在你觉得有错的地方设置断点。

    在代码行数前,点击右键,注意是右键,然后选择Toggle Breakpoint。

    你可能会问,我如何知道在哪儿放置断点?

    • 如果对这个问题完全没有感觉,你完全可以多打几个断点,单步调试直到找到异常,只是多花一点时间而已,而且这样可以更深入了解程序的执行过程!
    • 当然,如果你大致可以直到哪里可能会出问题,或者异常信息报告类位置,那么就可以在这里设置断点。

    2.点击Debug,如果是web程序,需要你将Tomcat或者Apache服务器以Debug模式启动

    这很重要,标准的Start模式,不能进入预先设置的断点,也就不能达到调试的目的。

    3.运行程序,当程序运行到刚才设置断点的位置就会停下来,并且那行代码底色会高亮显示。

     

    这时候,你可以通过屏幕按钮或者键盘控制程序的进行。

    下面是键盘对应调试的快捷键,如果不起作用,你可以检查一下是不是键盘冲突

    比如有道词典的快捷键,经常会和Debud模式下的Resume冲突。

    作用域 功能 快捷键 

    全局 单步返回 F7 
    全局 单步跳过 F6 
    全局 单步跳入 F5 
    全局 单步跳入选择 Ctrl+F5 
    全局 调试上次启动 F11 
    全局 继续 F8 
    全局 使用过滤器单步执行 Shift+F5 
    全局 添加/去除断点 Ctrl+Shift+B 
    全局 显示 Ctrl+D 
    全局 运行上次启动 Ctrl+F11 
    全局 运行至行 Ctrl+R 
    全局 执行 Ctrl+U

    4.进入调试界面可以看到你想要的信息。

    5.在Variables里面可以查看所有变量的值,比如刚才设置的断点里面的值,右键ChangeValue可以更改,部分IDE支持在窗口中热更改并执行代码。

     

    6.下面按钮第一个是进入方法执行,比如你调用了其他方法,可以进入方法一步一步执行,如果点击第二个按钮,只会在本方法内一步一步执行,第三个按钮时你跳出此方法,继续执行调用这个方法的原方法,说明如下。

     

    7.执行完程序。

    8.为Eclipse添加反编译插件,更好的调试

    一般来说,我们的项目或多或少的都会引用一些外部jar包,如果可以查看jar包的源代码,对于我们的调试可以说是事半功倍。

    1、下载并安装jad.exe。将jad.exe解压到程序目录(可以放置任意目录),例如:C:\Program Files\Jad\jad.exe。

    2、安装jadclipse插件。下载并解压net.sf.jadclipse_3.3.0.jar,将其拷贝到eclipse\plugins目录下,重新启动eclipse。

    3、配置jadclipse。在eclipse窗口下,点击Window > Preferences > Java > JadClipse > Path to Decompiler。 
    (设置jad的绝对路径,例如 C:\Program Files\Jad\jad.exe) 
    可将Use Eclipse code formatter(overrides Jad formatting instructions)选项打勾,这样可以与Ctrl+Shif+F格式化出来的代码样式一致。

    执行完这几个步骤,再在导入自Jar包的类或者方法上点击查看,就可以查看源代码了,如果不能,参考下面的解决办法:

    多数情况下,是eclipse未能自动将JadClipse Class File Viewer设置成class文件的缺省打开方式。
    在Eclipse的Windows——> Perference——>General->Editors->File Associations中修改“*.class”和“*.class without source”默认关联的编辑器为“JadClipse Class File Viewer”。
    曾经配置过几次jad插件,如果不能反编译,如此设置后,屡试不爽。

    展开全文
  • java调试_深入Java调试

    2020-05-11 06:30:11
    java调试 软件错误是开发过程中不可避免的一部分。 因此,Java调试是每个程序员都应具备的一项核心技能。 它不仅可以帮助您了解程序流程,还可以提高代码质量。 最后,它提高了您作为开发人员解决问题的能力,并...

    java调试

    软件错误是开发过程中不可避免的一部分。 因此,Java调试是每个程序员都应具备的一项核心技能。

    它不仅可以帮助您了解程序流程,还可以提高代码质量。 最后,它提高了您作为开发人员解决问题的能力,并最终提供了质量更高的软件。

    本文深入探讨了Java调试的一些最重要的技巧和技术。 在生产环境中调试Java时,我们还将介绍最佳实践。

    项目的Java调试

    本节汇总了调试Java项目时要使用的一些技巧和窍门,包括断点,步骤过滤器,拖放到框架等。 即使在最复杂的情​​况下,也可以应用这些方法。

    条件断点

    断点用于指定调试时程序执行应停止的点。 通过暂时中止执行,您可以调查程序的字段和变量,更改其值等等。 在本文中,示例在Eclipse上运行,但是相同的概念可以应用于其他Java IDE。

    条件断点设置为在满足特定条件时触发,以便您可以检查程序的状态,调用堆栈甚至是贵重物品的值。 在旨在计算音乐专辑评分或分数的几何平均值的程序中,我们可以设置条件断点。

    该程序一目了然并且运行良好,但是如果返回0,则其中一张专辑的评分可能为0。 在for循环中放置一个简单的条件断点意味着程序将停止太多次(想象一下,当我们有数百张专辑时)。

    在这种情况下,解决方案是将album.getRating().score == 0指定为断点的基本条件。 这将暂停在虚假或零评级专辑上的执行。

    更多类型的断点

    除了上面介绍的断点类型之外,还有其他类型的断点,但是这取决于您使用的Java IDE。 它们包括:

    • 基于事件的断点 -这些断点与事件相关,通常在遇到调试器识别的事件时触发。
    • 现场观察点 –这种类型的br
    • 每当给定字段或表达式的值更改时,eakpoint都会停止执行程序。 调试时,可以指定字段观察点以在读取,修改或同时读取和/或表达式时停止执行。
    • 方法断点 -用于在到达或退出指定方法或其实现时挂起程序。 这使您可以检查特定方法的进入或退出条件。
    • 行断点 -暂停执行
    • 到达断点中设置的特定代码行时的代码。

    使用转出使使用断点更容易

    Rookout是下一代调试平台,旨在帮助开发人员在程序代码中添加不间断的断点,以便他们可以收集所需的任何类型的数据。 它允许您指定规则,以完善何时或如何触发断点的条件。

    借助断点上的断点进行调试的一件很酷的事情是它的断点状态功能–一个简单的可视指示器,可提供有关断点行为方式的快速反馈。 断点状态显示为断点旁边(在右侧窗格中)的警告标志或灯泡。

    转换中的断点状态有五种类型,分别是:

    • 活动(纯紫色)
    • 待处理(空心紫色)
    • 警告(带三角形的纯紫色)
    • 错误(空心紫色,带有三角形)
    • 残障人士(空心灰色)

    断点状态功能使您甚至可以在收到日志行或调试消息之前就对程序有所了解。 此信息使您的调试更加容易。

    阶跃滤波器

    通过步骤筛选,可以定义在调试期间可以跳过的程序包。 当您需要从一个类转换到另一个类,访问外部库或使用框架而不测试框架的类时,这一点尤其重要。

    通过使用步骤过滤器功能限制转换,可以跳过特定的程序包。 在Eclipse中,可以通过路径窗口首选项>> Java调试>>步骤过滤来配置步骤过滤器

    放到框架

    此技术使您可以在调试过程中选择并重新运行程序的一部分。 您可以选择调用堆栈中的任何帧,并将调试器设置为从该点重新启动。

    要使用此功能,请在堆栈中选择一个框架或一个关卡,然后在“调试”视图中单击“ 放置到框架”按钮。

    尽管此功能执行某种后退或重置,但它不会影响您的字段或现有数据。 例如,如果您将一些条目写入数据库,然后下降到上一个级别或框架,则这些条目仍将保留在数据库中。

    远程调试

    大多数Java IDE(例如Eclipse)允许开发人员调试在另一台设备或Java虚拟机(JVM)上运行的应用程序。 但是,您将需要使用某些标志来启动Java程序。

    下面的代码片段演示了如何完成此操作:

    Eclipse用户可以输入主机名和端口号进行连接以进行远程调试 ,如下快照所示:

    在生产中调试Java

    当今的软件开发环境节奏如此之快,因此研发团队一直在努力通过将代码更快地投入生产来满足这些需求。 但是,无论您的测试过程有多严格,有时都会出错。

    此外,当代码投入生产且实际数据正在系统中流动时,情况可能会很快消失。 为了在系统扩展时保持领先地位,解决应用程序中的潜在漏洞很重要。

    每个Java开发人员都可以实施以下策略:

    • 识别何时发生错误
    • 评估错误对优先级的严重程度
    • 找出导致程序运行错误的状态
    • 接下来,追踪并解决根本原因
    • 最后,部署有效的修补程序

    除了使用上面的五步策略之外, 还应该遵循一些最佳的生产调试实践

    提升日志记录级别

    如果错误日志包含足够详细的消息,则调试应用程序会更容易。 在大多数情况下,错误消息不能提供足够的上下文,因此程序员应增加日志级别。

    这使您能够捕获整个上下文并了解到底是什么导致了错误。 每条日志行都应帮助您跟踪错误的根本原因。

    跟踪错误发生的一种实用但经常未充分利用的方法是在每个线程的应用程序入口点生成UUID。 理想情况下,您可以按如下所示格式化线程名称:

    因此,您的应用程序不会产生像“ pool-7-thread-22”之类的匿名名称,而是创建以“ threadName: pool-7-thread-22, UUID: EB85GTA, MsgType: AnalyzeGraph, MsgID: 415669, 29/03/2020 04:44 ”。

    集中日志

    在生产期间或应用程序生命周期的任何其他阶段处理错误时, 有效的日志记录机制应该是优先事项。

    在会话期间记录所有重要事件并将日志存储在集中式服务器中进行分析可以使调试更加容易。

    它还可以帮助您在跟踪关键产品指标时,关注应用程序出现问题的情况。

    检查堆栈跟踪和其他日志

    调试异常时, 堆栈跟踪会派上用场。 它们可以帮助您确定程序崩溃时调用了哪些函数。

    使用堆栈跟踪,更容易找出问题所在,因为您可以查看导致异常的调用顺序。

    下面的代码片段可以使用异常类中的printStack()方法打印异常堆栈:

    它产生以下结果:

    复制实例

    保留日志后,重要的是要复制情况/实例。 这可以通过创建类似的环境来完成,以便您可以在IDE中看到问题。

    重现情况有助于解决错误,因为您可以更好地理解代码和环境。

    结语

    Java调试过程不必是一场噩梦。 您只需要具有创新的心态并使用正确的工具即可。 淘汰工作就是这样一种工具,可以帮助开发人员加快调试过程。 它旨在帮助您以更高的置信度,速度和准确性来识别,诊断和解决代码中的错误。

    翻译自: https://hackernoon.com/diving-into-java-debugging-614q3y1c

    java调试

    展开全文
  • 开发建设开发建设开发建设开发建设开发建设开发建设开发建设开发建设开发建设开发建设开发建设开发建设开发建设开发建设开发建设开发建设开发建设开发建设开发建设开发建设开发建设开发建设开发建设开发建设开发建设...
  • java调试参数

    2012-01-08 22:30:47
    java调试参数
  • java调试技术

    2013-05-07 14:07:52
    java调试技术 帮你轻松解决java开发过程中的问题
  • JPDA(Java Platform Debugger Architecture) 是 Java 平台调试体系结构的缩写,通过 JPDA 提供的 API,开发人员可以方便灵活的搭建 Java 调试应用程序。JPDA 主要由三个部分组成:Java 虚拟机工具接口(JVMTI),...

    JPDA(Java Platform Debugger Architecture) 是 Java 平台调试体系结构的缩写,通过 JPDA 提供的 API,开发人员可以方便灵活的搭建 Java 调试应用程序。JPDA 主要由三个部分组成:Java 虚拟机工具接口(JVMTI),Java 调试线协议(JDWP),以及 Java 调试接口(JDI),本系列将会详细介绍这三个模块的内部细节、通过实例为读者揭开 JPDA 的面纱。本文是该系列的最后一篇,将会着重介绍 Java 调试接口 - JDI,以及如何使用 JDI 编写用户自定义的 Java 调试程序。

    JDI 简介

    JDI(Java Debug Interface)是 JPDA 三层模块中最高层的接口,定义了调试器(Debugger)所需要的一些调试接口。基于这些接口,调试器可以及时地了解目标虚拟机的状态,例如查看目标虚拟机上有哪些类和实例等。另外,调试者还可以控制目标虚拟机的执行,例如挂起和恢复目标虚拟机上的线程,设置断点等。

    目前,大多数的 JDI 实现都是通过 Java 语言编写的。比如,Java 开发者再熟悉不过的 Eclipse IDE,它的调试工具相信大家都使用过。它的两个插件 org.eclipse.jdt.debug.ui 和 org.eclipse.jdt.debug 与其强大的调试功能密切相关,其中 org.eclipse.jdt.debug.ui 是 Eclipse 调试工具界面的实现,而 org.eclipse.jdt.debug 则是 JDI 的一个完整实现。

    JDI 工作方式

    首先,调试器(Debuuger)通过 Bootstrap 获取唯一的虚拟机管理器,见 清单 1。

    清单 1. 获取虚拟机管理器(VirtualMachineManager)

    VirtualMachineManager virtualMachineManager 
        = Bootstrap.virtualMachineManager();

    虚拟机管理器将在第一次被调用时初始化可用的链接器。一般地,调试器会默认地采用启动型链接器进行链接,见 清单 2。

    清单 2. 获取默认的链接器(Connector)

    LaunchingConnector defaultConnector = virtualMachineManager.defaultConnector();

    然后,调试器调用链接器的 launch () 来启动目标程序,并完成调试器与目标虚拟机的链接,见 清单 3。

    清单 3. 启动目标程序,连接调试器(Debuuger)与目标虚拟机(VirtualMachine)

    VirtualMachine targetVM = defaultConnector.launch(arguments);

    当链接完成后,调试器与目标虚拟机便可以进行双向通信了。调试器将用户的操作转化为调试命令,命令通过链接被发送到前端运行目标程序的虚拟机上;然后,目标虚拟机根据接受的命令做出相应的操作,将调试的结果发回给后端的调试器;最后,调试器可视化数据信息反馈给用户。

    从功能上,可以将 JDI 分成三个部分:数据模块,链接模块,以及事件请求与处理模块。数据模块负责调试器和目标虚拟机上的数据建模,链接模块建立调试器与目标虚拟机的沟通渠道,事件请求与处理模块提供调试器与目标虚拟机交互方式,下面将逐一地介绍它们。

    JDI 数据模块

    Mirror

    Mirror 接口是 JDI 最底层的接口,JDI 中几乎所有其他接口都继承于它。镜像机制是将目标虚拟机上的所有数据、类型、域、方法、事件、状态和资源,以及调试器发向目标虚拟机的事件请求等都映射成 Mirror 对象。例如,在目标虚拟机上,已装载的类被映射成 ReferenceType 镜像,对象实例被映射成 ObjectReference 镜像,基本类型的值(如 float 等)被映射成 PrimitiveValue(如 FloatValue 等)。被调试的目标程序的运行状态信息被映射到 StackFrame 镜像中,在调试过程中所触发的事件被映射成 Event 镜像(如 StepEvent 等),调试器发出的事件请求被映射成 EventRequest 镜像(如 StepRequest 等),被调试的目标虚拟机则被映射成 VirtualMachine 镜像。但是,JDI 并不保证目标虚拟机上的每份信息和资源都只有唯一的镜像与之对应,这是由 JDI 的具体实现所决定的。例如,目标虚拟机上的某个事件有可能存在多个 Event 镜像与之对应,例如 BreakpointEvent 等。

    Mirror 实例或是由调试器创建,或是由目标虚拟机创建,调用 Mirror 实例 virtualMachine() 可以获取其虚拟机信息,如下所示。

    清单 4. 获取 Mirror 对象实例的虚拟机

    VirtualMachine virtualMachine = mirror.virtualMachine();

    返回的目标虚拟机对象实现了 VirtualMachine 接口,该接口提供了一套方法,可以用来直接或间接地获取目标虚拟机上所有的数据和状态信息,也可以挂起、恢复、终止目标虚拟机,详情见 图 1。

    这里写图片描述

    这样,调试器便可以获取目标虚拟机上的信息,维持与目标虚拟机间的通信,并且检查,修改和控制目标虚拟机上资源等。

    Value 和 Type

    Value 和 Type 接口分别代表着目标虚拟机中对象、实例变量和方法变量的值和类型。通过 Value 接口的 type(),可以获取该值对应的类型。JDI 中定义了两种基本的数据类型:原始类型(PrimitiveType)和引用类型(ReferenceType)。与其对应的数值类型分别是原始值(PrimtiveValue)和对象引用(ObjectReference)。Value 和 Type 的具体对应关系,请参见 表 1。

    表 1. JDI 中 Value-Type 的对照表

    (Value, Type)说明
    (PrimtiveValue, PrimtiveType)(ByteValue, ByteType)表示一个字节
    (CharValue, CharType)表示一个字符
    (ShortValue, ShortType)表示一个短整型数据
    (IntegerValue, IntegerType)表示一个整型数据
    (LongValue, LongType)表示一个长整型数据
    (FloatValue, FloatType)表示一个浮点型数据
    (DoubleValue, DoubleType)表示一个双精度浮点型数据
    (BooleanValue, BooleanType)表示一个布尔型数据
    (ObjectReference, ReferenceType)(ObjectReference, ReferenceType)表示目标虚拟机上的一个对象
    (ArrayReference, ArrayType)表示目标虚拟机上的一个数组
    (StringReference, ClassType)表示目标虚拟机上的一个字符串对象
    (ThreadReference, ClassType)表示目标虚拟机上的一个线程对象,有一套方法可以获得当前设置的断点,堆栈,也能挂起和恢复该线程等
    (ThreadGroupReference, ClassType)表示目标虚拟机上的一个线程组对象
    (ClassObjectReference, ClassType)表示目标虚拟机上的一个类的 java.lang.Class 实例
    (ClassLoaderReference, ClassType)表示目标虚拟机上的一个 ClassLoader 对象
    (VoidValue, VoidType)表示 void 类型

    PrimitiveType 包括 Java 的 8 种基本类型,ReferenceType 包括目标虚拟机中装载的类,接口和数组的类型(数组也是一种对象,有自己的对象类型)。ReferenceType 有三种子接口:ClassType 对应于加载的类,InterfaceType 对应于接口,ArrayType 对应于数组。另外,ReferenceType 还提供了一组方法,可以用来获取该类型中声明的所有变量、方法、静态变量的取值、内嵌类、运行实例、行号等信息。

    PrimtiveValue 封装了 PrimitiveType 的值,它提供一组方法可将 PrimtiveValue 转化为 Java 原始数据。例如,IntegerValue 的 value () 将返回一个 int 型数据。对应地,VirtualMachine 也提供了一组方法,用以将 Java 原始数据转化为 PrimtiveValue 型数据。例如 mirrorOf(float value) 将给定的 float 数据转化为 FloatValue 型数据。

    ObjectReference 封装了目标虚拟机中的对象,通过 getValue() 和 setValue() 方法可以访问和修改对象中变量的值,通过 invokeMethod() 可以调用该对象中的指定方法,通过 referringObjects() 可以获得直接引用该对象的其他对象,通过 enableCollection() 和 disableCollection() 可以允许和禁止 GC 回收该对象。

    TypeComponent

    TypeComponent 接口表示 Class 或者 Interface 所声明的实体(Entity),它是 Field 和 Method 接口的基类。Field 表示一个类或者实例的变量,调用其 type() 可返回域的类型。Method 表示一个方法。TypeComponent 通过方法 declaredType() 获得声明该变量或方法的类或接口,通过 name() 获得该变量或者方法的名字(对于 Field 返回域名,对于一般方法返回方法名,对于类构造函数返回 ,对于静态初始化构造函数返回 )。

    JDI 的链接模块

    链接是调试器与目标虚拟机之间交互的渠道,一次链接可以由调试器发起,也可以由被调试的目标虚拟机发起。一个调试器可以链接多个目标虚拟机,但一个目标虚拟机最多只能链接一个调试器。链接是由链接器(Connector)生成的,不同的链接器封装着不同的链接方式。JDI 中定义三种链接器接口,分别是依附型链接器(AttachingConnector)、监听型链接器(ListeningConnector)和启动型链接器(LaunchingConnector)。在调试过程中,实际使用的链接器必须实现其中一种接口。

    根据调试器在链接过程中扮演的角色,可以将链接方式划分为主动链接和被动链接。主动链接是较常见一种链接方式,表示调试器主动地向目标虚拟机发起链接。下面将举两个主动链接的例子:

    由调试器启动目标虚拟机的链接方式:这是最常见、最简单的一种链接方式。

    • 调试器调用 VirtualMachineManager 的 launchingConnectors() 方法获取所有的启动型链接器实例;
    • 根据传输方式或其他特征选择一个启动型链接器,调用其 launch() 方法启动和链接目标虚拟机;
    • 启动后,返回目标虚拟机的实例。

    更高级的,当目标虚拟机已处于运行状态时,可以采用调试器 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 类型参数名称说明
    LaunchingConnectorhome表示 java.home 的值,指向 JRE
    main表示所要执行的 Java 类的类名
    options表示使用的 Java 命令行参数
    suspend表示是否在启动目标虚拟机后挂起虚拟机
    AttachingConnector ListeningConnectorhostname表示被链接一端的地址
    port表示被链接一端的端口
    timeout表示等待链接的时间

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

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

    Map<String,Connector.Argument> defaultArguments 
        = connector.defaultArguments();

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

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

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

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

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

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

    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。
    这里写图片描述

    一个 JDI 的简单实例

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

    清单 8. HelloWorld 类文件

    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 导入的类和声明的全局变量

    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() 方法

    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() 的实现

    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。

    清单 12. SimpleDebugger 的 execute () 方法

    private static void execute(Event event) throws Exception {
        if (event instanceof VMStartEvent) {
            System.out.println("VM started");
            eventSet.resume();
        } else if (event instanceof ClassPrepareEvent) {
            ClassPrepareEvent classPrepareEvent = (ClassPrepareEvent) event;
            String mainClassName = classPrepareEvent.referenceType().name();
            if (mainClassName.equals("com.ibm.jdi.test.HelloWorld")) {
                System.out.println("Class " + mainClassName
                        + " is already prepared");
            }
            if (true) {
                // Get location
                ReferenceType referenceType = prepareEvent.referenceType();
                List locations = referenceType.locationsOfLine(10);
                Location location = (Location) locations.get(0);
    
                // Create BreakpointEvent
                BreakpointRequest breakpointRequest = eventRequestManager
                        .createBreakpointRequest(location);
                breakpointRequest.setSuspendPolicy(EventRequest.SUSPEND_ALL);
                breakpointRequest.enable();
            }
            eventSet.resume();
        } else if (event instanceof BreakpointEvent) {
            System.out.println("Reach line 10 of com.ibm.jdi.test.HelloWorld");
            BreakpointEvent breakpointEvent = (BreakpointEvent) event;
            ThreadReference threadReference = breakpointEvent.thread();
            StackFrame stackFrame = threadReference.frame(0);
            LocalVariable localVariable = stackFrame
                    .visibleVariableByName("str");
            Value value = stackFrame.getValue(localVariable);
            String str = ((StringReference) value).value();
            System.out.println("The local variable str at line 10 is " + str
                    + " of " + value.type().name());
            eventSet.resume();
        } else if (event instanceof VMDisconnectEvent) {
            vmExit = true;
        } else {
            eventSet.resume();
        }
    }

    最后列出了以上程序的运行结果,见 清单 13。

    清单 13. 运行结果

    VM started
    Class com.ibm.jdi.test.HelloWorld is already prepared
    Reach line 10 of com.ibm.jdi.test.HelloWorld
    The local variable str at line 10 is Hello world! of java.lang.String

    结束语

    本文介绍了 Java 调试接口(JDI)的体系结构和工作方式,JDI 是 JPDA 体系结构的调试器后端接口,开发人员通过使用它所提供的接口与 JDWP(Java 调试线协议)前端 Agent 通信,以这种方式访问和控制被调试的目标虚拟机。最后,本文希望能够帮助开发人员在无须掌握 JPDA 的技术细节的情况下,能够编写出实用、高效的 Java 调试器程序。

    展开全文
  • Visual Studio Code的Java调试
  • eclipse中java调试

    2011-06-12 21:36:23
    eclipse中java调试debug 总结
  • JPDA(JavaPlatformDebuggerArchitecture)是Java平台调试体系结构的缩写,通过JPDA提供的API,开发人员可以方便灵活的搭建Java调试应用程序。JPDA主要由三个部分组成:Java虚拟机工具接口(JVMTI),Java调试线协议...
  • JPDA主要由三个部分组成:Java虚拟机工具接口(JVMTI)、Java调试线协议(JDWP),以及Java调试接口(JDI)。本系列将会详细介绍这三个模块的内部细节,并通过实例为读者揭开JPDA的面纱。本系列的第1部分从整体上...
  • 本文内容包括:JDWP协议介绍...JPDA主要由三个部分组成:Java虚拟机工具接口(JVMTI),Java调试线协议(JDWP),以及Java调试接口(JDI),本系列将会详细介绍这三个模块的内部细节、通过实例为读者揭开JPDA的面纱。本
  • IE开发者工具 Firefox开发者工具 Chrome开发者工具(功能最强大) 课程目标 了解使用调试工具的好处 了解谷歌浏览器js调试工具的用法。 了解谷歌浏览器js调试工具的...了解java调试的小技巧 了解如何进行java远程调试
  • Java 调试技巧

    2014-09-16 23:42:32
    Eclipse调试方法入门,java debug技术
  • JPDA(Java Platform Debugger Architecture)是 Java 平台调试体系结构的缩写,通过 JPDA 提供的 API,开发人员可以方便灵活的搭建 Java 调试应用程序。 JPDA 主要由三个部分组成:Java 虚拟机工具接口(JVMTI),...
  • Java调试那点事

    千次阅读 2015-12-17 12:50:02
    来自阿里内部流出的一篇技术文章,关于java调试的心得。

    原文地址:http://yq.aliyun.com/articles/56?spm=5176.100238.yqhn2.7.NLnzoh

    应该是阿里的某个大牛写的,从内部流出的。感觉不错,与大家分享。


    该文章来自于阿里巴巴技术协会(ATA)精选文章。

    Java调试概述

    程序猿都调式或者debug过Java代码吧?都体会过被PM,PD,测试,业务同学们围观debug吧?说调试,先看看调试严格定义是什么。引用Wikipedia定义

    调试(De-bug),又称除错,是发现和减少计算机程序或电子仪器设备中程序错误的一个过程。调试的基本步骤:
    1. 发现程序错误的存在
    2. 以隔离、消除的方式对错误进行定位
    3. 确定错误产生的原因
    4. 提出纠正错误的解决办法
    5. 对程序错误予以改正,重新测试

    用调试的好处是我们就无需每次新测试都要重新编译了,不用copy-paste一堆的System.out.println(很low但很多时候很管用有没有?)。

    更多时候我们调试最直接简单的办法就是IDE,Java程序员用的最多的必然是Eclipse,Netbeans和IntelliJ也有各自忠实的粉丝,各有优劣。关于用IDE如何调试可以另起一个话题再讨论。
    ide

    除了IDE之外,JDK也自带了一些命令行调试工具也很方便。大家用的比较多的如下表所示:

    命令 描述
    jdb 命令行调试工具
    jps 列出所有Java进程的PID
    jstack 列出虚拟机进程的所有线程运行状态
    jmap 列出堆内存上的对象状态
    jstat 记录虚拟机运行的状态,监控性能
    jconsole 虚拟机性能/状态检查可视化工具


    具体用法可以参考JDK文档,这些大家在线上调试应用的时候用的也不少,比如一般线上load高的问题排查步骤是

    1. 先用top找到耗资源的进程
    2. ps+grep找到对应的java进程/线程
    3. jstack分析哪些线程阻塞了,阻塞在哪里
    4. jstat看看FullGC频率
    5. jmap看看有没有内存泄露

    但这个也不是今天的重点,那么问题来了(blue fly is the strongest):这些工具如何能获取远程Java进程的信息的?又是如何远程控制Java进程的运行的? 相信有不少人和我一样对这些工具的 实现原理 很好奇,本文就尝试介绍下各中缘由。

    Java调试体系JPDA简介

    Java虚拟机设计了专门的API接口供调试和监控虚拟机使用,被称为Java平台调试体系即Java Platform Debugger Architecture(JPDA)。JPDA按照抽象层次,又分为三层,分别是

    • JVM TI - Java VM Tool Interface
      • 虚拟机对外暴露的接口,包括debug和profile
    • JDWP - Java Debug Wire Protocol
      • 调试器和应用之间通信的协议
    • JDI - Java Debug Interface
      • Java库接口,实现了JDWP协议的客户端,调试器可以用来和远程被调试应用通信

    jpda

    用一个不是特别准确但是比较容易理解的类比,大家可以和HTTP做比较,可以推断他就是一个典型的C/S应用,所以也可以很自然的想到,JDI是用TCP Socket和虚拟机通信的,后面会详细再介绍。

    • IDE+JDI = 浏览器
    • JDWP = HTTP
    • JVMTI = RESTful接口
    • Debugee虚拟机= REST服务端

    和其他的Java模块一样,Java只定义了Spec规范,也提供了参考实现(Reference Implementation),但是第三方完全可以参照这个规范,按照自己的需要去实现其中任意一个组件,原则上除了规范上没有定义的功能,他们应该能正常的交互,比如Eclipse就没有用Sun/Oracle的JDI,而是自己实现了一套(由于开源license的兼容原因),因为直接用JDWP协议调用JVMTI是不会受GPL“污染”的。的确有第三方调试工具基于JVMTI做了一套调试工具,这样效率更高,功能更丰富,因为JDI出于远程调用的安全考虑,做了一些功能的限制。用户还可以不用JDI,用自己熟悉的C或者脚本语言开发客户端,远程调试Java虚拟机,所以JPDA真个架构是非常灵活的。

    JVMTI

    JVMTI是整个JPDA中最中要的API,也是虚拟机对外暴露的接口,掌握了JVMTI,你就可以真正完全掌控你的虚拟机,因为必须通过本地加载,所以暴露的丰富功能在安全上也没有太大问题。更完整的API内容可以参考JVMTI SPEC:

    • 虚拟机信息
      • 堆上的对象
      • 线程和栈信息
      • 所有的类信息
      • 系统属性,运行状态
    • 调试行为
      • 设置断点
      • 挂起现场
      • 调用方法
    • 事件通知
      • 断点发生
      • 异步调用

    在JPDA的这个图里,agent是其中很重要的一个模块,正是他把JDI,JDWP,JVMTI三部分串联成了一个整体。简单来说agent的特性有

    • C/C++实现的
    • 被虚拟机以动态库的方式加载
    • 能调用本地JVMTI提供的调试能力
    • 实现JDWP协议服务器端
    • 与JDI(作为客户端)通信(socket/shmem等方式)

    Code speak louder than words. 上个代码加注释来解释:

    // Agent_OnLoad必须是入口函数,类似于main函数,规范规定
    JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved)
    {
            ....
            MethodTraceAgent* agent = new MethodTraceAgent();
            agent->Init(vm);
            agent->AddCapability();
            agent->RegisterEvent();
            ...
    }
    
       /****** AddCapability():   init():  初始化jvmti函数指针,所有功能的函数入口 *****/
        jvmtiEnv* MethodTraceAgent::m_jvmti = 0;
        jint ret = (vm)->GetEnv(reinterpret_cast<void**>(&jvmti), JVMTI_VERSION_1_0);
    
        /****** AddCability(): 确认agent能访问的虚拟机接口  *****/
        jvmtiCapabilities caps;
        memset(&caps, 0, sizeof(caps));
        caps.can_generate_method_entry_events = 1;
        // 设置当前环境
        m_jvmti->AddCapabilities(&caps);
    
         /******  RegisterEvent(): 创建一个新的回调函数   *****/ 
        jvmtiEventCallbacks callbacks;
        memset(&callbacks, 0, sizeof(callbacks));
        callbacks.MethodEntry = &MethodTraceAgent::HandleMethodEntry;
        // 设置回调函数
        m_jvmti->SetEventCallbacks(&callbacks, static_cast<jint>(sizeof(callbacks)));
        // 开启事件监听
        m_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_METHOD_ENTRY, 0);
    
         /******  HandleMethodEntry: 注册的回调,获取对应的信息   *****/
         // 获得方法对应的类
         m_jvmti->GetMethodDeclaringClass(method, &clazz);
         // 获得类的签名
         m_jvmti->GetClassSignature(clazz, &signature, 0);
         // 获得方法名字
         m_jvmti->GetMethodName(method, &name, NULL, NULL);
    

    写好agent后,需要编译,并在启动Java进程时指定加载路径

    // 编译动态链接库
    g++ -w -I${JAVA_HOME}/include/ -I${JAVA_HOME}/include/linux MethodTraceAgent.cpp Main.cpp -fPIC -shared -o libAgent.so
    
    // 拷贝到 LD_LIBRARY_PATH
    export LD_LIBRARY_PATH=/home/xiaoxia/lib
    cp libAgent.so ~/lib
    
    // 运行测试效果,记得load编译的动态库
    javac MethodTraceTest.java
    java -agentlib:Agent=first MethodTraceTest
    



    Agent实现的动态链接库其实有两种加载方式:

    • 虚拟机启动初期加载 这个链接库必须实现Agent_OnLoad作为函数入口。这种方式可以利用的接口和功能更多,因为他在被调式虚拟机运行的应用初始化之前就被调用了,但是限制是必须以显示的参数指定启动方式,这在线上环境上是不大现实的。
     java -agentlib:<agent-lib-name>=<options> JavaClass
     //Linux从LD_LIBRARY_PATH找so文件, Windows从PATH找该DLL文件。
     java -agentpath:<path-to-agent>=<options> JavaClass
     //直接从绝对路径查找
    
    • 动态加载 这是更灵活的方式,Java进程可以正常启动,如果需要,通过Sun/Orale提供的私有Attach API可以连上对应的虚拟机,再通过JPDA方式控制,不过因为虚拟机已经开始运行了,所以功能上会有限制。我们比较熟悉的jstack等jdk工具就是通过这种方式做的,动态库必须实现Agent_OnAttach作为函数入口。如果有兴趣理解Attach机制细节的话,可以参考这个blog,简单来说,就是虚拟机默认起了一个线程(没错,就是jstack时看到Signal Dispatcher这货),专门接受处理进程间singal通知,当他收到SIGQUIT时,就会启动一个新的socket监听线程(就是jstack看到的Attach Listener线程)来接收命令,Attach Listener就是一个agent实现,他能处理很多dump命令,更重要的是他能再加载其他agent,比如jdwp agent。



    通过Attach机制,我们能自己非常方便的实现一个jinfo或者其他jdk tools,只需通过JPS获取pid,在通过attach api去load我们提供的agent,完整的jinfo例子也在附件里。
    jinfo

    import java.io.IOException;
    import com.sun.tools.attach.VirtualMachine;
    
    public class JInfo {
    
        public static void main(String[] args) throws Exception {
             String pid = args[0]; 
             String agentName = "JInfoAgent"; 
    
             System.out.printf("Atach to Pid %s, dynamic load agent %s \n", pid, agentName);
             VirtualMachine virtualMachine = com.sun.tools.attach.VirtualMachine.attach(pid);
             virtualMachine.loadAgentLibrary(agentName, null);
             virtualMachine.detach();
        }
    }
    

    JDWP

    JDWP 是 Java Debug Wire Protocol 的缩写,它定义了调试器(debugger)和被调试的 Java 虚拟机(debugee)之间的通信协议。他就是同过JVMTI Agent实现的,简单来说,他就是对JVMTI调用(输入和输出,事件)的通信定义。

    JDWP 有两种基本的包(packet)类型:命令包(command packet)和回复包(reply packet)。JDWP 本身是无状态的,因此对 命令出现的顺序并不受限制。而且,JDWP 可以是异步的,所以命令的发送方不需要等待接收到回复就可以继续发送下一个命令。Debugger 和 Debugee 虚拟机都有可能发送命令:

    • Debugger 通过发送命令获取Debugee虚拟机的信息以及控制程序的执行。Debugger虚拟机通过发送 命令通知 Debugger 某些事件的发生,如到达断点或是产生异常。
    • 回复是用来确认对应的命令是否执行成功(在包定义有一个flag字段对应),如果成功,回复还有可能包含命令请求的数据,比如当前的线程信息或者变量的值。从 Debugee虚拟机发送的事件消息是不需要回复的。

    下图展示了一个可能的实现方式,再次强调下,Java的世界里只定义了规范(Spec),很多实现细节可以自己提供,比如虚拟机就有很多中实现(Sun HotSpot,IBM J9,Google Davik)。
    jdwpagent

    一般我们启动远程调试时,都会看到如下参数,其实表面了JDWP Agent就是通过启动一个socket监听来接受JDWP命令和发送事件信息的,而且,这个TCP连接可以是双向的:

    // debugge是server先启动监听,ide是client发起连接
    agentlib:jdwp=transport=dt_socket,server=y,address=8000
    
    // debugger ide是server,通过JDI监听,JDWP Agent作为客户端发起连接
    agentlib:jdwp=transport=dt_socket,address=myhost:8000
    

    JDI

    JDI属于JPDA中最上层接口,也是Java程序员接触的比较多的。他用起来也比较简单,参考JDI的API Doc即可。所有的功能都和JVMTI提供的调试功能一一对应的(JVMTI还包括很多非调式接口,JDK5以前JVMTI是分为JVMDI和JVMPI的,分别对应调试debug和调优profile)。

    jdi

    还是用一个例子来解释最直接,大家可以看到基本的流程都是类似的,真个JPDA调试的核心就是通过JVMTI的 调用 和事件 两个方向的沟通实现的。

    import java.util.List;
    import java.util.Map;
    import com.sun.jdi.*;
    import com.sun.jdi.connect.*;
    import com.sun.jdi.event.*;
    import com.sun.jdi.request.*;
    
    public class MethodTrace {
        private VirtualMachine vm;
        private Process process;
        private EventRequestManager eventRequestManager;
        private EventQueue eventQueue;
        private EventSet eventSet;
        private boolean vmExit = false;
        //write your own testclass
        private String className = "MethodTraceTest";
    
        public static void main(String[] args) throws Exception {
    
            MethodTrace trace = new MethodTrace();
            trace.launchDebugee();
            trace.registerEvent();
    
            trace.processDebuggeeVM();
    
            // Enter event loop
            trace.eventLoop();
    
            trace.destroyDebuggeeVM();
    
        }
    
        public void launchDebugee() {
            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(className);
            suspendArg.setValue("true");
            try {
                vm = launchingConnector.launch(defaultArguments);
            } catch (Exception e) {
                // ignore
            }
        }
    
        public void processDebuggeeVM() {
            process = vm.process();
        }
    
        public void destroyDebuggeeVM() {
            process.destroy();
        }
    
        public void registerEvent() {
            // Register ClassPrepareRequest
            eventRequestManager = vm.eventRequestManager();
            MethodEntryRequest entryReq = eventRequestManager.createMethodEntryRequest();
    
            entryReq.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
            entryReq.addClassFilter(className);
            entryReq.enable();
    
            MethodExitRequest exitReq = eventRequestManager.createMethodExitRequest();
            exitReq.addClassFilter(className);
            exitReq.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
            exitReq.enable();
        }
    
        private 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);
                    if (!vmExit) {
                        eventSet.resume();
                    }
                }
            }
        }
    
        private void execute(Event event) throws Exception {
            if (event instanceof VMStartEvent) {
                System.out.println("VM started");
            } else if (event instanceof MethodEntryEvent) {
                Method method = ((MethodEntryEvent) event).method();
                System.out.printf("Enter -> Method: %s, Signature:%s\n",method.name(),method.signature());
                System.out.printf("\t ReturnType:%s\n", method.returnTypeName());
            } else if (event instanceof MethodExitEvent) {
                Method method = ((MethodExitEvent) event).method();
                System.out.printf("Exit -> method: %s\n",method.name());
            } else if (event instanceof VMDisconnectEvent) {
                vmExit = true;
            }
        }
    }
    
    

    总结

    整个JDPA有非常清晰的分层,各司其职,让整个调式过程简单可以扩展,而这一切其实都是构建在高司令巨牛逼的Java虚拟机抽象之上的,通过JVMTI将抽象良好的虚拟机控制暴露出来,让开发者可以自由的掌控被调试的虚拟机。有兴趣的同学可以运行下附近中的几个例子,应该会有更充分的了解。

    而且由于规范的灵活性,如果有特殊需求,完全可以自己去重新实现和扩展,而且不限于Java,举个例子,我们可以通过agent去加密解密加载的类,保护知识产权;我们可以记录虚拟机运行过程,作为自动化测试用例; 我们还可以把线上问题的诊断实践自动化下来,做一个快速预判 ,争取最宝贵的时间。


    展开全文
  • java调试

    千次阅读 2011-06-13 00:34:00
    Eclipse 平台的特色在于内置了 Java 调试器,该调试器提供所有标准调试功能,包括进行单步执行、设置断点和值、检查变量和值以及暂挂和恢复线程的能力。Eclipse 平台工作台(Eclipse Platform Workbench)及其工具是...
  • 可提高Java调试功能的5款工具

    千次阅读 2017-05-22 07:25:12
    可提高Java调试功能的5款工具
  • JPDA(Java Platform Debugger Architecture)是 Java 平台调试体系结构的缩写,通过 JPDA 提供的 API,开发人员可以方便灵活的搭建 Java 调试应用程序。 JPDA 主要由三个部分组成:Java 虚拟机工具接口(JVMTI),...
  • java调试命令jdb

    千次阅读 2017-03-26 13:29:46
    在命令行中可以使用jdb命令来进行类的调试:类Hello.java如下:class Hello{ public static void main(String[] args){ System.out.println("输出100/0的结果:"); System.out.println(100/0); } }在类目录下使用...
  • JPDA(Java Platform Debugger Architecture) 是 Java 平台调试体系结构的缩写,通过 JPDA 提供的 API,开发人员可以方便灵活的搭建 Java 调试应用程序。JPDA 主要由三个部分组成:Java 虚拟机工具接口(JVMTI),...
  • JPDA(Java Platform Debugger Architecture)是 Java 平台调试体系结构的缩写,通过 JPDA 提供的 API,开发人员可以方便灵活的搭建 Java 调试应用程序。 JPDA 主要由三个部分组成:Java 虚拟机工具接口(JVMTI),...
  • Java调试体系及协议

    2010-12-23 02:42:11
    主要描述Java调试体系,可以用作软件模拟调试软件设计的参考。
  • 刚开始学习java,编写一个输出HelloWorld程序,点击调试出错,是什么原因。但是可以运行。![图片说明](https://img-ask.csdn.net/upload/201712/31/1514685501_603536.png) 网上的说法包括关闭防火墙等都不好用还有 ...
  • Linux下Java调试方法

    千次阅读 2017-11-24 15:34:00
    1、如何开启一个Java进程的调试选项? 替换原有java程序: 1 mv/usr/bin/java/usr/bin/java_true 在/usr/bin/目录下生成一个java文件,其内容如下: 1 java_true-Xdebug-Xrunjdwp:transport=dt...
  • java调试,调试模式不进入内部类解决

    千次阅读 2017-03-15 13:06:18
    myeclipse 编辑java 代码是内部类 断点无法进入, 调试时将内部类方法的第一行断点可进入内部类调用的方法

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 488,752
精华内容 195,500
关键字:

java调试

java 订阅