Java远程调试

Java远程调试(Remote Debugging)的那些事

本文主要基于一篇英文原作翻译而成,删减部分无用文字,添加了必要的注解和补充。

*英文原文是一篇对远程调试讲解很通俗的博文: *
http://blog.trifork.com/2014/07/14/how-to-remotely-debug-application-running-on-tomcat-from-within-intellij-idea/

前言

这篇文章将研究如何处理和调试那些只发生在生产环境(或其他远程环境)而本地开发环境可能没办法重现的“问题”。任何碰到过这种情况的人都不得不承认,试图定位这种“问题”原因的过程,很大可能性是以一堆胡乱猜测而告终:一个非常耗时且低效的过程。

还有一种场景, 你得到了一个可以部署的war/jar包,只有class没有java源代码,而应用部署在本地/远程后,是否可以调试? 读完本文就明白怎么做了。

配置

远程调试包括两个步骤:

  1. 启动Tomcat启用远程调试
  2. 用 IDE (这里用IntelliJ IDEA)要能够调试远程Tomcat应用

Tomcat启用远程调试

这里有多种方法可以做到,根据tomcat所运行的操作系统而有些微的不同。但是不管用哪种方法,这些配置的背后都做了同一件事:传递特定的启动参数给 JVM,让它启用远程调试(remote debugging)。
JVM 激活远程调试的启动参数有 JPDA_OPTS, CATALINA_OPTS 和 JAVA_OPTS。其中 JAVA_OPTS 是通常不建议使用的, 因为基于 JAVA_OPTS 的参数设定会暴露给所有的 JVM 应用, 而 CATALINA_OPTS 定义的设定值限制在Tomcat 内。

1 使用JPDA_OPTS

在 CATALINA_HOME/bin 目录下创建可执行脚本文件 setenv.sh ( Windows 创建 setenv.bat ),加入内容:

Linux setenv.sh


export JPDA_OPTS="-agentlib:jdwp=transport=dt_socket,address=1043,server=y,suspend=n"

Windows setenv.bat


set JPDA_OPTS="-agentlib:jdwp=transport=dt_socket,address=1043,server=y,suspend=n"

这些参数要做的事情就是启用远程调试和配置有效的选项:

  • 指定运行的被调试应用和调试者之间的通信协议,(ie: transport=dt_socket)
  • 远程被调试应用开通的端口,(ie: address=1043), 可定义其他端口,比如9999
  • server=y 表示这个 JVM 即将被调试
  • suspend=n 用来告知 JVM 立即执行,不要等待未来将要附着上/连上(attached)的调试者。如果设成 y, 则应用将暂停不运行,直到有调试者连接上

suspend=y的一个比较适用的场景是,当debug一个会阻止应用成功启动的问题时, 通过suspend=y可以确保调试者连上来之后再启动应用,否则应用已经启动报错了再调试也没意义了。*

当然上面的设置也可以直接放到 catalina.sh (catalina.bat )内,但是有个 setevn.* 额外的配置文件一直是最佳选择, tomcat会自动读取。

要注意的是, 有些人会碰到过用另一个配置方法来启用远程调试:

-Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=1043,suspend=n

-Xdebug and -Xrunjdw 与我们上面推荐的设置不同之处在于, 它是一种旧方式,适用于JVM 小于 JAVA 5.0 的版本(包括5.0), 而 agentlib:jdwp适用于 JAVA 5.0 和以后版本。

最后通过下面的命令行启动tomcat,即可完成tomcat启用远程调试啦。

$CATALINA_HOME/bin/catalina.sh jpda start

2 使用 JAVA_OPTS / CATALINA_OPTS

如果你是在 Windows 系统把 Tomcat 作为系统服务来运行的,直接打开 Apache Tomcat 的属性对话框,在Java Tab也添加启动参数:

-agentlib:jdwp=transport=dt_socket,

address=1043,server=y,suspend=n

请确保每一条配置都是新的行,参数选项之间没有空格

但如果Tomcat没有作为 Windows 系统服务, 启用方法与前面类似,在 setenv.bat 文件中写入:

set CATALINA_OPTS="-agentlib:jdwp=transport=dt_socket,address=1043,server=y,suspend=n"

如果运行在Linux上, setenv.sh 中写入:

export CATALINA_OPTS="-agentlib:jdwp=transport=dt_socket,address=1043,server=y,suspend=n"

