首页标签分类
007 JAVA中的远程调试
2025-08-28 · 更新 2026-03-03约 6 分钟 · 1507 字
开发工具包
000

目录

JAVA中的远程调试
远程调试的目的
原生java远程调试功能
实际操作
ARTHAS的使用
安装和启动
场景1 dashboard 查看jvm整体情况
场景2 thread 线程情况
场景3 jad 查看线上代码实际情况
场景4 watch 查看方法的入参和出参
场景5 trace 查看一个方法的栈调用耗时
场景6 stack 查看方法栈调用
场景7 tt 场景回放

JAVA中的远程调试

远程调试的目的

往往很多情况本地调试和真实线上调试测试的结果是不一致的,真实调用线上的执行环境最能体现实际情况,配置java的远程调试功能可以现实这一需求。当一般线上环境的申请步骤往往非常复杂,包括权限申请,风险评估一系列的步骤,周期往往更长。

原生java远程调试功能

Java 远程调试的核心是 Java 平台调试器体系结构(Java Platform Debugger Architecture, JPDA)。JPDA 由三个主要部分组成:

  • Java 虚拟机工具接口(JVMTI):这是虚拟机提供的一套用于调试和监控的接口。
  • Java 调试线协议(JDWP):该协议定义了被调试的 Java 应用程序(debuggee)和调试器(debugger)之间的通信格式。
  • Java 调试接口(JDI):这是一套高级 Java API,供调试工具的开发者使用,以方便地与远程的被调试虚拟机进行交互

调试的步骤:

  1. 在远程服务器上启动 Java 应用程序,并附加特定的 JVM 参数,使其在指定的端口上监听来自调试器的连接请求。
  2. 在本地的集成开发环境(IDE)中,配置一个远程调试会话,指定远程服务器的 IP 地址和端口号。
  3. IDE 通过 JDWP 协议连接到远程正在运行的 Java 应用程序。
  4. 连接建立后,您就可以像调试本地应用程序一样,在远程应用程序中设置断点、单步执行代码、检查变量值等。

实际操作

需要注意的是,用于远程debug的代码必须与远程部署的代码完全一致,不能发生任何的修改,否则打上的断点将无法命中

服务端

bash
自动换行:关
放大阅读
展开代码
java -Denv=dev -jar -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:9999 ./blogactions.jar

参数说明:

  • -agentlib
    的一个参数,用于加载一个原生代理库 (native agent library)。代理库是一种可以介入 JVM 运行过程的底层模块,可以用来做性能分析、监控、以及我们这里的调试
  • transport=dt_socket: 指定调试器和被调试 JVM 之间通信所使用的传输方式
  • server=y
    ,哪一方是服务器(监听方),哪一方是客户端(主动连接方)。
  • suspend=n
    JVM 在启动后是否立即暂停 (suspend),以等待调试器连接
  • ddress=*
    指定了 JVM 监听调试连接的地址和端口,*表示允许任一主机链接调试,9999表示监听的端口,在需要被外部访问时,需要注意防火墙开启

不同的jdk版本参数大致相同,可以参考: Connection and Invocation Details

当出现 Listening for transport dt_socket at address:字样表示服务端调试服务启动成功

bash
自动换行:关
放大阅读
展开代码
[hedeoer@centos79 test]$ java -Denv=dev -jar -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:9999 ./blogactions.jar Listening for transport dt_socket at address: 9999

客户端调试工具,此处以Intellij IDEA为例

image-20250828152634509

Transport(通信方式):有Socket和shared memory两种,最常用、最标准的调试通信方式。IntelliJ IDEA 和被调试的 Java 应用程序会通过网络套接字 (TCP/IP) 进行通信;其次共享内存是一种进程间通信 (Inter-Process Communication, IPC) 的方式,因此调试器和被调试的应用程序必须运行在同一台物理机器上。

allow multiple instances :当勾选时,则当你使用这个配置点击 "Run" 或 "Debug" 按钮启动一个实例后,这个实例会开始运行。此时,如果你再次点击 "Run" 或 "Debug" 按钮,IntelliJ IDEA 不会停止第一个实例,而是会直接启动一个全新的、独立的第二个实例

开启调试前,需要设置需要调试断点: image-20250828152741950

出现idea出现如下字样表示调试链接成功: image-20250828152831313

ARTHAS的使用

arthas的github地址

工具定位:

image-20250828170228448

安装和启动

plaintext
自动换行:关
放大阅读
展开代码
安装直接下载对应文件jar或者系统安装包(deb,rpm等)即可

启动

bash
自动换行:关
放大阅读
展开代码
java -jar ./arthas-boot.jar

arthas 4.0.5文件目录说明

bash
自动换行:关
放大阅读
展开代码
. # Arthas 的根目录 ├── arthas-agent.jar # Arthas的Java Agent实现,负责注入目标JVM并进行字节码增强。 ├── arthas-bin.zip # 你下载的Arthas完整二进制发行版的压缩包。 ├── arthas-boot.jar # Arthas的启动器,通过 `java -jar arthas-boot.jar` 运行,用于发现并附加(attach)到目标Java进程。 ├── arthas-client.jar # Arthas的命令行客户端,提供与用户交互的界面。 ├── arthas-core.jar # Arthas的核心实现库,包含了所有诊断命令的逻辑。 ├── arthas.properties # Arthas的配置文件,用于修改默认设置。 ├── arthas-spy.jar # "间谍"jar包,用于解决目标应用和Arthas之间的ClassLoader隔离问题。 ├── as.bat # Windows系统下的便捷启动脚本。 ├── as-service.bat # Windows系统下将Arthas作为服务运行的脚本。 ├── as.sh # Linux/macOS系统下的便捷启动脚本。 ├── async-profiler/ # 存放async-profiler工具的目录,为`profiler`火焰图命令提供底层支持。 ├── libasyncProfiler-linux-arm64.so # 适用于Linux ARM64架构的原生探查库。 ├── libasyncProfiler-linux-x64.so # 适用于Linux x86_64架构的原生探查库。 └── libasyncProfiler-mac.dylib # 适用于macOS的原生探查库。 ├── install-local.sh # 在本地安装Arthas的脚本,方便在任何路径下启动。 ├── lib/ # 存放Arthas自身所需的JNI(Java Native Interface)原生库的目录。 ├── libArthasJniLibrary-aarch64.so # 适用于Linux ARM64架构的Arthas原生库。 ├── libArthasJniLibrary.dylib # 适用于macOS的Arthas原生库。 ├── libArthasJniLibrary-x64.dll # 适用于Windows x86_64架构的Arthas原生库。 └── libArthasJniLibrary-x64.so # 适用于Linux x86_64架构的Arthas原生库。 ├── logback.xml # 日志框架Logback的配置文件,控制Arthas自身的日志输出。 └── math-game.jar # 一个用于演示和学习的Java示例程序,方便用户快速上手练习Arthas命令。

搭配 Itellij idea使用,需要安装插件arthas idea

image-20250828171558895

安装该插件后,只要保持本地代码和服务器运行代码一致,通过该插件生成arthas相关命令,可以快速调试线上环境。比如:

bash
自动换行:关
放大阅读
展开代码
trace -E cn.hedeoer.actions.handler.RepeatCommitHandler handleRepeatCommit -n 5 --skipJDKMethod false '1==1'

image-20250828171805323

场景1 dashboard 查看jvm整体情况

bash
自动换行:关
放大阅读
展开代码
dashboard

image-20250828171950917

可以查看java 线程级别各个线程的运行状态,占用的cpu情况;内存占用情况,以及整体jvm信息

场景2 thread 线程情况

bash
自动换行:关
放大阅读
展开代码
thread -n 3

找出最忙的前3个线程:

