程序開發(fā)過(guò)程中许赃,debug 是必不可少的一部分跟压,它能幫助我們及時(shí)發(fā)現(xiàn)一些不易察覺的 bug,但并不是所有的 bug 都能有幸在開發(fā)過(guò)程中就被發(fā)現(xiàn)塞茅,當(dāng)程序被部署到遠(yuǎn)程服務(wù)器后亩码,bug 的排查可能就不那么輕松了。
開發(fā)過(guò)程中常常會(huì)發(fā)現(xiàn)程序在我們本地運(yùn)行的時(shí)候一切正常野瘦,但在測(cè)試環(huán)境或生產(chǎn)環(huán)境會(huì)出現(xiàn)不可預(yù)測(cè)的問(wèn)題描沟,也就是一些潛在的 bug 在特定的環(huán)境下才會(huì)暴露出來(lái)飒泻,可能是數(shù)據(jù)引起的,也可能是其他不確定的因素吏廉。此時(shí)通常的方法可能就是通過(guò)打印出更加詳細(xì)的日志再進(jìn)行分析泞遗,而日志的詳細(xì)粒度也往往不容易把控,過(guò)多會(huì)增加分析的復(fù)雜度席覆,過(guò)少又不易于發(fā)現(xiàn)問(wèn)題史辙。總之沒有在本地 debug 來(lái)的痛快佩伤。有同學(xué)可能會(huì)說(shuō)聊倔,”我們可以讓遠(yuǎn)程 JVM 在啟動(dòng)的時(shí)候加載 JDWP Agent,然后在本地 IDE 中指定端口進(jìn)行遠(yuǎn)程連接生巡,從而進(jìn)行調(diào)試“耙蔑。不可否認(rèn),這是最理想的方案障斋,但現(xiàn)實(shí)往往有點(diǎn)骨感纵潦,起碼在我們公司,這個(gè)過(guò)程會(huì)讓人崩潰垃环。我們所有的服務(wù)器都部署在北美邀层,由于網(wǎng)絡(luò)原因,讓 IDE 與遠(yuǎn)程 JVM 建立連接就需要消耗一點(diǎn)時(shí)間遂庄,之后服務(wù)器上的程序可能早就已經(jīng)跑到斷點(diǎn)了寥院,而本地 IDE 還沒有及時(shí)反應(yīng)過(guò)來(lái)。這種卡頓涛目、延遲現(xiàn)象讓調(diào)試過(guò)程來(lái)的相當(dāng)痛苦秸谢,還不如直接去分析日志。
換個(gè)思路:
“難道我們不能直接在遠(yuǎn)程服務(wù)器上直接進(jìn)行調(diào)試嗎霹肝?”
有同學(xué)可能會(huì)說(shuō):
“服務(wù)器通常是沒有桌面的估蹄,如何在上面使用 IDE?”
這其實(shí)還是思維的固化沫换,IDE 提供的 Debugger(調(diào)試器)其實(shí)就是 Java Debug Interface(JDI)的一個(gè)實(shí)現(xiàn)臭蚁,比如我們?cè)偈煜げ贿^(guò)的Eclipse,它的兩個(gè)插件org.eclipse.jdt.debug.ui
和org.eclipse.jdt.debug
讯赏,前者是Debugger的界面實(shí)現(xiàn)垮兑,后者就是JDI的一個(gè)完整實(shí)現(xiàn)。而 JDK 自帶的jdb
也是 JDI 的一個(gè)實(shí)現(xiàn)漱挎,所以我們完全可以直接使用這個(gè)自帶工具進(jìn)行調(diào)試系枪。
Java Debugger(JDB)是一個(gè)用來(lái)調(diào)試Java類文件的命令行工具,它跟 Eclipse磕谅、Intellij 等 IDE 里的調(diào)試器一樣私爷,都是 Java Platform Debugger Architecture - JPDA 三大模塊中最高層模塊 JDI 的完整實(shí)現(xiàn)雾棺。
JPDA - Java 調(diào)試體系
說(shuō)到這里,我們有必要先簡(jiǎn)單了解一下 JPDA当犯。JPDA 由三個(gè)相對(duì)獨(dú)立的模塊組成垢村,由低到高分別是 JVM 工具接口(JVMTI)、Java 調(diào)試線協(xié)議(JDWP)嚎卫、Java 調(diào)試接口(JDI)嘉栓,層次結(jié)構(gòu)如下圖:
1. JVMTI
處于整個(gè) JPDA 體系的最底層的 Java 虛擬機(jī)工具接口,是一套由虛擬機(jī)直接提供的 native 接口拓诸,由 C 語(yǔ)言實(shí)現(xiàn)侵佃,所有調(diào)試功能本質(zhì)上都需要通過(guò) JVMTI 來(lái)提供。通過(guò)這些接口奠支,開發(fā)人員不僅調(diào)試在該虛擬機(jī)上運(yùn)行的 Java 程序馋辈,還能查看它們運(yùn)行的狀態(tài),設(shè)置回調(diào)函數(shù)倍谜,控制某些環(huán)境變量迈螟,從而優(yōu)化程序性能。
2. JDWP
一個(gè)通訊交互協(xié)議尔崔,定義了調(diào)試器與目標(biāo)虛擬機(jī)之間傳遞的信息的格式答毫,包括請(qǐng)求命令、回應(yīng)數(shù)據(jù)和錯(cuò)誤代碼季春。同樣也是由 C 語(yǔ)言實(shí)現(xiàn)洗搂。
3. JDI
三個(gè)模塊中最高層的接口,在多數(shù)的 JDK 中载弄,它是由 Java 語(yǔ)言實(shí)現(xiàn)的耘拇。 通過(guò)它,調(diào)試工具開發(fā)人員就能通過(guò)調(diào)試器來(lái)遠(yuǎn)程操控目標(biāo)虛擬機(jī)上被調(diào)試程序的運(yùn)行宇攻。
Java Debugger(JDB)
下面我們來(lái)了解一下這個(gè) JDK 自帶工具的使用方法惫叛。JDB 提供了多種連接目標(biāo)程序 JVM 的方式,這里介紹最常用的兩種逞刷。
1. 由 jdb
命令創(chuàng)建目標(biāo) JVM
這種方式下挣棕,jdb
命令直接為目標(biāo) Java 程序啟動(dòng)一個(gè) JVM,加載類信息亲桥,即程序的啟動(dòng)是由 jdb
命令直接觸發(fā)的,啟動(dòng)成功后固耘,目標(biāo) JVM 就會(huì)被暫停题篷,等待用戶輸入命令來(lái)讓程序得以執(zhí)行。這就是我們?cè)诒镜赜?IDE 進(jìn)行 debug 的方式厅目。
比如用 JDB 調(diào)試如下程序:
// 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);
}
}
編譯上面兩個(gè)源文件后番枚,在控制臺(tái)進(jìn)行調(diào)試:
- 通過(guò)
jdb <主類的全路徑名>
啟動(dòng) JVM法严,這個(gè)示例當(dāng)中就是jdb demo.Main
dereck-mbp:temp Dereck$ jdb demo.Main 正在初始化jdb... >
- 設(shè)置斷點(diǎn),兩種方式
對(duì)于本例葫笼,我們通過(guò)方法名的方式給 add() 加斷點(diǎn)深啤,執(zhí)行命令如下,斷點(diǎn)會(huì)設(shè)置在方法的第一行> stop ? 用法: stop at <class>:<line_number> 或 stop in <class>.<method_name>[(argument_type,...)]
> stop in demo.Test.add 正在延遲斷點(diǎn)demo.Test.add路星。 將在加載類后設(shè)置溯街。 >
- 通過(guò)
run
命令運(yùn)行程序,它會(huì)自動(dòng)從 main() 執(zhí)行洋丐,一直到斷點(diǎn)處暫停呈昔,等待用戶輸入后續(xù)命令。run
命令只適用于由jdb
直接創(chuàng)建啟動(dòng) JVM 的方式> run 運(yùn)行demo.Main 設(shè)置未捕獲的java.lang.Throwable 設(shè)置延遲的未捕獲的java.lang.Throwable > VM 已啟動(dòng): 設(shè)置延遲的斷點(diǎn)demo.Test.add 斷點(diǎn)命中: "線程=main", demo.Test.add(), 行=8 bci=0 8 return base + a; main[1]
- 通過(guò)
print
或dump
命令查看此時(shí)指定變量的值友绝,print
用于查看簡(jiǎn)單類型堤尾,dump
用于查看對(duì)象類型
如果執(zhí)行main[1] print a a = 2 main[1] print this this = "demo.Test@41975e01" main[1] dump this this = { base: 1 } main[1]
print
命令查看方法參數(shù)a
報(bào)如下錯(cuò)誤(“未知變量名
a”)的話,需要在編譯的時(shí)候迁客,給javac
加一個(gè)參數(shù)-g
郭宝, 比如javac -g demo/Test.java
main[1] print a com.sun.tools.example.debug.expr.ParseException: Name unknown: a a = 空值 main[1]
- 通過(guò)
step
、next
或cont
命令繼續(xù)執(zhí)行程序-
step
命令相當(dāng)于 Eclipse 當(dāng)中的 F5掷漱,如果當(dāng)前語(yǔ)句是另一個(gè)方法調(diào)用時(shí)粘室,會(huì)進(jìn)入那個(gè)方法當(dāng)中 -
next
命令相當(dāng)于 F6,只會(huì)逐行執(zhí)行切威,不會(huì)進(jìn)入被調(diào)用的其它方法 -
cont
命令相當(dāng)于 F8育特,從當(dāng)前行一直執(zhí)行到下一個(gè)斷點(diǎn),如果沒有就一直執(zhí)行到程序結(jié)束
main[1] cont > 3 應(yīng)用程序已退出 dereck-mbp:temp Dereck$
-
更多 JDB 命令請(qǐng)參考 Oracle 官方文檔
2. 由 jdb
命令 attach 到已經(jīng)處于運(yùn)行狀態(tài)的目標(biāo) JVM
這種方式適用于遠(yuǎn)程調(diào)試先朦,也是我們直接在遠(yuǎn)程服務(wù)器上進(jìn)行 debug 的方式缰冤。 它需要目標(biāo) JVM 自身在啟動(dòng)的時(shí)候傳入一些額外的參數(shù),大致格式如下:
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=<PORT> <主類的全路徑名>
其中 address
參數(shù)可選喳魏,如果不指定的話會(huì)隨機(jī)分配一個(gè)可用端口棉浸。
為了演示,這里用 Spring Initializr 創(chuàng)建了一個(gè)簡(jiǎn)單的 Web Application : Helloworld刺彩,除了生成的代碼迷郑,新建了一個(gè)簡(jiǎn)單的 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;
}
}
- 通過(guò) Maven 構(gòu)建打包成 jar 文件
mvn package
- 打包成功后,啟動(dòng) WEB 應(yīng)用
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n -jar helloworld-0.0.1-SNAPSHOT.jar
- 啟動(dòng)成功后创倔,日志中會(huì)打印出提供給 jdb 連接的端口嗡害,因?yàn)槲覀冎拔粗付ǎ赃@里隨機(jī)分配了一個(gè)
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
- 通過(guò)
jdb
命令連接到正在運(yùn)行的 JVMdereck-mbp:~ Dereck$ jdb -connect com.sun.jdi.SocketAttach:port=51750 設(shè)置未捕獲的java.lang.Throwable 設(shè)置延遲的未捕獲的java.lang.Throwable 正在初始化jdb... >
- 后續(xù)設(shè)置斷點(diǎn)及輸入命令進(jìn)行調(diào)試的步驟與方式1一樣畦攘,這里就不在累贅了
這種方式?jīng)]有因?yàn)榫W(wǎng)絡(luò)原因而導(dǎo)致的卡頓霸妹、延遲現(xiàn)象,但操作起來(lái)可能比較復(fù)雜知押,不是很直觀叹螟,但對(duì)于在不改變系統(tǒng)運(yùn)行環(huán)境鹃骂、又沒有詳細(xì) log 的情況下快速進(jìn)行問(wèn)題的排查還是有一定的幫助的。不過(guò)罢绽,不要在生產(chǎn)環(huán)境使用畏线,因?yàn)橐坏┻M(jìn)入斷點(diǎn),程序就會(huì)被中斷了