从java的远程debug说起
java开发中,我们最常见的是java的远程debug,配置了下述参数,就可以实现远程像本地一样代码一行一行执行,并且能够看到相关上下文的数据。
-Xdebug -Xrunjdwp:transport=dt_socket,suspend=n,server=y,address=9999
Java远程调试的原理是两个VM之间通过debug协议进行通信,然后以达到远程调试的目的,两者之间可以通过socket进行通信。我们知道,Java 程序都是运行在 Java虚拟机上的,我们要调试 Java程序,事实上就需要向 Java 虚拟机请求当前运行态的状态,并对虚拟机发出一定的指令,设置一些回调等等,那么Java的调试体系,就是虚拟机的一整套用于调试的工具和接口。早期使用 JVMDI 和 JVMPI 进行远程联调,后期使用JVMTI,JVMTI 是一个双路接口,支持 JVM 与本机代理程序之间进行通信。它取代了 JVMDI 和 JVMPI 接口。随着时代的变迁,我们目前只关注JVMTI。
在介绍JVMTI之前,需要先了解下Java平台调试体系JPDA(Java PlatformDebugger Architecture)。它是Java虚拟机为调试和监控虚拟机专门提供的一套接口。如下图所示,JPDA被抽象为三层实现。其中JVMTI就是JVM对外暴露的接口。JDI是实现了JDWP通信协议的客户端,调试器通过它和JVM中被调试程序通信。
JVMTI 本质上是在JVM内部的许多事件进行了埋点。通过这些埋点可以给外部提供当前上下文的一些信息。甚至可以接受外部的命令来改变下一步的动作。外部程序一般利用C/C++实现一个JVMTIAgent,在Agent里面注册一些JVM事件的回调。当事件发生时JVMTI调用这些回调方法。Agent可以在回调方法里面实现自己的逻辑。JVMTIAgent是以动态链接库的形式被虚拟机加载的。
JVMTI处于整个JPDA 体系的最底层,所有调试功能本质上都需要通过 JVMTI 来提供。从大的方面来说,JVMTI 提供了可用于 debug 和profiler 的接口;同时,在 Java 5/6 中,虚拟机接口也增加了监听(Monitoring),线程分析(Thread analysis)以及覆盖率分析(Coverage Analysis)等功能。从小的方面来说包含了虚拟机中线程、内存、堆、栈、类、方法、变量,事件、定时器处理等等诸多功能。具体可以参考oracle 的文档:https://docs.oracle.com/en/java/javase/18/docs/specs/jvmti.html。通过这些接口,开发人员不仅可以调试在该虚拟机上运行的 Java 程序,还能查看它们运行的状态,设置回调函数,控制某些环境变量,从而优化程序性能。
本文的目的不在于怎么debug项目,在于关注在整个debug项目的过程中发生了什么。同样的,java提供的原生工具:jmap, jps,jstack也是类似的原理。我们进一步抽象简化,可以得出以下结论
- jvmti是通过向jvm注入钩子函数的形式,无论在启动前还是启动后,都是可以无缝拿到对应jvm的运行情况,这些运行情况包括了每一步代码执行时的出入参,上下文信息等
- 进一步可以认为jvmti是jvm层面的监控或者aop,可以实时获取相关代码运行情况
基于此,我们提出一个问题,这些都是运行时的数据,能否将其保存下来,在本地进行重放呢?一方面可以让我们在项目重构的时候,确保自己代码是改正确的,另一方面,新的业务开发,影响范围评估可以精确到具体的接口。此时,我们就需要定制性的实现自己的jvmti,也即jvm沙箱技术。
jvm沙箱技术之类增强技术
我们从上文分析,可以得知,需要重点关注业务的运行时数据,那么目标可以进一步精确到java的运行时类/对象。因此我们需要想办法用类似sping的aop技术把java类包裹一层,拦截出入参并记录起来即可。这个技术,我们称作jvm沙箱技术。