使用 JDB debug Java 应用程序
写了很多年Java 程序, 很少有机会去使用 JDB debug Java 程序. 因为本地都是使用 IDE 里面的工具, 对于没有桌面的生产环境, 基本都是加入打日志的新代码, 或者使用 Btrace/Bytesman 进行注入. 今天我们将使用 Jdb 在生产环境debug 一段代码.
debug 一段简单的代码
下面是使用 MBean 获取系统剩余空闲内存的代码, 我们将用 jdb 去debug 它.
import java.lang.management.ManagementFactory;
import com.sun.management.OperatingSystemMXBean;
public class FreeMemoryExample {
@SuppressWarnings("restriction")
public static void main(String[] args) {
OperatingSystemMXBean osBean = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
long freePhysicalMemorySize = osBean.getFreePhysicalMemorySize();
System.out.println("Free Physical Memory Size: " + freePhysicalMemorySize + " bytes");
}
}
下面编译这段代码并且进入 debug 运行:
$ javac FreeMemoryExample.java
$ jdb FreeMemoryExample
nitializing jdb ...
> stop at FreeMemoryExample:8
Deferring breakpoint FreeMemoryExample:8.
It will be set after the class is loaded.
> run
run FreeMemoryExample
Set uncaught java.lang.Throwable
Set deferred uncaught java.lang.Throwable
>
VM Started: Set deferred breakpoint FreeMemoryExample:8
Breakpoint hit: "thread=main", FreeMemoryExample.main(), line=8 bci=7
8 long freePhysicalMemorySize = osBean.getFreePhysicalMemorySize();
上面的运行过程中, 我们把断点设置在 FreeMemoryExample
的第八行. 然后执行 run
命令, 启动程序, 然后程序停在了第8行, 并且打印了该行代码.
执行 list
子命令, 显示当前行的前后几行代码.
main[1] list
4 public class FreeMemoryExample {
5 @SuppressWarnings("restriction")
6 public static void main(String[] args) {
7 OperatingSystemMXBean osBean = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
8 => long freePhysicalMemorySize = osBean.getFreePhysicalMemorySize();
9 System.out.println("Free Physical Memory Size: " + freePhysicalMemorySize + " bytes");
10 }
11 }
执行 threads
子命令, 显示所有线程
main[1] threads
Group system:
(java.lang.ref.Reference$ReferenceHandler)0x16c Reference Handler running
(java.lang.ref.Finalizer$FinalizerThread)0x16d Finalizer cond. waiting
(java.lang.Thread)0x16e Signal Dispatcher running
Group main:
(java.lang.Thread)0x1 main running (at breakpoint)
Group InnocuousThreadGroup:
(jdk.internal.misc.InnocuousThread)0x198 Common-Cleaner cond. waiting
执行 locals
子命令, 显示所有本地变量
main[1] locals
Local variable information not available. Compile with -g to generate variable information
可以看到这次并没有显示本地变量, 是因为在编译的时候没有加 -g
参数. javac
关于 -g
参数的说明如下:
-g Generate all debugging info
-g:{lines,vars,source} Generate only some debugging info
-g:none Generate no debugging info
所以 -g
就是添加 debugging
信息的. 所以, 我们重新编译一下, 然后继续上面的 locals
子命令
main[1] locals
Method arguments:
args = instance of java.lang.String[0] (id=831)
Local variables:
osBean = instance of com.sun.management.internal.OperatingSystemImpl(id=832)
这次我们可以看到 main
函数的参数 args
和 osBean
变量.
添加方法进入断点
我们可以看到下面讲要执行 OperatingSystemImpl.getFreePhysicalMemorySize
, 我们进入这个方法的段点, 并通过执行 cont
执行到该断点.
main[1] stop in com.sun.management.internal.OperatingSystemImpl.getFreePhysicalMemorySize
Set breakpoint com.sun.management.internal.OperatingSystemImpl.getFreePhysicalMemorySize
main[1] cont
>
Breakpoint hit: "thread=main", com.sun.management.internal.OperatingSystemImpl.getFreePhysicalMemorySize(), line=243 bci=0
执行 where 查看当前的栈
main[1] where
[1] com.sun.management.internal.OperatingSystemImpl.getFreePhysicalMemorySize (OperatingSystemImpl.java:243)
[2] FreeMemoryExample.main (FreeMemoryExample.java:8)
通过 help
子命令, 我们可以看到所有的子命令.
如何通过 jdb 连接一个正在运行的程序
上面的例子都是通过 jdb
启动想要调试的程序, 那么如何调试另一个独立的程序呢? 我们先看一下 IDE 是如何做的, 首先, 我们本地使用 Eclipse 打开刚才的程序, 设置断点, 然后启动程序. 这个时候, 应用停在断点处. 我们查看这个应用是如何启动的.
$ jps
57184 FreeMemoryExample
69238 Eclipse
57225 Jps
$ jcmd 57184 VM.command_line
57184:
VM Arguments:
jvm_args: -agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:63529 -javaagent:/Users/xiatian/work/tools/eclipse/Eclipse.app/Contents/Eclipse/configuration/org.eclipse.osgi/223/0/.cp/lib/javaagent-shaded.jar -Dfile.encoding=UTF-8
java_command: FreeMemoryExample
可以看到 Eclipse 启动程序的时候, 添加了 -agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:63529
这就是启动调试的关键. 它相当于在应用端开启了一个端口在 63529
监听端口并可以完成某些命令, 然后 jdb
通过连接找个端口, 使用 jdwp
协议与之通信, 完成 jdb
的各种命令.
连接器 是Java调试器(JDB)与被调试的Java程序之间通信的接口。它允许JDB与正在运行的Java程序建立连接,并在程序执行时进行交互式的调试操作。
jdb
的 connectors
子命令能显示当前 jdb
支持的连接器, 如:
Connector: com.sun.jdi.ProcessAttach Transport: local
Connector: com.sun.jdi.RawCommandLineLaunch Transport: dt_socket
Connector: com.sun.jdi.SocketAttach Transport: dt_socket
ref: