现状与意义
Debug作为一种常见的调试和分析手段,被广泛应用于项目开发的全生命周期中。当前不乏采用IDE debug java项目的工具及教程,也不乏如何搭建一个可编译/调试的openjdk源码项目教程,但尚未发现一个完整的从项目源码debug至openjdk源码的中文教程,可能的原因包括:1、项目主要采用java实现、openjdk广泛使用c语言(如内存分配、锁机制、类加载等),两者应用的开发环境(IDE)和运行层次不同;2、虚拟机提供了相当多的命令足够满足排查日常分析内存溢出等问题需求的命令,如jmap、jstack等;3、实际开发中较少对两者紧密结合的需求不强,debug业务代码主要目的是发现代码异常,debug openjdk更多是为了探究jvm和jdk的运行原理(如各种垃圾回收机制等),从而实现底层性能优化或者单纯学习目的。
实际上,如果能够把对业务源码的debug和openjdk的debug进行串联起来,或许能够为研发人员提供对实际业务和jvm之间更加细粒度的观察,对寻求项目在不同运行阶段状态优化方案提供另一个视角。
针对类似需求,本文尝试将IDEA的远程debug和clion的debug相互连接,实现对一个简单springboot项目,从业务源码到jdk源码的debug,并观察其类加载、对象TLAB内存分配过程。
实现思路
核心是利用Remote debug作为沟通桥梁,没有其他特殊技巧,如果对相关技术【Java 动态调试技术原理及实践 、 JPDA 体系概览 、Debugging in CLion 、 LLDB调试器使用简介】较为熟悉,可跳至文末,利用相关推荐教程进行实验。
远程debug springboot项目
新建一个简单的springboot - web项目
为了后续可控debug方便,我们需要一个简单的springboot-web项目,可以参考官方教程【SpringBoot-Quickstart 】或其他中文教程【例:使用Springboot2快速创建web项目】 ,我们快速搭建一个springboot项目,包含两个方法:输出系统时间、批量创建对象。目录结构如下:
HelloController.java代码
@RestController
public class HelloController {
@GetMapping("/")
public String createObjects() {
return new Date().toString();
}
}
EscapeController.java代码 (EscapeTest对象可随意定义,默认表示某类业务对象)
@RestController
@RequestMapping("/tlab")
public class EscapeController {
@GetMapping("/createObjects")
public String createObjects() {
long start = System.currentTimeMillis();
for (int i = 0; i < 5_000_000; i++) {
new Object();
new EscapeTest();
}
return "cost = " + (System.currentTimeMillis() - start) + "ms";
}
}
启动并使用Restfultool验证,restfultool能够帮助我们快速构建项目接口关系树,提升研发效率【RestfulToolkit(接口自测工具)】。针对2020.1版本的IDEA不兼容问题,可通过 【支持2020.1版本idea的restfultool插件】进行安装。
打包成可执行jar包
通过mvn clean install 将项目打包成可执行jar包,可以参考【idea工具将SpringBoot工程打包成 jar或war】得到jar包,并获取绝对路径,如:/Users/xxx/Code/DebugJdk/target/debugjdk-0.0.1-SNAPSHOT.jar
检查是否可正常启动:
java -jar /Users/xxx/Code/DebugJdk/target/debugjdk-0.0.1-SNAPSHOT.jar
正常情况如下:
实现远程debug
利用JVM提供的远程通讯协议标准,可以在IDE上调试不同配置的远程服务。例如:本地机器没有某个资源访问权限或者本地机器内存不足,可以将服务部署到相关远程机器,然后开启debug端口,实现远程调试。原理可以参考 :使用IDEA实现远程代码DEBUG调试教程详解 、 Intellij IDEA远程debug教程实战和要点总结 。 其中核心配置为:-Xrunjdwp:server=y,transport=dt_socket,address=8099,suspend=n
可编译/调试的openjdk源码 - 以jdk12为例
《深入理解JVM编程》广泛引用openjdk源码,并且在第一章-1.6节介绍了如何编译jdk,实际编译和debug openjdk过程中可能遇到较多不同的问题,其中较好的教程推荐包括:
下面简单重复一下教程中核心步骤
环境准备
安装jdk11
jdk12需要jdk11作为编译base版本,下载地址: http://jdk.java.net/archive/ , 根据系统下载安装,安装后设置sdk : sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer/。
下载jdk12源码
下载地址源码:https://hg.openjdk.java.net/jdk/jdk12/
编译jdk源码
configure --enable-debug --with-jvm-variants=server
make images
成功后可以在
jdk12/build/macosx-x86_64-server-fastdebug/jdk/bin 找到编译出的 java命令,并用version验证
用编译的java命令启动本地项目
安装Clion
推荐通过Jetbrains Toolbox安装管理clion
配置Clion
添加默认的toolchains
打开jdk下载文件解压后的根目录,导入成c++项目,配置custom build targets
设置make和make clean命令,arguments为:CONF=macosx-x86_64-server-fastdebug、CONF=macosx-x86_64-server-fastdebug clean 。工作目录为jdk目录
配置启动项目参数,包括创建一个custom build application, 选择上一个步骤的target,executable选择编译出来的java命令。
参数为 -jar 项目jar包路径,例:
-jar /Users/suxiongye/Code/DebugJdk/target/debugjdk-0.0.1-SNAPSHOT.jar
如果进入LLDB断点,可以输入
process handle SIGSEGV --stop=false
process handle SIGBUS --stop=false
业务代码与Openjdk debug联通
在上述步骤后,实现两者之间的关联即水到渠成
在clion中启动项目并开启debug监听端口为8099,参数如下:
-Xdebug
-Xrunjdwp:server=y,transport=dt_socket,address=8099,suspend=n
-Xlog:gc
-Xms1000M
-Xmn1000M
-XX:-DoEscapeAnalysis
-XX:+UseTLAB
-XX:-ResizeTLAB
-jar
/Users/xxx/Code/DebugJdk/target/debugjdk-0.0.1-SNAPSHOT.jar
待完全启动后,idea中建立远程连接
点击send后,可以看见,成功实现远程debug,此时的虚拟机环境也处于debug状态
此时,我们可以根据业务情况,自行控制业务代码流程,同时观察jdk的变化。切记,如果项目有更新需要重新打包成jar : mvn install -Dmaven.test.skip=true
下文将以此为基础,同步观察业务代码运行中,虚拟机在TLAB和类加载的动作。
应用案例——观察类创建过程中触发TLAB分配机制
TLAB相关机制和原理可以参考 : Java中的逃逸分析和TLAB以及Java对象分配 、 JVM之逃逸分析以及TLAB 、 JVM源码分析之线程局部缓存TLAB 。
通过 -XX:+UseTLAB 可以控制是否适用TLAB, -XX:-ResizeTLAB 控制是否容许线程自行调整TLAB空间大小。启动服务后,发起请求,通过两边同时debug,探究服务运行中的tlab分配情况。
因此可以观察得到两个tlab不同参数在业务运行中的影响如下
服务完全启动后再打debug断点,避免非业务以外的springboot对象分配影响
+UseTLAB && +ResizeTLAB
优先TLAB,由于开启了自动调整大小,初始请求阶段,tlab容量较小,每次请求均先走线程内,然后走到线程外分配。后续请求次数增大后,可以发现,tlab自动扩大了容量,走tlab外的次数变得较不频繁,如果调大内存,则现象更明显。
其他实验参数,可以适当调整jvm内存
-Xms200M
-Xmn200M
+UseTLAB && -ResizeTLAB
优先TLAB,由于开启固定大小,tlab空间仅与内存设置有关系,而不会自动调节。每次请求,tlab会分配一块内存,用完后再分配一块,如果内存设置较小导致tlab空间较小,则会导致多次分配。从图中可以观察,每次分配一次TLAB后,从其中两次触发TLAB外内存分配断点可以看出,每块内存能够生成的对象数量基本一致。
第一次断点
第二次断点
-UseTLAB && +ResizeTLAB || -UseTLAB && -ResizeTLAB
取消TLAB,会直接走 allocate_outside_tlab , 线程外内存分配,resizeTLAB无效
观察对象内存分配
为了更细粒度观察某个对象的分配,可以关闭useTLAB,分别在Object和EscapeTest处打断点观察。
一般每个对象会触发多次内存分配,跟包含的变量及类型有关系,可以通过debug进行分析
利用Clion,双击变量,即可以看变量状态。
还可以通过跟踪以下类,查看类对象分配过程中做的工作以及更细粒度观察不同对象占用的空间情况
hotspot>share>gc>shared>collectedHeap.cpp
hotspot>share>oops>instanceKlass.cpp
hotspot>share>classfile>javaClasses.cpp
hotspot>share>services>classLoadingService.cpp
hotspot>share>prims>jvm.cpp
附:由于不同版本jvm支持命令不同,可以参考 JVM参数之查看JVM参数 ,通过 -XX:+PrintFlagsFinal 查看当前版本jvm可配置参数列表,更新参数后进行对应的实验探究。
文章同时发布于INFOQ:https://xie.infoq.cn/article/3eac3aacede75b4fb09029954
参考资料:
Intellij IDEA 自带逆天 restful 插件功能