十分钟!带你理解Java断点调试原理解析!
引言
平时我们都会使用IDE进行java的断点调试,你是否有去思考过,为什么我打下一个断点,程序就会停下来?JVM中发生了什么?本文带你打破砂锅问到底,揭开JVM断点调试的神秘面纱。
对于断点的使用方式,我们都不陌生:我们首先需要在IDE里标记上一行代码,然后程序会在执行到这行代码的时候停下,此时我们可以进行观察变量、单步执行、恢复执行等等操作。那么IDE是如何通知被调试的程序该什么时候停下来?被调试的程序又如何向IDE传递当前变量的信息?市面上形形色色的调试器,都支持这种调试方式,它们是如何被开发的?这就不得不提及JAVA的调试体系——JPDA。
JPDA
调试过程存在几个很自然的概念:调试器(debugger)、被调试者(debugee)、通信机制,这几个概念分别对应到了JPDA的三个层次:JAVA调试接口(JDI)、JAVA虚拟机工具接口(JVMTI)、JAVA调试线协议(JDWP)。
JDI(Java Debug Interface)
属于调试器的JDI,是三个层次中最高层的接口,它封装了丰富的Java API,来帮助调试器的开发人员可以快速地实现一个调试器,它不仅能帮助开发人员格式化 JDWP 数据,而且还能为 JDWP 数据传输提供队列、缓存等优化服务。
JVMTI(Java Virtual Machine Tool Interface)
JVMTI是JAVA SE 5开始,由虚拟机提供的一套native接口,通过这套接口可以控制和获知JVM的运行状态。
关于调试,只是JVMTI提供的功能之一。JVMTI具备四大块功能:调试、事件处理和回调函数、内存控制和对象获取、线程和锁。
JDWP(Java Debug Wire Protocol)
JDWP定义了调试器(debugger)和被调试的 Java 虚拟机(target vm)之间的通信协议。它只定义数据传输格式,不定义传输层如何实现(socket/共享内存)。
JPDA中的断点
写了这么多,究竟断点在JPDA中是如何体现的呢?
断点调试有三个个关键的场景:设置断点、恢复执行、断点触发,对于设置断点和恢复执行,在JPDA的层面来看,是十分相似的,所以这一章节,我们只挑一个来分析。
设置断点
对于设置断点,JDI提供了这样一个方法
com.sun.jdi.request.EventRequestManager#createBreakpointRequest(com.sun.jdi.Location location)
在JDI中调用这个方法后,数据通过JDWP传输到JVM,并调用JVMTI的设置断点的api:
JvmtiBreakpoint::set
一个大体的流程图是这样的:
断点触发
JVMTI向外回调的数据,会被JDWP封装为事件(Event)类型,断点的触发也不例外。当断点发生时,JVMTI就会触发一个JVMTI消息,调用JDWP中预先定义好的处理该事件的回调函数HandleBreakPoint,这个回调函数会生成一个JDWP所描述的断点事件来告知调试器。
这里还要谈到一个细节,断点在触发的时候,需要所有java线程挂起,这一步由谁来完成呢?
之前我们提到了HandleBreakPoint这个函数,我们说它是来处理断点的回调函数,是它的执行线程来完成的吗?显然不是,如果它先挂起所有线程,就无法再向调试器发送数据(因为自身也被挂起);如果它先发送数据,再挂起,就不能保证调试器收到信息的时候,所有java线程真的全被挂起了。
因此,JDWP提供了一个的线程来解决这个问题,叫EventDispatcher,它来进行挂起和发送信息。具体的过程是,JVMTI会把断点消息放到一个消息队列里,EventDispatcher来取,取出后,会根据事件中的设定,来挂起所要求的java线程,随之发出事件。
JVM中的断点
文章开头我们提到过,这篇文章是准备“打破砂锅问到底”的。在上一章节里,我们分析了断点事件在JPDA中的运作原理,但还是有很多问题没有解决。设置过断点之后,JVM中究竟发生了哪些变化?JVM在运行中,好比一个高速行驶的火车,他如何停下来,又如何恢复呢?
这些问题,需要深入源码来找到答案了。本章节以我们接触最多的HotSpot源码为例,解读HotSpot中,对于断点的管理。
特殊的字节码0xCA
在jvm的指令集中,有三个字节码是作为保留操作码,留给虚拟机内部使用的。
0xca breakpoint 调试时的断点标记
0xfe impdep1 为特定软件而预留的语言后门
0xff impdep2 为特定硬件而预留的语言后门
0xca作为一个断点标记,jvm在执行到这个字节码的时候,就代表触发了断点。
所以在设置断点的时候,jvm做的事情,就是根据断点对应的方法和字节码的位置,找到设置断点的位置,然后把正常的字节码替换为0xca,并保存好原来的字节码用于恢复。
特殊的字节码0xCA
在jvm的指令集中,有三个字节码是作为保留操作码,留给虚拟机内部使用的。
0xca breakpoint 调试时的断点标记
0xfe impdep1 为特定软件而预留的语言后门
0xff impdep2 为特定硬件而预留的语言后门
0xca作为一个断点标记,jvm在执行到这个字节码的时候,就代表触发了断点。
所以在设置断点的时候,jvm做的事情,就是根据断点对应的方法和字节码的位置,找到设置断点的位置,然后把正常的字节码替换为0xca,并保存好原来的字节码用于恢复。
断点管理
我们立刻就面临了另一个问题,这些被替换掉的字节码,是如何保存的?
HotSpot中,由BreakpointInfo类来管理单个断点的信息:
class BreakpointInfo : public CHeapObj{
private:
Bytecodes::Code _orig_bytecode; //原字节码
int _bci; //字节码偏移量,表示字节码在method中的位置
u2 _name_index;
u2 _signature_index; // 用于标识method
BreakpointInfo* _next;
}
一个类的所有断点,会作为一个链表,被保管在instanceKlass中。关于断点的操作和查询接口,被封装在Method类中,由Method去自身对应的instanceKlass中访问这个断点列表。
注:HotSpot的OOP-Klass模型中定义,虚拟机在加载class文件时,会在方法区对应的创建一个instanceKlass,表示其元数据,包括常量池、字段、方法等。这个instanceKlass,相当于java类在hotspot中的C++对等体。
这些底层能力,最终被JVMTI封装出来,暴露出JvmtiBreakpoint类,提供设置和清除断点的方法。另外,JVMTI还提供了JvmtiBreakpoints,主要进行批量操作和断点的缓存。
到此,我们完成了对HotSpot中断点管理的一个自下而上的分析。上文提到的C++类,它们之间的关系,如下图所示:
总结
我们已经介绍了在java中断点的原理。还有很多细节问题就不一一叙述了,价值不大。
对于其他语言来说,虽然实现方式会有所不同,但是断点的大体思想是差不多的。比如在X86系统中就是指令INT 3,用它来代替我们希望停止的指令。
写这篇文章,也是由于网上对于jvm断点的原理解释很少,反观C++断点调试原理的文章有很多,可能java天生为我们提供了JVM这样一个强大的平台,导致java程序员接触底层代码的机会确实比C++程序员少很多
「作者简介」
邵一哲
网易服务端开发,16年加入网易严选,深度参与了网易严选渠道销售领域从0到1的建设过程,也负责过严选权限中心、消息组件、文件等严选技术基础设施。
关注 分享 在看,就是所长最大的生产力!