bash
自动换行:关
放大阅读
展开代码
[arthas@15017]$ thread -n 3 "arthas-command-execute" Id=97 cpuUsage=0.12% deltaTime=0ms time=39ms RUNNABLE at java.management@11/sun.management.ThreadImpl.dumpThreads0(Native Method) at java.management@11/sun.management.ThreadImpl.getThreadInfo(ThreadImpl.java:466) at com.taobao.arthas.core.command.monitor200.ThreadCommand.processTopBusyThreads(ThreadCommand.java:206) at com.taobao.arthas.core.command.monitor200.ThreadCommand.process(ThreadCommand.java:122) at com.taobao.arthas.core.shell.command.impl.AnnotatedCommandImpl.process(AnnotatedCommandImpl.java:82) at com.taobao.arthas.core.shell.command.impl.AnnotatedCommandImpl.access$100(AnnotatedCommandImpl.java:18) at com.taobao.arthas.core.shell.command.impl.AnnotatedCommandImpl$ProcessHandler.handle(AnnotatedCommandImpl.java:111) at com.taobao.arthas.core.shell.command.impl.AnnotatedCommandImpl$ProcessHandler.handle(AnnotatedCommandImpl.java:108) at com.taobao.arthas.core.shell.system.impl.ProcessImpl$CommandProcessTask.run(ProcessImpl.java:385) at java.base@11/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515) at java.base@11/java.util.concurrent.FutureTask.run(FutureTask.java:264) at java.base@11/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304) at java.base@11/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) at java.base@11/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) at java.base@11/java.lang.Thread.run(Thread.java:834)

查看发生死锁二线程:

bash
自动换行:关
放大阅读
展开代码
[arthas@15017]$ thread -b No most blocking thread found!

场景3 jad 查看线上代码实际情况

对比线上环境的代码是否和本地的同步了

比如查看类RepeatCommitHandler中 handleRepeatCommit 方法的源代码

bash
自动换行:关
放大阅读
展开代码
jad --source-only cn.hedeoer.actions.handler.RepeatCommitHandler handleRepeatCommit

image-20250828174742502

场景4 watch 查看方法的入参和出参

实际方法

java
自动换行:关
放大阅读
展开代码
@Override public List<CommitFile> queryExistsFailSyncCommitFile(String commitHash) { return SqlSessionHolder.getVanblogSqlSession() .getMapper(CommitFileMapper.class) .queryExistsFailSyncCommitFile(commitHash); }

命令查看:

监视 CommitFileServiceImpl 类中的 queryExistsFailSyncCommitFile 方法,当它被调用时,打印出它的入参、返回值和抛出的异常。这个监视只执行 5 次,并且在打印对象时,最多展开 3 层深度。

bash
自动换行:关
放大阅读
展开代码
watch cn.hedeoer.actions.service.CommitFileServiceImpl queryExistsFailSyncCommitFile '{params,returnObj,throwExp}' -n 5 -x 3
bash
自动换行:关
放大阅读
展开代码
Affect(class count: 1 , method count: 1) cost in 104 ms, listenerId: 4 method=cn.hedeoer.actions.service.CommitFileServiceImpl.queryExistsFailSyncCommitFile location=AtExit ts=2025-08-28 18:06:17.445; [cost=2.661136ms] result=@ArrayList[ @Object[][ @String[93ce6a00ccdd0d77c40eb870c82c0d9f349a08d3], ], @ArrayList[isEmpty=true;size=0], null, ] method=cn.hedeoer.actions.service.CommitFileServiceImpl.queryExistsFailSyncCommitFile location=AtExit ts=2025-08-28 18:07:13.037; [cost=2.274507ms] result=@ArrayList[ @Object[][ @String[93ce6a00ccdd0d77c40eb870c82c0d9f349a08d3], ], @ArrayList[isEmpty=true;size=0], null, ]

场景5 trace 查看一个方法的栈调用耗时

