从业务代码到 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代码

@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之逃逸分析以及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可配置参数列表,更新参数后进行对应的实验探究。

文章同时发布于INFOQ:https://xie.infoq.cn/article/3eac3aacede75b4fb09029954

参考资料:

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参数

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值