Java 应用产生 core dump

什么是core dump

在 Linux 系统中,当一个进程崩溃(例如,由于段错误或其他严重错误),它通常会产生一个称为 "核心转储"(core dump)的文件。核心转储文件包含了进程崩溃时的内存映像和一些执行上下文信息,如寄存器状态、程序计数器位置、内存管理信息等。这些信息对于开发者来说非常有价值,因为它们可以用来调试程序,了解程序崩溃的原因。

Java 进程如何产生 core dump

对于正在运行的 Java 进程, 它就是一个标准的linux 进程, 可以使用 linux 上的各种工具来产生 core dump, 比如 gcore 或者 kill :

$ gcore <pid>
$ kill -11 <pid>

对于 Java 应用程序自身, 当它奔溃的时候, 默认会产生 core dump, 因为它有如下默认参数-XX:+CreateCoredumpOnCrash:

$ java -XX:+PrintFlagsFinal -version | grep Core
     bool CreateCoredumpOnCrash                    = true                                      {product} {default}

虽然上面的方式都能产生 core dump, 但是很有可能你并不能看到 core dump, 因为有各种其它条件会阻碍 core dump的产生:

  1. 系统设置了 core size 的大小太小, 查看 ulimit -c
  2. core dump 要写入的文件夹没有权限
  3. core dump 被系统设置拦截, 比如 apport, 它产生了 crash report, 却拦截了 core dump的产生.

core dump 有什么用?

core dump 里有进程当时时间点上的全部内存信息, 寄存器信息, 栈信息, 栈上变量值, 打开文件句柄, 打开的socket 等各种非常有用的信息, 对于诊断应用为什么崩溃具有很大的意义.

gdb 可以直接打开core dump 文件, 并读取里面的信息.