按照普通的方式启动Tomcat即可;

./startup.sh

或者
./catalina.sh start
3 使用JPDA启动

最后一种启用远程调试的方式是用 JPDA 切换, 用如下的启动命令将使用默认值自动启用远程调试,

catalina jpda start

该命令默认使用的设置是

-agentlib:jdwp=transport=dt_socket,address=8000,server=y,suspend=n

如果你想要修改默认设置中的选项怎么办?可以通过修改 Tomcat 需要的这些环境变量来实现:

//JPDA_TRANSPORT: 指定 jpda 传输协议

//JPDA_ADDRESS: 指定远程调试端口
//JPDA_SUSPEND: 指定 jvm 启动暂缓

export JDPA_ADDRESS=8080

然后再运行 catalina jpda start , 那么远程调试的端口将变成8080

配置Intellj Idea

确定远程 Tomcat 启动的应用已经开启了远程调试, 下一件事情就是配置Intellij Idea了。这里仍然有两种方式:Remote Tomcat 或者 Remote。

1 使用 Remote Tomcat 配置

首先保证 IDEA 里面已经打开了需要远程调试的工程源码,
然后点击 Run ➝ Edit Configurations ➝ **+ **按钮 ➝ Tomcat Server ➝ Remote

输入必要的远程 IP 地址和端口(Tomcat http端口);

然后转到 Startup / Connnection Tab 页,选择 ”Debug", 输入远程调试端口,我们例子是 1043。

保存后,开始 debug 启动远程调试,如果运行成功会显示如下的界面,然后在源码加断点开始调试。

 
2 使用 Remote 配置(推荐)

第一个方法有个缺陷,你打开的工程源码必须是编译通过的工程,否则会启动会报错;
而介绍的这第二种方法可以在你的工程目录乱七八糟,不是一个完整的可以部署的工程,甚至是一个解压缩的 war/ jar 的情况下都可以调试。

同上步骤,只是选择“Remote",然后输入Name, 修改Host, Port (1043) 即可, 保存后开始Debug。

 

设置比 Remote Tomcat 更简单,这里介绍一个实际案例。

我手里有一个可部署的war包,没有源码,在远程已经部署完毕。这时我想调试那个远程应用,怎么做呢?

解压缩war包到一个文件夹,然后用Intellij Idea打开这个文件夹,如图的结构,编译的Class都在 WEB-INF/classes 目录下

找到我要debug的那个class, 这里示例Handler.class, 通过Idea反编译出来的类代码,拷贝到一个新的文件Handler.java

虽然如图可以看到各种的编译错误,但是完全不影响你启动,代码中加断点和调试哦。

 

远程JVM调试怎么工作的

一切源于被称作 Agents 的东西。

运行着各种编译过的 .class 文件的JVM, 有一种特性,可以允许外部的库(Java或C++写的libraries)在运行时注入到 JVM 中。这些外部的库就称作 Agents, 他们有能力修改运行中 .class 文件的内容。

这些 Agents 拥有的这些 JVM 的功能权限, 是在 JVM 内运行的 Java Code 所无法获取的, 他们能用来做一些有趣的事情,比如修改运行中的源码, 性能分析等。 像 JRebel 工具就是用了这些功能达到魔术般的效果。

传递一个 Agent Lib 给 JVM, 通过添加 agentlib:libname[=options] 格式的启动参数即可办到。像上面的远程调试我们用的就是 **-agentlib:jdwp=... **来引入 jdwp 这个 Agent 的。

jdwp 是一个 JVM 特定的 JDWP(Java Debug Wire Protocol) 可选实现,用来定义调试者与运行JVM之间的通讯,它的是通过 JVM 本地库的 jdwp.so 或者 jdwp.dll 支持实现的。

它到底是怎么工作的呢?

简单来说, jdwp agent 会建立运行应用的 JVM 和调试者(本地或者远程)之间的桥梁。既然他是一个Agent Library, 它就有能力拦截运行的代码。

在 JVM 架构里, debugging 功能在 JVM 本身的内部是找不到的,它是一种抽象到外部工具的方式(也称作调试者 debugger)。这些调试工具或者运行在 JVM 的本地 或者在远程。这是一种解耦,模块化的架构。

更多关于远程部署相关,以及 JDWP 的深入说明,大家有兴趣可以自己研究一下。

来源: https://www.jianshu.com/p/d168ecdce022
-------------------

Java远程调试

集群前后台协议需要做一些修改,我负责jdbc这边的修改。按照协议内容修改完代码之后却面临一个测试的问题:修改后的后台又部署在北京,但北京并不是所有的机器都对天津这边开放,只给提供一台机器A,就是集群的服务器。
这样的话,就无法创建节点的连接,测试没有办法进行。

一开始用了最简单的办法,把打好的jar包通过远程ssh放到A上面,再通过ssh去跑用例,打印结果看看正确与否。但是这样的效率真的太低了,每做一次修改都要打包、部署、运行、分析log。于是借这个机会学习一下java程序的远程调试。以下为总结。

一些概念

JDPA: java平台调试架构

JVMTI: JVM端调试接口

JDI: java端调试接口

JDWP: java调试网络协议

 

JPDA 定义了一套如何开发调试工具的接口和规范。

JPDA 由三个独立的模块 JVMTI、JDWP、JDI 组成。

调试者通过 JDI 发送接受调试命令。

JDWP 定义调试者和被调试者交流数据的格式。

JVMTI 可以控制当前虚拟机运行状态。

上图中的前端工具就是我们要用到的调试工具。如JDB、Eclipse等等。这些工具实现了JDI接口,通过这些工具我们可以达到在命令行或者图形界面下调试的目的。

关于这部分,只是简单的了解一下概念,更多的关于JDPA的介绍:JDPA体系

使用JDB进行本地调试

JDB 是jdk自带的一个调试工具,用于命令行下调试java程序

jdb.exe就位于jdk安装目录的bin目录下,安装好jdk并设置好环境变量之后就可以愉快的使用jdb了。

C:\Users\kyu>jdb -help
用法: jdb <options> <class> <arguments>

其中, 选项包括:
    -help             输出此消息并退出
    -sourcepath <";" 分隔的目录>
                      要在其中查找源文件的目录
    -attach <address>
                      使用标准连接器附加到指定地址处正在运行的 VM
    -listen <address>
                      等待正在运行的 VM 使用标准连接器在指定地址处连接
    -listenany
                      等待正在运行的 VM 使用标准连接器在任何可用地址处连接
    -launch
                      立即启动 VM 而不是等待 'run' 命令
    -listconnectors   列出此 VM 中的可用连接器
    -connect <connector-name>:<name1>=<value1>,...
                      使用所列参数值通过指定的连接器连接到目标 VM
    -dbgtrace [flags] 输出信息供调试jdb
    -tclient          在 HotSpot(TM) 客户机编译器中运行应用程序
    -tserver          在 HotSpot(TM) 服务器编译器中运行应用程序

转发到被调试进程的选项:
    -v -verbose[:class|gc|jni]
                      启用详细模式
    -D<name>=<value>  设置系统属性
    -classpath <";" 分隔的目录>
                      列出要在其中查找类的目录
    -X<option>        非标准目标 VM 选项

<class> 是要开始调试的类的名称
<arguments> 是传递到 <class> 的 main() 方法的参数

要获得命令的帮助, 请在jdb提示下键入 'help'

上面是启动JDB的语法说明。

现假设运行程序的工程目录如下:

JDBTest
  |----bin(编译生成的class文件)
  |     |----*.class
  |----src(源文件)
  |     |----*.java
  |----lib(依赖的第三方jar)
  |     |----*.jar

开始启动JDB调试:

Y:\project\JavaProject\JDBTest>jdb -classpath ./bin/;./lib/* -sourcepath ./src/ test.JDBTest

注意,如果有多个文件,windows下使用 ";" 分隔每个文件或目录,Linux下使用 ":" 分隔每个文件或目录

-classpath 指定了类路径,-sourcepath 指定了源文件的路径

回车,出现如下信息:

Y:\project\JavaProject\JDBTest>jdb -classpath ./bin/;./lib/* -sourcepath ./src/ test.JDBTest
正在初始化jdb...
> 

此时,JDB调试器等待用户的输入,输入help,出现如下信息:

Y:\project\JavaProject\JDBTest>jdb -classpath ./bin/;./lib/* -sourcepath ./src/ test.JDBTest
正在初始化jdb...
> help
** 命令列表 **
connectors                -- 列出此 VM 中可用的连接器和传输

run [class [args]]        -- 开始执行应用程序的主类

threads [threadgroup]     -- 列出线程
thread <thread id>        -- 设置默认线程
suspend [thread id(s)]    -- 挂起线程 (默认值: all)
resume [thread id(s)]     -- 恢复线程 (默认值: all)
where [<thread id> | all] -- 转储线程的堆栈
wherei [<thread id> | all]-- 转储线程的堆栈, 以及 pc 信息
up [n frames]             -- 上移线程的堆栈
down [n frames]           -- 下移线程的堆栈
kill <thread id> <expr>   -- 终止具有给定的异常错误对象的线程
interrupt <thread id>     -- 中断线程

print <expr>              -- 输出表达式的值
dump <expr>               -- 输出所有对象信息
eval <expr>               -- 对表达式求值 (与 print 相同)
set <lvalue> = <expr>     -- 向字段/变量/数组元素分配新值
locals                    -- 输出当前堆栈帧中的所有本地变量

classes                   -- 列出当前已知的类
class <class id>          -- 显示已命名类的详细资料
methods <class id>        -- 列出类的方法
fields <class id>         -- 列出类的字段

threadgroups              -- 列出线程组
threadgroup <name>        -- 设置当前线程组

stop in <class id>.<method>[(argument_type,...)]
                          -- 在方法中设置断点
stop at <class id>:<line> -- 在行中设置断点
clear <class id>.<method>[(argument_type,...)]
                          -- 清除方法中的断点
clear <class id>:<line>   -- 清除行中的断点
clear                     -- 列出断点
catch [uncaught|caught|all] <class id>|<class pattern>
                          -- 出现指定的异常错误时中断
ignore [uncaught|caught|all] <class id>|<class pattern>
                          -- 对于指定的异常错误, 取消 'catch'
watch [access|all] <class id>.<field name>
                          -- 监视对字段的访问/修改
unwatch [access|all] <class id>.<field name>
                          -- 停止监视对字段的访问/修改
trace [go] methods [thread]
                          -- 跟踪方法进入和退出。
                          -- 除非指定 'go', 否则挂起所有线程
trace [go] method exit | exits [thread]
                          -- 跟踪当前方法的退出, 或者所有方法的退出
                          -- 除非指定 'go', 否则挂起所有线程
untrace [methods]         -- 停止跟踪方法进入和/或退出
step                      -- 执行当前行
step up                   -- 一直执行, 直到当前方法返回到其调用方
stepi                     -- 执行当前指令
next                      -- 步进一行 (步过调用)
cont                      -- 从断点处继续执行

list [line number|method] -- 输出源代码
use (或 sourcepath) [source file path]
                          -- 显示或更改源路径
exclude [<class pattern>, ... | "none"]
                          -- 对于指定的类, 不报告步骤或方法事件
classpath                 -- 从目标 VM 输出类路径信息

monitor <command>         -- 每次程序停止时执行命令
monitor                   -- 列出监视器
unmonitor <monitor#>      -- 删除监视器
read <filename>           -- 读取并执行命令文件

lock <expr>               -- 输出对象的锁信息
threadlocks [thread id]   -- 输出线程的锁信息

pop                       -- 通过当前帧出栈, 且包含当前帧
reenter                   -- 与 pop 相同, 但重新进入当前帧
redefine <class id> <class file name>
                          -- 重新定义类的代码

disablegc <expr>          -- 禁止对象的垃圾收集
enablegc <expr>           -- 允许对象的垃圾收集

!!                        -- 重复执行最后一个命令
<n> <command>             -- 将命令重复执行 n 次
# <command>               -- 放弃 (无操作)
help (?)               -- 列出命令
version                   -- 输出版本信息
exit (或 quit)            -- 退出调试器

<class id>: 带有程序包限定符的完整类名
<class pattern>: 带有前导或尾随通配符 ('*') 的类名
<thread id>: 'threads' 命令中报告的线程编号
<expr>: Java(TM) 编程语言表达式。
支持大多数常见语法。

可以将启动命令置于 "jdb.ini"".jdbrc" 中
位于 user.home 或 user.dir 中
>

上面的帮助信息说明了如何进行JDB调试,解释一下其中的几个:

step: -- 执行当前行 相当于Eclipse中的F5

step up: -- 一直执行, 直到当前方法返回到其调用方 相当于Eclipse中的F7

next: -- 步进一行 (步过调用) 相当于Eclipse中的F6

cont: -- 从断点处继续执行 相当于Eclipse中的F8

此时,继续输入:

> stop at test.JDBTest:7
正在延迟断点test.JDBTest:7。
将在加载类后设置。
> run
运行test.JDBTest
设置未捕获的java.lang.Throwable
设置延迟的未捕获的java.lang.Throwable
>
VM 已启动: 设置延迟的断点test.JDBTest:7

断点命中: "线程=main", test.JDBTest.main(),=7 bci=0
7               JDBTest jdbTest = new JDBTest();

main[1]

stop at test.JDBTest:7 表示在这个类文件的第7行处打一个断点

接着,输入run,就开始进入调试步骤了。现在可以输入上面帮助中的语法来了解当前程序的执行情况了。一试便知

注意, 若想要在调试时能够正常输出调试信息如变量值等等,需要在编译java文件时指定 -g 参数,否则无法获得其运行时的调试信息

另外,使用list可以打印当前断点处的源代码,如果没有在启动JDB时指定源代码路径 -sourcepath ./src/ ,那么会提示没有源代码信息,无法输出。此时可以使用命令 use ./src/ 来指定源代码路径,再使用list命令时可以正常打印了。

以上就是使用JDB调试本地程序的方法,具体的使用可根据实际情况参照语法说明去执行。

使用JDB进行远程调试

如果程序不是运行在本机,而是在其他机器或者现场的时候,可以使用java提供的远程调试功能。

假设程序现运行在主机 192.168.101.72 这台机器上,该机器为linux环境,且只可以通过ssh作为一个普通用户连接。我们想要在自己的机器上调试运行在192.168.101.72这台机器上的程序。

启动要调试的程序

在192.168.101.72这台主机上以下面的方式启动java程序:还是以JDBTest为例

java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=8899 -classpath ./bin/:./lib/* test.JDBTest

此时,命令行输出

Listening for transport dt_socket at address: 8899

并处于等待状态

下面是几个参数的解释:

-Xdebug 启用调试特性。

-Xrunjdwp:<sub-options> 在目标 VM 中加载 JDWP 实现。它通过传输和 JDWP 协议与独立的调试器应用程序通信。下面介绍一些特定的子选项。
从 Java V5 开始,您可以使用 -agentlib:jdwp 选项,而不是 -Xdebug 和 -Xrunjdwp。但如果连接到 V5 以前的 VM,只能选择 -Xdebug 和 -Xrunjdwp。下面简单描述 -Xrunjdwp 子选项。

transport 这里通常使用套接字传输。但是在 Windows 平台上也可以使用共享内存传输。

server 如果值为 y,目标应用程序监听将要连接的调试器应用程序。否则,它将连接到特定地址上的调试器应用程序。

address 这是连接的传输地址。如果服务器为 n,将尝试连接到该地址上的调试器应用程序。否则,将在这个端口监听连接。

suspend 如果值为 y,目标 VM 将暂停,直到调试器应用程序进行连接。

本机连接远程程序并启动调试

在本机上命令行下输入:

jdb -connect com.sun.jdi.SocketAttach:hostname=192.168.101.72,port=8899

然后就进入了调试界面,你可以像调试本机程序那样使用JDB的一些命令来调试了。当退出调试程序时,远程主机上的程序也就退出了。

使用Eclipse进行远程调试

可以使用Eclipse进行远程调试,就上上面使用JDB一样。

启动要调试的程序

与JDB远程调试一样,启动远程主机上的程序:

java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=8899 -classpath ./bin/:./lib/* test.JDBTest

本机启动Eclipse进行调试

首先要右键工程->java compiler

image.png

上图中的几个选项最好全部打勾,否则调试时会出现无法打断点或者获取不到行号等问题(关于这几个选项的含义在之前的总结中有提到)

接着,右键工程->Debug As->Run Configurations, 在出现的对话框中选择Remote Java Application, 右键->New, 出现如下界面:

 

在Connect页中,选择对应的java 工程,Connection Type选择 Socket Attach,然后填写远程主机的ip和端口,这里应该填写192.168.101.72和8899。
在Source页中可以添加源代码,如用到的第三方jar的源代码或者引用的工程,调试时就可以进入到这部分代码查看。在Common页可以设置编码的配置。

接下来点击Debug按钮,就可以愉快的在本机调试远程程序了,就像调试本地程序那样。只不过可能有一点一点慢,不过比打Log的方式要好很多了。

来源: https://www.jianshu.com/p/5362c7b8ddac