使用JDB直接在远程服务器上进行调试

程序开发过程中,debug 是必不可少的一部分,它能帮助我们及时发现一些不易察觉的 bug,但并不是所有的 bug 都能有幸在开发过程中就被发现,当程序被部署到远程服务器后,bug 的排查可能就不那么轻松了。

开发过程中常常会发现程序在我们本地运行的时候一切正常,但在测试环境或生产环境会出现不可预测的问题,也就是一些潜在的 bug 在特定的环境下才会暴露出来,可能是数据引起的,也可能是其他不确定的因素。此时通常的方法可能就是通过打印出更加详细的日志再进行分析,而日志的详细粒度也往往不容易把控,过多会增加分析的复杂度,过少又不易于发现问题。总之没有在本地 debug 来的痛快。有同学可能会说,”我们可以让远程 JVM 在启动的时候加载 JDWP Agent,然后在本地 IDE 中指定端口进行远程连接,从而进行调试“。不可否认,这是最理想的方案,但现实往往有点骨感,起码在我们公司,这个过程会让人崩溃。我们所有的服务器都部署在北美,由于网络原因,让 IDE 与远程 JVM 建立连接就需要消耗一点时间,之后服务器上的程序可能早就已经跑到断点了,而本地 IDE 还没有及时反应过来。这种卡顿、延迟现象让调试过程来的相当痛苦,还不如直接去分析日志。

换个思路:

“难道我们不能直接在远程服务器上直接进行调试吗?”

有同学可能会说:

“服务器通常是没有桌面的,如何在上面使用 IDE?”

这其实还是思维的固化,IDE 提供的 Debugger(调试器)其实就是 Java Debug Interface(JDI)的一个实现,比如我们再熟悉不过的Eclipse,它的两个插件org.eclipse.jdt.debug.uiorg.eclipse.jdt.debug,前者是Debugger的界面实现,后者就是JDI的一个完整实现。而 JDK 自带的jdb也是 JDI 的一个实现,所以我们完全可以直接使用这个自带工具进行调试。

Java Debugger(JDB)是一个用来调试Java类文件的命令行工具,它跟 Eclipse、Intellij 等 IDE 里的调试器一样,都是 Java Platform Debugger Architecture - JPDA 三大模块中最高层模块 JDI 的完整实现。

JPDA - Java 调试体系

说到这里,我们有必要先简单了解一下 JPDA。JPDA 由三个相对独立的模块组成,由低到高分别是 JVM 工具接口(JVMTI)、Java 调试线协议(JDWP)、Java 调试接口(JDI),层次结构如下图:

JPDA - Java调试体系

1. JVMTI

处于整个 JPDA 体系的最底层的 Java 虚拟机工具接口,是一套由虚拟机直接提供的 native 接口,由 C 语言实现,所有调试功能本质上都需要通过 JVMTI 来提供。通过这些接口,开发人员不仅调试在该虚拟机上运行的 Java 程序,还能查看它们运行的状态,设置回调函数,控制某些环境变量,从而优化程序性能。

2. JDWP

一个通讯交互协议,定义了调试器与目标虚拟机之间传递的信息的格式,包括请求命令、回应数据和错误代码。同样也是由 C 语言实现。

3. JDI

三个模块中最高层的接口,在多数的 JDK 中,它是由 Java 语言实现的。 通过它,调试工具开发人员就能通过调试器来远程操控目标虚拟机上被调试程序的运行。

Java Debugger(JDB)

下面我们来了解一下这个 JDK 自带工具的使用方法。JDB 提供了多种连接目标程序 JVM 的方式,这里介绍最常用的两种。

1. 由 jdb 命令创建目标 JVM

这种方式下,jdb 命令直接为目标 Java 程序启动一个 JVM,加载类信息,即程序的启动是由 jdb 命令直接触发的,启动成功后,目标 JVM 就会被暂停,等待用户输入命令来让程序得以执行。这就是我们在本地用 IDE 进行 debug 的方式。
比如用 JDB 调试如下程序:

// Test.java

package demo;

public class Test {
    private int base = 1;
  
    public int add(int a) {
        return base + a;
    }
}
// Main.java

package demo;

public class Main {

    public static void main(String[] args) {
        Test t = new Test();
        int result = t.add(2);
        System.out.println(result);
    }

}