$ gdb core core.720444` 

使用 gdb 去debug Java 程序比较麻烦.

但是可以使用 JDK 自带的 jdb:

# java 8 版本
$ jdb -listconnectors
$ jdb -connect sun.jvm.hotspot.jdi.SACoreAttachingConnector:javaExecutable=$JAVA_HOME/bin/java,core=core.720444

java 11 版本
$ jhsdb clhsdb --exe ErrorExample --core core.720444

Core Dump vs Java Heap Dump

Core Dump

  • 定义
    Core dump 是操作系统在进程异常终止时生成的一个文件,它包含了进程终止时内存中的内容。
  • 内容
    Core dump 包含了进程的整个内存映像,包括程序计数器、寄存器、堆栈、堆内存、全局变量、打开的文件描述符、环境变量、程序代码、加载的共享库等。
  • 用途
    主要用于程序崩溃后的调试和故障排查。可以使用调试工具(如 gdb)来分析 core dump 文件,确定程序崩溃的原因。
  • 大小
    Core dump 文件通常很大,因为它包含了整个进程的内存映像。
  • 生成方式
    Core dump 通常由操作系统在进程崩溃时自动生成,或者可以使用 gcore 命令手动生成。

Java Heap Dump

  • 定义
    Java heap dump 是 JVM 在某一时刻的堆内存的快照,包含了所有的 Java 对象和类信息。
  • 内容
    Heap dump 专注于 Java 堆内存,包括对象实例、数组、类实例、垃圾收集器信息等。
  • 用途
    主要用于分析 Java 应用程序的内存使用情况,如检测内存泄漏、查看对象的分配和引用情况等。
  • 大小
    Heap dump 文件的大小取决于 Java 堆的大小,通常比完整的 core dump 小。
  • 生成方式
    Heap dump 可以通过 JVM 提供的工具(如 jmap)、管理接口(如 JMX)生成,或在发生 OutOfMemoryError 时自动生成(如果配置了 -XX:+HeapDumpOnOutOfMemoryError JVM 参数)。

heap dump vs crash log file

这就类似
core dump -> apport crash report
heap dump -> crash log file (err_log_pid.log)

-XX:OnError 选项

如果启动参数配置了-XX:OnError选项, 当 fatal error 产生的时候, JVM 就会执行该选项配置的命令. 多个执行命令可以用 ; 分割开. 可以用 %p 指代当前进程号, 因为 % 用作了特殊符, 所以遇到真的 %, 就要用两个 %% 代替. 官方给的例子:

java -XX:OnError="pmap %p" MyApp #使用 pmap 查看内存空间
java -XX:OnError="gcore %p; dbx - %p" MyApp # 使用 gcore 产生core dump 并用 dbx 进行debug
java -XX:OnError="gdb - %p" MyApp # 使用 gdb debug

-XX:ErrorFile 选项

设置当 fatal error 产生的时候, 把log写到哪个文件去, 需要全路径名(%p指代当前进程号, %%=%).
如果该文件存在, 并且可以写, 那么就覆盖.
如果不设置, 它的默认行为是: 产生一个 java_error_%p.log 文件, %p是进程号, 默认放在当前进程的当前工作目录(CWD), 如果当前工作目录不可用(比如写权限,空间不够等), 会写到临时文件夹目录(/tmp).
参考: https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/felog001.html

试验 -XX:OnError 和 -XX:ErrorFile

更多细节在:
https://www.tianxiaohui.com/index.php/Troubleshooting/Java-%E5%A6%82%E4%BD%95%E5%88%9B%E5%BB%BA%E4%B8%80%E4%B8%AA-fatal-error.html

jhsdb

jhsdb(Java HotSpot Debugger)是在JDK 9中引入的命令行实用程序,它是服务性代理(Serviceability Agent)工具的一部分。服务性代理是一个框架,允许对运行中的Java虚拟机(JVM)或崩溃后的核心转储进行深入分析。引入jhsdb的目的有几个:

  1. 统一的调试工具:在JDK 9之前,开发者需要使用不同的工具,如jmapjstackjinfo等来执行各种调试任务。jhsdb将这些工具统一到一个命令行界面下,使得在不切换多个工具的情况下更容易执行广泛的调试和诊断任务。
  2. 增强的事后诊断jhsdb提供了分析核心转储和实时进程的能力,这对事后诊断至关重要。当JVM崩溃时,开发者需要了解崩溃时JVM的状态,这一点尤其有用。
  3. 高级分析能力:使用jhsdb,开发者可以执行高级分析,如检查堆、分析内存使用情况和获取线程堆栈跟踪。它还允许使用内置的命令行调试器(CLD)来调试Java进程,这是一个类似于gdb的基于文本的界面。
  4. 跨平台一致性jhsdb旨在在不同平台上保持一致的工作,为在各种操作系统上进行诊断提供了标准化的方法。
  5. 与HotSpot JVM集成:由于jhsdb是为HotSpot JVM量身定制的,它可以利用HotSpot的内部特性来提供有关JVM内部的详细信息,这对于性能调优和解决复杂问题非常宝贵。
  6. 可访问性:通过包含在JDK中,jhsdb可以立即供所有Java开发者使用,无需额外下载或安装。
  7. 可脚本化:作为命令行工具,jhsdb可以轻松地编写脚本并集成到自动化的调试和诊断工作流中,这对于持续集成和部署管道非常有益。

总的来说,JDK 9中引入jhsdb的目的是通过提供一个强大而多功能的JVM诊断和调试工具来改善开发者体验,从而增强分析和解决Java应用程序中复杂问题的能力。

实践

linux 系统现在有一个正在运行的 Java 程序, 进程号是 256487, 却遇到了下面的错误

$ jhsdb jmap  --pid 256487
Attaching to process ID 256487, please wait...
ERROR: ptrace(PTRACE_ATTACH, ..) failed for 256487: Operation not permitted
Error attaching to process: sun.jvm.hotspot.debugger.DebuggerException: Can't attach to the process: ptrace(PTRACE_ATTACH, ..) failed for 256487: Operation not permitted
sun.jvm.hotspot.debugger.DebuggerException: sun.jvm.hotspot.debugger.DebuggerException: Can't attach to the process: ptrace(PTRACE_ATTACH, ..) failed for 256487: Operation not permitted

这是因为 ptrace 权限的问题. 查看 /proc/sys/kernel/yama/ptrace_scope 可以看到里面的值是 1, 改成 0 就好了

$ cat /proc/sys/kernel/yama/ptrace_scope
1
$ echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
0

$ jhsdb jmap  --pid 256487 # as pmap output
Attaching to process ID 256487, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 11.0.22+7-post-Ubuntu-0ubuntu222.04.1
0x0000563f4efe6000    14K    /usr/lib/jvm/java-11-openjdk-amd64/bin/java
0x00007fe2671ba000    44K    /usr/lib/jvm/java-11-openjdk-amd64/lib/libzip.so
0x00007fe2671c5000    66K    /usr/lib/x86_64-linux-gnu/libresolv.so.2
0x00007fe2671d9000    22K    /usr/lib/x86_64-linux-gnu/libkeyutils.so.1.9
0x00007fe2671e0000    50K    /usr/lib/x86_64-linux-gnu/libkrb5support.so.0.1
0x00007fe2671ee000    18K    /usr/lib/x86_64-linux-gnu/libcom_err.so.2.1
0x00007fe2671f4000    178K    /usr/lib/x86_64-linux-gnu/libk5crypto.so.3.1
0x00007fe267223000    808K    /usr/lib/x86_64-linux-gnu/libkrb5.so.3.3
0x00007fe2672ee000    330K    /usr/lib/x86_64-linux-gnu/libgssapi_krb5.so.2.2
0x00007fe267342000    178K    /usr/lib/x86_64-linux-gnu/libtirpc.so.3.0.0
0x00007fe267370000    91K    /usr/lib/x86_64-linux-gnu/libnsl.so.2.0.1
0x00007fe26738a000    54K    /usr/lib/x86_64-linux-gnu/libnss_nis.so.2.0.0
0x00007fe267399000    42K    /usr/lib/x86_64-linux-gnu/libnss_compat.so.2
0x00007fe2673b8000    39K    /usr/lib/jvm/java-11-openjdk-amd64/lib/libjimage.so
0x00007fe2673c1000    213K    /usr/lib/jvm/java-11-openjdk-amd64/lib/libjava.so
0x00007fe2673ef000    14K    /usr/lib/x86_64-linux-gnu/librt.so.1
0x00007fe2674f4000    122K    /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
0x00007fe267514000    918K    /usr/lib/x86_64-linux-gnu/libm.so.6
0x00007fe2675fb000    2207K    /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.30
0x00007fe267827000    22708K    /usr/lib/jvm/java-11-openjdk-amd64/lib/server/libjvm.so
0x00007fe268afc000    2168K    /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007fe268d25000    80K    /usr/lib/jvm/java-11-openjdk-amd64/lib/jli/libjli.so
0x00007fe268d38000    106K    /usr/lib/x86_64-linux-gnu/libz.so.1.2.11
0x00007fe268d54000    67K    /usr/lib/jvm/java-11-openjdk-amd64/lib/libverify.so
0x00007fe268d67000    235K    /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2

$ jhsdb -h
    clhsdb           command line debugger
    hsdb             ui debugger
    debugd --help    to get more information
    jstack --help    to get more information
    jmap   --help    to get more information
    jinfo  --help    to get more information
    jsnap  --help    to get more information

$ jhsdb jmap --help

$ jhsdb jmap --binaryheap --pid 274471
Attaching to process ID 274471, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 11.0.22+7-post-Ubuntu-0ubuntu222.04.1
heap written to heap.bin

Java 如何创建一个 fatal error

之前在写另外一篇文章的时候, 讨论到如果遇到 fatal error, 就能通过 JVM 启动参数 -XX:OnError 来触发执行定制的命令, 以及用 -XX:ErrorFile 来产生 hs_err_pid*.log. 那么什么是 fatal error 呢?

我们知道Java 的错误分为 ErrorException 可是即便抛出一个 Error 并不能触发上面的2个参数, 所以文档里指的 fatal error 肯定是更为严重的错误.

你以前可能看到过 hs_err_pid*.log, 可是真要故意创建一个 fatal error 的时候, 却犯难了.

如何创建一个 fatal error

有趣的是, 但我问 chatGPT 如何创建这种 fatal error 场景的时候, 它提示我: This content may violate our usage policies. 竟然拒绝回答. 当我告诉它, 我只是在做一个本地实验的时候, 它告诉我通过下面3种方法能造成 fatal error:

  1. 调用 native C 代码, 读取错误地址的代码, 造成 Segment fault;
  2. 错误使用 JVM 参数, 如设置 -XX:CompileThreshold 特小, 导致编译死循环;
  3. 手动触发特定 Error, 如 OutOfMemoryError.

试了上面的第三项, 不论是通过消耗内存真造成 OOM 还是直接抛出 OutOfMemoryError 都无法触发.

谷歌搜索了一下, 也没找到我这种想触发的人, 都是遇到这种问题的人.

思路 从JDK 源代码找到可能触发的场景

搜索 hs_err_pid site:openjdk.java.net 可以看到几个相关的issue, 比如 JDK-8220786JDK-8220787 都是讨论把 hs-err 从定向到 stdout 或 stderr 去.

接着继续搜索 github 上的 JDK 源码, 找到下面的代码:
https://github.com/openjdk/jdk/blob/4e5c25ee43d4ec31ed5160fd93a2fd15e35182f8/src/hotspot/share/utilities/vmError.cpp#L1827

fd_log = prepare_log_file(ErrorFile, "hs_err_pid%p.log", true, buffer, sizeof(buffer));

从这里我们看到了如何写入以及什么时候才能写入 hs_err_pid%p.log 的逻辑. 这个代码位于: vmError.cpp VMError::report_and_die 方法, 并且这里面有好几个重载的该方法.

接下来, 我们只要在该 repo 里面搜索什么时候调用这个 report_and_die 方法的, 就能找到我们寻找的触发条件.

其中一处是关于在 linux 平台触发的在 https://github.com/openjdk/jdk/blob/4e5c25ee43d4ec31ed5160fd93a2fd15e35182f8/src/hotspot/os/posix/signals_posix.cpp#L649, 这个方法是: JVM_HANDLE_XXX_SIGNAL, 在其注释中提到 This routine may recognize any of the following kinds of signals: SIGBUS, SIGSEGV, SIGILL, SIGFPE, SIGQUIT, SIGPIPE, SIGXFSZ, SIGUSR1 .

所以, 我们可以使用 linux signal 来测试.

使用 SIGSEGV 测试

在 linux 机器上写一个 main 函数里面让线程 sleep 的代码, 然后启动它. 同时开另外一个窗口, 对它发送 SIGSEGV 信号, 观察到:

$ java -XX:OnError="touch /tmp/eric.txt"  ErrorExample 
$ kill -11 <pid>

发现 /tmp/eric.txt 确实产生了. 同时观察到在当前目录产生了 hs_err_pid702969.log 等日志文件. 虽然Java 进程奔溃会默认产生 core dump, 但是却没有找到 core dump.

为什么没有产生 core dump

JVM 有个启动参数 -XX:+CreateCoredumpOnCrash 默认是打开的, 所以, 理论上当 crash 的时候, 会产生core dump.

$ java -XX:+PrintFlagsFinal -version | grep Core
     bool CreateCoredumpOnCrash                    = true                                      {product} {default}

检查了 ulimit -c 确认是 unlimited, 同时搜索了全磁盘, 没有找到对应的 core dump.
如果查看上面提到的 hs_err_pid702969.log, 就会发现里面这么写的:

# Core dump will be written. Default location: Core dumps may be processed with "/usr/share/apport/apport -p%p -s%s -c%c -d%d -P%P -u%u -g%g -- %E" (or dumping to /home/supra/work/java/error/core.702969)

这里内容提及了 core dump, 以及 apport 和可能放置 core dump 的路径, 但该路径的core dump 却不存在.

apport 是什么

apport 是一款系统程序, 它能拦截应用在 crash 时候的 core dump 数据, 并且生成详细的report. 对于绝大多数用户来说, 遇到程序crash生成 core dump 并不能带来什么价值, 如果把 core dump 转成一份可以被用户或者admin 能读取的固定格式报告, 将会非常有用, apport 就是这么一种程序, 官方网站: https://wiki.ubuntu.com/Apport

如果你使用桌面版本的 Ubuntu 发行版, 在程序crash 的时候, 你可能会看到一个弹窗, 问你要不要发送crash report 给 ubuntu 官方, 它就是 apport 的结果.

通常它产生的 crash report 在 /var/crash/ 文件夹.

为什么 core dump 被 apport 接管了?

查看 core dump 的官方文档: https://man7.org/linux/man-pages/man5/core.5.html.
可以看到: /proc/sys/kernel/core_pattern 是一个内核参数,用于控制核心转储文件的生成方式。当一个进程崩溃时,内核会查看 core_pattern 的值来决定如何处理核心转储。

core_pattern 可以包含以下几种类型的值:

  1. 静态文件路径:如果 core_pattern 包含一个普通的文件路径(不包含管道符 |),核心转储文件将被写入该路径指定的位置。路径中可以包含一些特殊的格式化字符(如 %p 表示进程 ID),这些字符会被替换为相应的值,以便为每个核心转储生成唯一的文件名。
  2. 管道命令:如果 core_pattern 的值以管道符 | 开头,后面跟随的是一个命令,那么内核会执行该命令,并将核心转储数据作为标准输入传递给它。这允许将核心转储数据发送到一个自定义的处理程序,如 Apport 或 Systemd-coredump,而不是直接写入文件系统。

如: /var/coredumps/core.%e.%p|/usr/share/apport/apport %p %s %c %d %P %u %g -- %E

我实验用的 linux 版本就是上面的带管道的例子, 所以, 它使用 apport 生成了 crash dump, 我去 /var/crash/ 目录, 就看到了 crash report _usr_lib_jvm_java-11-openjdk-amd64_bin_java.1000.crash.

现在如何产生 core dump

  1. 使用 -XX:OnError="gdb %p"
  2. 修改 /proc/sys/kernel/core_pattern 为 core dump 文件模版(去除管道).
$ java -XX:OnError="gcore %p"  ErrorExample
$ ls -alh | grep core
$ core.720444

总结

  1. -XX:OnError 决定当致命错误是干啥
  2. -XX:ErrorFile 决定了 hs_err 日志的文件名, 还有另外两个类似的参数决定是不是写到 stdout 和 stderr.
  3. -XX:+CreateCoredumpOnCrash 决定了崩溃时默认产生core dump, core dump 文件名或怎么处理根据 /proc/sys/kernel/core_pattern 来定.

Java JDPA

JDPA 是一个框架, 它定义了 debugger 和 debuggee 是如何交互的, 两端交互的API 和 协议, 并不包含具体的实现. 它包含3部分.

  1. debugger 端的API: JDI (Java Debug Interface)
  2. debuggee 端的API: JVM TI
  3. 连接两端的协议: JDWP

JDPA.png

jdb

jdb 是 JDK 自带的命令行 debugger 端, 是 JDI 的实现.

  1. 它能连接本地和远程的目标 Java 应用, 执行 debug 命令.
  2. 它能连接本地和远程的 core dump 文件, 并获取信息.
  3. 它能启动本地要debug 的Java 应用, 并且debug.

    jdb -connect sun.jvm.hotspot.jdi.SAPIDAttachingConnector:pid=9302
    jdb -connect com.sun.jdi.SocketAttach:port=12306
    jdb -connect sun.jvm.hotspot.jdi.SACoreAttachingConnector:javaExecutable=$JAVA_HOME/bin/java,core=core.20441
    远程的 core dump 需要 jsadebugd. 

jdb 本地启动一个app 的背后

下面本地启动一个需要debug的app, 然后观察 jdb 是如何跟这个app交互的.

$ jdb FreeMemoryExample # 启动 jdb 
$ stop at FreeMemoryExample:8 # 设置断点
$ run # 启动应用. 

打开另外一个窗口, 然后观察如下:

$ jps #查看当前机器的 java 进程
3790574 TTY
3790626 FreeMemoryExample
3793734 Jps #jps 本身

$ sudo lsof -T -p  3790574 | grep 5598 #查看进程 3790574 里面连接的 tcp, 它的55985连到本地 55888
jdb     3790574 supra    6u  IPv6           37707266       0t0      TCP suprabox:55985->localhost:55888
$ sudo lsof -T -p 3790626 | grep 5598 #查看进程 3790626 里面连接的 tcp, 它的55888连到本地 55985
java    3790626 supra    4u  IPv4           37703541       0t0      TCP localhost:55888->suprabox:55985
$ ps aux | grep 3790626 #查看 3790626 的启动命令
supra    3790626  0.1  0.2 7021672 41712 pts/0   Sl+  00:06   0:01 /usr/lib/jvm/java-11-openjdk-amd64/bin/java -Xdebug -Xrunjdwp:transport=dt_socket,address=suprabox:55985,suspend=y FreeMemoryExample
$ ps aux | grep 3790574 #查看 3790574 的启动命令
supra    3790574  0.2  0.4 7619496 65040 pts/0   Sl+  00:06   0:02 jdb FreeMemoryExample

所以, 当运行 jdb FreeMemoryExample 的时候, 其实它调用java 进程另外启动了需要被 debug 的程序,并且建立了 tcp socket 通信.

被启动的进程使用了参数: -Xdebug -Xrunjdwp:transport=dt_socket,address=suprabox:55985,suspend=y

添加 debug 参数启动应用, 并连接

只要在对应的 Java 进程启动的时候, 添加类似如下参数, 就能启动debug 监听端口, 等待准备被连接:

$ java -agentlib:jdwp=transport=dt_socket,server=y,address=12306 FreeMemoryExample #启动应用端
# 更多参数参看: https://docs.oracle.com/javase/8/docs/technotes/guides/jpda/conninv.html

$ jdb -connect com.sun.jdi.SocketAttach:port=12306 # 启动jdb 并连接
 stop at FreeMemoryExample:8  # 设置断点
 run # 开始运行
 locals #显示本地变量
 where # 显示栈

使用 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 函数的参数 argsosBean 变量.

添加方法进入断点

我们可以看到下面讲要执行 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程序建立连接,并在程序执行时进行交互式的调试操作。

jdbconnectors 子命令能显示当前 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: