写点什么

从业务代码到 Openjdk 源码的 debug 之路

用户头像
飞影
关注
发布于: 2020 年 06 月 29 日
从业务代码到Openjdk源码的debug之路

现状与意义

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 CLionLLDB调试器使用简介较为熟悉,可跳至文末,利用相关推荐教程进行实验。

远程 debug springboot 项目

新建一个简单的 springboot - web 项目

为了后续可控 debug 方便,我们需要一个简单的 springboot-web 项目,可以参考官方教程【SpringBoot-Quickstart 】或其他中文教程【例:使用Springboot2快速创建web项目】 ,我们快速搭建一个 springboot 项目,包含两个方法:输出系统时间、批量创建对象。目录结构如下:

HelloController.java 代码

@RestControllerpublic 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之逃逸分析以及TLABJVM源码分析之线程局部缓存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 可配置参数列表,更新参数后进行对应的实验探究。

参考资料:

SpringBoot-Quickstart

使用Springboot2快速创建web项目

RestfulToolkit(接口自测工具)

支持2020.1版本idea的restfultool插件

Intellij IDEA 自带逆天 restful 插件功能

idea工具将SpringBoot工程打包成 jar或war

Java 动态调试技术原理及实践

JPDA 体系概览

使用IDEA实现远程代码DEBUG调试教程详解

Intellij IDEA远程debug教程实战和要点总结

Debugging in CLion

LLDB调试器使用简介

Mac编译OpenJDK12

《深入理解Java虚拟机》学习之openjdk12环境搭建

OpenJDK系列(一):编译/调试与项目结构

手把手教你构建、debug、开发Java虚拟机

Java中的逃逸分析和TLAB以及Java对象分配

JVM之逃逸分析以及TLAB

JVM源码分析之线程局部缓存TLAB

JVM参数之查看JVM参数


文章同时发布于 CSDN:https://blog.csdn.net/suxiongye/article/details/107157502


发布于: 2020 年 06 月 29 日阅读数: 497
用户头像

飞影

关注

还未添加个人签名 2018.04.10 加入

还未添加个人简介

评论

发布
暂无评论
从业务代码到Openjdk源码的debug之路