编译上面两个源文件后,在控制台进行调试:

  • 通过 jdb <主类的全路径名> 启动 JVM,这个示例当中就是 jdb demo.Main
    dereck-mbp:temp Dereck$ jdb demo.Main
    正在初始化jdb...
    > 
    
  • 设置断点,两种方式
    > stop ?
    用法: stop at <class>:<line_number> 或
         stop in <class>.<method_name>[(argument_type,...)]
    
    对于本例,我们通过方法名的方式给 add() 加断点,执行命令如下,断点会设置在方法的第一行
    > stop in demo.Test.add
    正在延迟断点demo.Test.add。
    将在加载类后设置。
    > 
    
  • 通过 run 命令运行程序,它会自动从 main() 执行,一直到断点处暂停,等待用户输入后续命令。run 命令只适用于由 jdb 直接创建启动 JVM 的方式
    > run
    运行demo.Main
    设置未捕获的java.lang.Throwable
    设置延迟的未捕获的java.lang.Throwable
    > 
    VM 已启动: 设置延迟的断点demo.Test.add
    
    断点命中: "线程=main", demo.Test.add(), 行=8 bci=0
    8            return base + a;
    
    main[1] 
    
  • 通过 printdump 命令查看此时指定变量的值,print 用于查看简单类型,dump 用于查看对象类型
    main[1] print a
     a = 2
    main[1] print this
     this = "demo.Test@41975e01"
    main[1] dump this
     this = {
        base: 1
    }
    main[1] 
    
    如果执行 print 命令查看方法参数 a 报如下错误(“未知变量名
    a”)的话,需要在编译的时候,给 javac 加一个参数 -g, 比如 javac -g demo/Test.java
    main[1] print a
    com.sun.tools.example.debug.expr.ParseException: Name unknown: a
     a = 空值
    main[1] 
    
  • 通过 stepnextcont 命令继续执行程序
    • step 命令相当于 Eclipse 当中的 F5,如果当前语句是另一个方法调用时,会进入那个方法当中
    • next 命令相当于 F6,只会逐行执行,不会进入被调用的其它方法
    • cont 命令相当于 F8,从当前行一直执行到下一个断点,如果没有就一直执行到程序结束
    main[1] cont
    > 3
    
    应用程序已退出
    dereck-mbp:temp Dereck$ 
    

更多 JDB 命令请参考 Oracle 官方文档

2. 由 jdb 命令 attach 到已经处于运行状态的目标 JVM

这种方式适用于远程调试,也是我们直接在远程服务器上进行 debug 的方式。 它需要目标 JVM 自身在启动的时候传入一些额外的参数,大致格式如下:

java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=<PORT> <主类的全路径名> 

其中 address 参数可选,如果不指定的话会随机分配一个可用端口。
为了演示,这里用 Spring Initializr 创建了一个简单的 Web Application : Helloworld,除了生成的代码,新建了一个简单的 HelloworldController.java

package com.example.helloworld.helloworld;

import org.springframework.stereotype.*;
import org.springframework.web.bind.annotation.*;


@Controller
public class HelloworldController {

    private final String message = "helloworld";

    @RequestMapping("/")
    @ResponseBody
    String home() {
        return message;
    }
  
}
  • 通过 Maven 构建打包成 jar 文件
    mvn package
    
  • 打包成功后,启动 WEB 应用
    java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n -jar helloworld-0.0.1-SNAPSHOT.jar
    
  • 启动成功后,日志中会打印出提供给 jdb 连接的端口,因为我们之前未指定,所以这里随机分配了一个
    dereck-mbp:target Dereck$ java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n -jar helloworld-0.0.1-SNAPSHOT.jar 
    Listening for transport dt_socket at address: 51750
    
  • 通过 jdb 命令连接到正在运行的 JVM
    dereck-mbp:~ Dereck$ jdb -connect com.sun.jdi.SocketAttach:port=51750
    设置未捕获的java.lang.Throwable
    设置延迟的未捕获的java.lang.Throwable
    正在初始化jdb...
    > 
    
  • 后续设置断点及输入命令进行调试的步骤与方式1一样,这里就不在累赘了

这种方式没有因为网络原因而导致的卡顿、延迟现象,但操作起来可能比较复杂,不是很直观,但对于在不改变系统运行环境、又没有详细 log 的情况下快速进行问题的排查还是有一定的帮助的。不过,不要在生产环境使用,因为一旦进入断点,程序就会被中断了

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,500评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,599评论 18 139
  • 集群前后台协议需要做一些修改,我负责jdbc这边的修改。按照协议内容修改完代码之后却面临一个测试的问题:修改后的后...
    德彪阅读 3,666评论 0 2
  • 每次出去观看一部影片,就给做贼似的,不想让孩子和婆婆妈知道,因为出去看电影不带孩子,我自己就觉得有愧。可他提前就订...
    任小艺阅读 1,560评论 1 3
  • 李特这特题目有点蛋疼,因为目前只接受一种结果。我做的恰好和它要的结果不一样,但是我觉得我这种走法走出来也是没错的。...
    土汪阅读 480评论 0 1