详解Java DEBUG的基本原理

作者:京东云开发者-京东保险 蒋信
debug 的时候,都遇到过手速太快,直接跳过了自己想调试的方法、代码的时候吧……
一旦跳过,可能就得重新执行一遍,准备数据、重新启动可能几分钟就过去了。
好在 ide 们都很强大,还给你后悔的机会,可以直接删除某个 stack frame,直接返回到之前的状态,确切的说是返回到之前的某个 stack frame,从而实现让程序 “逆向运行”。
这个 reset frame 的能力,可不只是返回上一步,上 n 步也是可以的;选中你期望的那个帧,直接 reset frame/drop frame,可以直接回到调用栈上的某个栈帧,时间反转!
可惜这玩意也不是那么万能,毕竟是通过 stack pop 这种操作实现,实际上只是给调用栈栈顶的 n 个 frame pop 出来而已,还谈不上是真正的 “反向 debug”。
相比之下, gdb 的 reverse debugging 就比较强大,真正的 “反向” debug,逆向运行,实现回放。
所以吧在运行过程中,已经修改的数据,比如引用传递的方法参数、变量,一旦修改肯定回退不了,不然真的成时光机了。
这些乱七八糟的调试功能,都是基于 java 内置的 debug 体系来实现的。
java debug 体系
java 提供了一个完整的 debug 体系 jpda(java platform debugger architecture),这个 jpda 架构体系由 3 部分组成:
jvm ti- java vm tool interface
jdwp- java debug wire protocol
jdi- java debug interface
如果结合 ide 来看,那么一个完整的 debug 功能看起来就是这个样子:
解释一下这个体系:
jvm ti 是一个 jvm 提供的一个调试接口,提供了一系列控制 jvm 行为的功能,比如分析、调试、监控、线程分析等等。也就是说,这个接口定义了一系列调试分析功能,而 jvm 实现了这个接口,从而提供调试能力。
不过吧,这个接口毕竟是 c++ 的,调用起来确实不方便,所以 java 还提供了 jdi 这么个 java 接口。
jdi 接口使用 jdwp 这个私有的应用层协议,通过 tcp 和目标 vm 的 jvmti 接口进行交互。
也可以把简单这个 jdwp 协议理解为 jsf/dubbo 协议;相当于 ide 里通过 jdi 这个 sdk,使用 jdwp 协议调用远程 jvmti 的 rpc 接口,来传输调试时的各种断点、查看操作。
可能有人会问,搞什么套壳!要什么 jdwp,我直接 jvmti 调试不是更香,链路越短性能越高!
当然可以,比如 arthas 里的部分功能,就直接使用了 jvmti 接口,要什么 jdi!直接 jvmti 干就完了。
开个玩笑,arthas 毕竟不是 debug 工具,人家根本就不用 jdi 接口。而且 jvmti 的能力也不只是断点,它的功能非常多:
左边的功能类,提供了各种乱七八糟的功能,比如我们常用的添加一个断点:
jvmtierrorsetbreakpoint(jvmtienv* env, jmethodid method, jlocation location)  
右边的事件类,可以简单的理解为回调;还是拿断点举例,如果我用上面的 setbreakpoint 添加了一个断点,那么当执行到该位置时,就会触发这个事件:
void jnicallbreakpoint(jvmtienv *jvmti_env, jnienv* jni_env, jthread thread, jmethodid method, jlocation location)jvmti 的功能非常之多,而 jdi 只是实现了部分 jvmti 的方法,所以某些专业的 profiler 工具,可能会直接使用 jvmti,从而实现更丰富的诊断分析功能。  
远程调试与本地调试
不知道大家有没有留意过本地 debug 启动时的日志: 第一行是隐藏了后半段的启动命令,展开后是这个样子:
/path/to/java -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:53631,suspend=y,server=n -javaagent:/path/to/jetbrains/debugger-agent.jar ...第二行是一个 connected 日志,意思是使用 socket 连接到远程 vm 的 53631 端口 上一段说到,ide 通过 jdi 接口,使用 jdwp 协议和目标 vm 的 jvmti 交互。这里的 53631 端口,就是目标 jvm 暴露出的 jvm ti 的 server 端口。 而第一行里,idea 自动给我们加上了 -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:53631 这么一段,这个参数的意思就是,让 jvm 以 53631 暴露 jdwp 协议 小知识,这个 agentlib 可不只是为 jvmti 提供的。它还可以让 jvm 加载其他的 native lib 包,直接 “外挂” 到你的 jvm 上,下面是 “外挂” 的参数格式: 所以吧,上面的描述其实不太严谨,更专业的说法是: 让 jvm 加载 jdwp 这个 agent 库,参数为 transport=dt_socket,address=127.0.0.1:53631,这个 jdwp agent 库以 53631 端口提供了 jdwp 协议的 server。只不过这个 jdwp 是 jvm 内部的库,不需要额外的 so/dylib/dll 文件。 如有需要,你完全可以弄个 “datupiao” 的 agentlib,“外挂” 到这个 jvm 上,然后在这个 lib 里调用 jvmti 接口,然后暴露个端口提供服务和远程交互,实现自己的 jdwp! 可能某些老板们注意到了,本地调试还要 127.0.0.1 走 tcp 交互一遍,那远程调试呢? 基于上面的解释,本地调试和远程调试真的没啥区别!或者说,在目前 idea/eclipse 的实现下,不存在本地调试,都是远程!只不过一个是 127.0.0.1,一个是远程的 ip 而已。 在本地调试时,idea 会自动给我们的 jvm 增加 agent 参数,随机指定一个端口,然后通过 jdi 接口连接,代码大概长这样(jdi 的 sdk 在 jdk_home/lib/tools.jar ):map env = connector.defaultarguments();env.get(hostname).setvalue(hostname);env.get(port).setvalue(port);virtualmachine vm = connector.attach(env);  
瞅瞅, virtualmachine 里的就这点方法,能力上比 jvmti 还是差远了
list classesbyname(string classname);list allclasses();void redefineclasses(map classtobytes);list allthreads();void suspend();void resume();list toplevelthreadgroups();eventqueue eventqueue();eventrequestmanager eventrequestmanager();voidvalue mirrorofvoid();process process();再回来看看 idea 中独立的远程调试,配置好之后,红框里的信息会提示你 ,远程的 jvm 需增加这一段启动参数,而且支持多个版本 jdk 的格式,cv 大法就能直接用。  
-agentlib 和 -javaagent
有些细心的同学可能发现了,idea 默认的启动脚本里,同时配置了 -agentlib 和 -javaagent。
-javaagent:/path/to/jetbrains/debugger-agent.jar这个 debugger-agent 吧,其实也没干啥事,只是对 jdk 内置的一些线程做了些增强,辅助 idea 的 debug 功能,支持一些异步的调试。 agentlib、javaagent 这俩兄弟,定位其实很像,都是加载自定义的代码。 不过区别在于,agentlib 是加载 native lib,需要 c/cpp 去写,相当于外挂自己的代码在 jvm 上,可以为所欲为,比如在 agentlib 里调用上面说的 jvmti 。 而 javaagent 是用 java 写的,可以直接用上层的 instrumentation api,做一些类的增强转换之类,这也是大多数 apm agent、profiler agent 实现的基本原理。  
arthas 的玩法
arthas 的核心入口,其实还是 javaagent,支持静态加载和动态加载两种玩法。 静态没啥好说的,启动脚本里增加一个 -javaagent:/tmp/test/arthas-agent.jar,然后为所欲为。 动态的叫 attach,使用 java 提供的 virtualmachine 就可以实现运行时添加 -javaagent,效果一样:
virtualmachine virtualmachine = virtualmachine.attach(virtualmachinedescriptor);virtualmachine.loadagent(agentpath, agentargs);这个 agent 在 jvm 里启动了一个 tcp server,用于收发 arthas client 的各种 trace、watch 、dashboard 等指令,然后通过 instrumentation 增强 class 插入代码、或者直接调用某些 java api,实现各种功能。 注意到了吗?arthas 可以直接下载一个 jar 包,java -jar 就能连上。 其实吧,它这个直接启动的 jar 包,是一个 boot 包,启动之后把乱七八糟的 jar 都下载下来。接着动态 attach 的方式,连接到本机指定进程号的 jvm,然后再为所欲为。 在 3.5 版本之后,arthas 还新增了一个 vmtool 命令,这个命令可以直接获取内存中的指定对象实例。$ vmtool --action getinstances --classname java.lang.string --limit 10@string[][ @string[com/taobao/arthas/core/shell/session/session], @string[com.taobao.arthas.core.shell.session.session], @string[com/taobao/arthas/core/shell/session/session], @string[com/taobao/arthas/core/shell/session/session], @string[com/taobao/arthas/core/shell/session/session.class], @string[com/taobao/arthas/core/shell/session/session.class], @string[com/taobao/arthas/core/shell/session/session.class], @string[com/], @string[java/util/concurrent/concurrenthashmap$valueiterator], @string[java/util/concurrent/locks/locksupport],]直接获取内存对象,这玩意只靠 instrumentation api 可做不到。arthas 搞了个骚操作,直接 jni 调用自定义 lib,用过 cpp 直接调用了 jvmti 的 api,融合了 instrumentation 和 jvmti 的能力,这下是真的为所欲为了!#include #include #include #include #include arthas_vmtool.h // under target/native/javah/static jvmtienv *jvmti;...extern cjniexport jobjectarray jnicalljava_arthas_vmtool_getinstances0(jnienv *env, jclass thisclass, jclass klass, jint limit) { jlong tag = gettag(); limitcounter.init(limit); jvmtierror error = jvmti->iterateoverinstancesofclass(klass, jvmti_heap_object_either, heapobjectcallback, &tag); if (error) { printf(error: jvmti iterateoverinstancesofclass failed!%u, error); return null; } jint count = 0; jobject *instances; error = jvmti->getobjectswithtags(1, &tag, &count, &instances, null); if (error) { printf(error: jvmti getobjectswithtags failed!%u, error); return null; } jobjectarray array = env->newobjectarray(count, klass, null); //添加元素到数组 for (int i = 0; i setobjectarrayelement(array, i, instances[i]); } jvmti->deallocate(reinterpret_cast(instances)); return array;}  
总结
debug 基于 jdpa 体系
ide 直接接入 jdpa 体系中的 jdi 接口完成
jdi 通过 jdwp 协议,调用远程 vm 的 jvmti 接口
jdwp 是通过 agentlib 加载的,agentlib 算是一个 native 的静态 “外挂” 接口
javaagent 是 java 层面的 “外挂” 接口,用过 instrumentation api(java)实现各种功能,主要用于 apm、profiler 工具
如果你想,在 javaagent 里调用功能更丰富的 jvmti 也不是不行。


DS2141A , DS2143三个频道塞绳和插入-DS21
SEMTECH可用于路由器之高效率电源降压调节器解决方案
智慧农业小型气象站功能特点的详细介绍
大金与NEC开发出防止员工犯困的空调管理技术,力争2年以内推向实用
中国联通沃3G开放黑莓BIS业务注册页面
详解Java DEBUG的基本原理
区块链迎来寒冬,如何在熊市中存活?剩者才是王者
5G时代的技术开发和投资能否扩大企业销售额?
为什么会有多种不同的无线通信技术,它们的区别是什么
普源精电(RIGOL)高新技术荣获2022年中国仪器仪表学会科学技术奖!
汽车元件EMI抗扰性测试最佳方法大探秘
互联网+医疗都经历了哪些演变
运动用什么降噪耳机好?适合运动用的降噪耳机推荐
蜂窝移动连接对牲畜应用的挑战
物联网发展 人类与工具的交互方式实现突破
边缘会不会最后吃掉云呢
eNSP—BGP综合实验
颜值大帝华为荣耀8和锋芒毕露华为麦芒5,谁才是华为真旗舰
华为余承东表示没有贸易战就会是全球排名第一的手机厂家
高通发布业界首款集成式2x2 802.11ax解决方案