java
自动换行:关
放大阅读
展开代码
@Override public Integer addUserRequestLog(UserRequestLog userRequestLog) { SqlSession session = SqlSessionHolder.getVanblogSqlSession(); return session.getMapper(UserRequestLogMapper.class).insertUserRequestLog(userRequestLog); }

追踪 UserRequestLogServiceImpl 类中 addUserRequestLog 方法的内部调用链路。追踪 5 次方法执行,并且在追踪过程中不要跳过 JDK 核心库里的方法调用。

bash
自动换行:关
放大阅读
展开代码
trace cn.hedeoer.actions.service.UserRequestLogServiceImpl addUserRequestLog -n 5 --skipJDKMethod false
bash
自动换行:关
放大阅读
展开代码
Affect(class count: 1 , method count: 1) cost in 75 ms, listenerId: 5 `---ts=2025-08-28 18:12:02.671;thread_name=JettyServerThreadPool-42;id=42;is_daemon=false;priority=5;TCCL=jdk.internal.loader.ClassLoaders$AppClassLoader@799f7e29 `---[6.525456ms] cn.hedeoer.actions.service.UserRequestLogServiceImpl:addUserRequestLog() +---[33.54% 2.188728ms ] cn.hedeoer.actions.utils.SqlSessionHolder:getVanblogSqlSession() #11 +---[0.99% 0.064879ms ] org.apache.ibatis.session.SqlSession:getMapper() #12 `---[63.52% 4.144776ms ] cn.hedeoer.actions.mapper.UserRequestLogMapper:insertUserRequestLog() #12

场景6 stack 查看方法栈调用

捕获 SqlSessionHolder 类中的 clear 方法被调用时的线程执行堆栈。这个动作会执行 5 次,每次方法被调用时,都会打印出当时的完整调用链路。

bash
自动换行:关
放大阅读
展开代码
stack cn.hedeoer.actions.utils.SqlSessionHolder clear -n 5

可以看出SqlSessionHolder类的clear方法在2025-08-28 18:17

.363时,被RepeatCommitHandler类的handleRepeatCommit方法调用了1次

bash
自动换行:关
放大阅读
展开代码
ts=2025-08-28 18:17:59.363;thread_name=JettyServerThreadPool-36;id=36;is_daemon=false;priority=5;TCCL=jdk.internal.loader.ClassLoaders$AppClassLoader@799f7e29 @cn.hedeoer.actions.utils.SqlSessionHolder.clear() at cn.hedeoer.actions.handler.RepeatCommitHandler.handleRepeatCommit(RepeatCommitHandler.java:134) at cn.hedeoer.actions.BlogGitCommitApplication.lambda$main$0(BlogGitCommitApplication.java:102) at io.javalin.http.JavalinServlet$lifecycle$1$1$1.invoke(JavalinServlet.kt:38) at io.javalin.http.JavalinServlet$lifecycle$1$1$1.invoke(JavalinServlet.kt:38) at io.javalin.http.JavalinServletHandler.executeNextTask(JavalinServletHandler.kt:99) at io.javalin.http.JavalinServletHandler.queueNextTaskOrFinish$lambda-1(JavalinServletHandler.kt:85) 其他内容省略。。。

场景7 tt 场景回放

CommitFileService 接口的 queryExistsFailSyncCommitFile 方法设置一个‘时光隧道’,录制下前 5 次该方法的调用现场。录制完成后,你可以随时回溯(查看)甚至重放(replay)某一次具体的调用

bash
自动换行:关
放大阅读
展开代码
tt -t cn.hedeoer.actions.service.CommitFileService queryExistsFailSyncCommitFile -n 5

实验触发3次:

image-20250828182725402

回放第二次,即编号为1001的

bash
自动换行:关
放大阅读
展开代码
tt -p -i 1001

结果

image-20250828183200926

表明回放方法正确返回(IS-RETURN true),没有异常(IS-EXCEPTION false),耗时3.459221毫秒,返回值为空列表。

本文作者:hedeoer

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!