一、JDB調試
JDB是 The Java Debugger 的簡稱,它可以用來debug一個Java程序,同時它是 JPDA 的一個參考實現,只是這個實現是基於命令行的。
1.1、JPDA

JDWP 有兩種基本的包(packet)類型:命令包(command packet)和回復包(reply packet)。
在JDB里的例子中,我們使用JDB通過socket向本地的JVM發送JDWP請求。
Debugger 和 target Java 虛擬機都有可能發送 command packet。Debugger 通過發送 command packet 獲取 target Java 虛擬機的信息以及控制程序的執行。Target Java 虛擬機通過發送 command packet 通知 debugger 某些事件的發生,如到達斷點或是產生異常。
Reply packet 是用來回復 command packet 該命令是否執行成功,如果成功 reply packet 還有可能包含 command packet 請求的數據,比如當前的線程信息或者變量的值。從 target Java 虛擬機發送的事件消息是不需要回復的。
1.1.1、三個模塊的主要功能
Java 虛擬機工具接口(JVMTI)
JVMTI(Java Virtual Machine Tool Interface)即指 Java 虛擬機工具接口,它是一套由虛擬機直接提供的 native 接口,它處於整個 JPDA 體系的最底層,所有調試功能本質上都需要通過 JVMTI 來提供。通過這些接口,開發人員不僅調試在該虛擬機上運行的 Java 程序,還能查看它們運行的狀態,設置回調函數,控制某些環境變量,從而優化程序性能。我們知道,JVMTI 的前身是 JVMDI 和 JVMPI,它們原來分別被用於提供調試 Java 程序以及 Java 程序調節性能的功能。在 J2SE 5.0 之后 JDK 取代了 JVMDI 和 JVMPI 這兩套接口,JVMDI 在最新的 Java SE 6 中已經不提供支持,而 JVMPI 也計划在 Java SE 7 后被徹底取代。
Java 調試交互協議(JDWP)
JDWP(Java Debug Wire Protocol)是一個為 Java 調試而設計的一個通訊交互協議,它定義了調試器和被調試程序之間傳遞的信息的格式。在 JPDA 體系中,作為前端(front-end)的調試者(debugger)進程和后端(back-end)的被調試程序(debuggee)進程之間的交互數據的格式就是由 JDWP 來描述的,它詳細完整地定義了請求命令、回應數據和錯誤代碼,保證了前端和后端的 JVMTI 和 JDI 的通信通暢。比如在 Sun 公司提供的實現中,它提供了一個名為 jdwp.dll(jdwp.so)的動態鏈接庫文件,這個動態庫文件實現了一個 Agent,它會負責解析前端發出的請求或者命令,並將其轉化為 JVMTI 調用,然后將 JVMTI 函數的返回值封裝成 JDWP 數據發還給后端。
另外,這里需要注意的是 JDWP 本身並不包括傳輸層的實現,傳輸層需要獨立實現,但是 JDWP 包括了和傳輸層交互的嚴格的定義,就是說,JDWP 協議雖然不規定我們是通過 EMS 還是快遞運送貨物的,但是它規定了我們傳送的貨物的擺放的方式。在 Sun 公司提供的 JDK 中,在傳輸層上,它提供了 socket 方式,以及在 Windows 上的 shared memory 方式。當然,傳輸層本身無非就是本機內進程間通信方式和遠端通信方式,用戶有興趣也可以按 JDWP 的標准自己實現。
Java 調試接口(JDI)
JDI(Java Debug Interface)是三個模塊中最高層的接口,在多數的 JDK 中,它是由 Java 語言實現的。 JDI 由針對前端定義的接口組成,通過它,調試工具開發人員就能通過前端虛擬機上的調試器來遠程操控后端虛擬機上被調試程序的運行,JDI 不僅能幫助開發人員格式化 JDWP 數據,而且還能為 JDWP 數據傳輸提供隊列、緩存等優化服務。從理論上說,開發人員只需使用 JDWP 和 JVMTI 即可支持跨平台的遠程調試,但是直接編寫 JDWP 程序費時費力,而且效率不高。因此基於 Java 的 JDI 層的引入,簡化了操作,提高了開發人員開發調試程序的效率。
1.1.2、三個模塊的不同點
1.2、Jdb使用
用法: 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() 方法的參數
更多參數
Java遠程調試 -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,server=y,address=3999,suspend=n -XDebug 啟用調試。 -Xnoagent 禁用默認sun.tools.debug調試器。 -Djava.compiler=NONE 禁止 JIT 編譯器的加載。 -Xrunjdwp 加載JDWP的JPDA參考執行實例。 transport 用於在調試程序和 VM 使用的進程之間通訊。 dt_socket 套接字傳輸。 dt_shmem 共享內存傳輸,僅限於 Windows。 server=y/n VM 是否需要作為調試服務器執行。 address=3999 調試服務器的端口號,客戶端用來連接服務器的端口號。 suspend=y/n 是否在調試客戶端建立連接之后啟動 VM 。
1.3、進入Jdb后調試命令
-
添加斷點
stop at com.test.MyClass:22 stop in java.lang.String.length stop in com.test.MyClass.<init>#構造函數 stop in com.test.MyClass.<clinit>#靜態代碼塊
-
查看線程
threads #查看所有線程 thread <id> #查看單個線程 where #查看線程堆棧 pop #當前幀出棧, 且打印當前幀
-
單步調試
step #執行當前行 step up #一直執行, 直到當前方法返回到其調用方 stepi #執行當前指令 next #步進一行 (調用) cont #從斷點處繼續執行
-
查看變量
print <expr> #輸出表達式的值 dump <expr> #輸出所有對象信息 eval <expr> #對表達式求值 (與 print 相同) set <lvalue> = <expr> #向字段/變量/數組元素分配新值 locals #輸出當前堆棧幀中的所有本地變量
-
其他
list [line number|method] -- 輸出源代碼 use (或 sourcepath) [source file path] #顯示或更改源路徑(目錄) run #運行
1.4、調試方式

package com.lhx.cloud.javathread; public class JdbTest { public void fn(){ System.out.println("-----fn-----"); } }
package com.lhx.cloud.javathread; public class JdbMain { public static void main(String[] args) { JdbTest jdbTest=new JdbTest(); jdbTest.fn(); } }
1.4.1、交互式調試【本機調試】
jdb -XX:-UseCompressedOops -XX:+UseSerialGC --啟動jdb,可帶參數
run com.lhx.cloud.javathread.JdbMain
或
jdb com.lhx.cloud.javathread.JdbMain
run
注意:在window上可以成功,mac上此種方式調試失敗
1.4.2、JDWP【遠端連接調試】【mac可用】
java應用啟動時,增加啟動參數(linux和windows配置方式不一樣
Linux:
一個窗口開啟,使用下面命令
java -Xdebug -Xrunjdwp:transport=dt_socket,address=8888,server=y,suspend=y com.lhx.cloud.javathread.MarkWord.JdbMain
開啟另一個新窗口
jdb -attach <ip>:8888 #連接到JVM,本機IP即可省略
WIndows
一個窗口開啟,使用下面命令
java -Xdebug -Xrunjdwp:transport=dt_shmem,address=debug,server=y,suspend=y com.lhx.cloud.javathread.MarkWord.JdbMain
開啟另一個新窗口
jdb -attach 'debug' #連接到JVM,本機IP即可省略
當然以上方式同時可以配置在eclipse、idea等工具上
1.4.3、Connector
通過** jdb -listconnectors** 命令可以查看本機JDK支持的連接器;
注意:通過Connector不需要做任何額外配置,但調速器不能對進程做任何修改,也就是說類似進入只讀模式;
-
調試本地進程
jdb -connect sun.jvm.hotspot.jdi.SAPIDAttachingConnector:pid=44159
- 調試遠程進程
- 啟動SA Debug Server
jsadebugd <pid> [server-id]
如果啟動多個debug server,可以配置server-id;
- 連接到遠程SA Debug Server
jdb -connect sun.jvm.hotspot.jdi.SADebugServerAttachingConnector:debugServerName=machine1
注:machine1為機器名或IP
- 調試本機CoreDump
jdb -connect sun.jvm.hotspot.jdi.SACoreAttachingConnector:core= <core file>,javaExecutable=$JAVA_HOME/bin/java
- 調試遠程CoreDump
- 啟動SA Debug Server
jsadebugd $JAVA_HOME/bin/java core.20441
- 連接到遠程SA Debug Server
jdb -connect sun.jvm.hotspot.jdi.SADebugServerAttachingConnector:debugServerName=machine1
注:machine1為機器名或IP
二、通過HSDB來查看HotSpot VM的運行時數據
package com.lhx.cloud.javathread.MarkWord public class Test { static Test2 t1 = new Test2(); Test2 t2 = new Test2(); public void fn() { Test2 t3 = new Test2(); System.out.println("-----fnfnfn-----"); } } class Test2 { }
main方法
package com.lhx.cloud.javathread.MarkWord; public class Main { public static void main(String[] args) { System.out.println("start"); Test test = new Test(); test.fn(); System.out.println("end"); } }
java -Xdebug -Xrunjdwp:transport=dt_socket,address=8888,server=y,suspend=y com.lhx.cloud.javathread.MarkWord.Main
再打開另一個終端, attach上后,打一個行號斷點在輸出end代碼,具體行號即可。
jdb -attach 8888
stop at com.lhx.cloud.javathread.MarkWord.Main:13
java -XX:-UseCompressedOops -XX:+UseSerialGC -Xmx10m -Xdebug -Xrunjdwp:transport=dt_socket,address=8888,server=y,suspend=y com.x.cloud.javathread.MarkWord.Main
2.1、啟動
java -cp .;%JAVA_HOME%/lib/sa-jdi.jar sun.jvm.hotspot.HSDB
注意:Linux和Solaris在Oracle/Sun JDK6就可以使用HSDB了,但Windows上要到Oracle JDK7才可以用HSDB
2、mac啟動
echo $JAVA_HOME # 查看存在java_home
執行啟動命令
java -cp $JAVA_HOME/lib/sa-jdi.jar sun.jvm.hotspot.HSDB
如果沒有權限:sudo chmod -R 777 你的文件夾名【sa-jdi.jar】,啟動后顯示如下
如果內部連接無效:可以嘗試使用命令。異常信息【在Attach to HotSpot process時,sun.jvm.hotspot.debugger.DebuggerException:Can't attach to the process.……lack of privilege。】
sudo java -cp $JAVA_HOME/lib/sa-jdi.jar sun.jvm.hotspot.HSDB
2.2、進程連接以及使用

剛開始打開的窗口是Java Threads,里面有個線程列表。雙擊代表線程的行會打開一個Oop Inspector窗口顯示HotSpot VM里記錄線程的一些基本信息的C++對象的內容。
不過這里我們更可能會關心的是線程棧的內存數據。先選擇main線程,然后點擊Java Threads窗口里的工具欄按鈕從左數第2個可以打開Stack Memory窗口來顯示main線程的棧:
Stack Memory窗口的內容有三欄:
左起第1欄是內存地址,請讓我提醒一下本文里提到“內存地址”的地方都是指虛擬內存意義上的地址,不是“物理內存地址”,請不要弄混了這倆概念;
第2欄是該地址上存的數據,以字寬為單位,本文例子中我是在Windows 7 64-bit上跑64位的JDK7的HotSpot VM,字寬是64位(8字節);
第3欄是對數據的注釋,豎線表示范圍,橫線或斜線連接范圍與注釋文字。
2.2.2、console使用以及命令
在菜單里選擇Windows -> Console

然后會得到一個空白的Command Line窗口。在里面敲一下回車就會出現hsdb>提示符
可以通過help命令看看命令列表
assert true | false attach pid | exec core buildreplayjars [ all | app | boot ] | [ prefix ] class name classes detach dis address [length] disassemble address dumpcfg { -a | id } dumpclass { address | name } [ directory ] dumpcodecache dumpheap [ file ] dumpideal { -a | id } dumpilt { -a | id } dumpreplaydata { <address > | -a | <thread_id> } echo [ true | false ] examine [ address/count ] | [ address,address] field [ type [ name fieldtype isStatic offset address ] ] findpc address flags [ flag | -nd ] help [ command ] history inspect expression intConstant [ name [ value ] ] jdis address jhisto jseval script jsload file jstack [-v] livenmethods longConstant [ name [ value ] ] mem address [ length ] pmap print expression printall printas type expression printmdo [ -a | expression ] printstatics [ type ] pstack [-v] quit reattach revptrs address scanoops start end [ type ] search [ heap | perm | rawheap | codecache | threads ] value source filename symbol address symboldump symboltable name sysprops thread { -a | id } threads tokenize ... type [ type [ name super isOop isInteger isUnsigned size ] ] universe #查看GC堆的地址范圍和使用情況 verbose true | false versioncheck [ true | false ] vmstructsdump whatis address where { -a | id }
基本命令說明:
1、universe命令來查看GC堆的地址范圍和使用情況
hsdb> universe Heap Parameters: Gen 0: eden [0x0000000110400000,0x00000001104ab870,0x00000001106b0000) space capacity = 2818048, 24.93129996366279 used from [0x00000001106b0000,0x00000001106b0000,0x0000000110700000) space capacity = 327680, 0.0 used to [0x0000000110700000,0x0000000110700000,0x0000000110750000) space capacity = 327680, 0.0 usedInvocations: 0 Gen 1: old [0x0000000110750000,0x0000000110750000,0x0000000110e00000) space capacity = 7012352, 0.0 usedInvocations: 0
可以發現HotSpot在1.8的Java堆中,已經去除了Perm gen區,由youyoung gen和old gen組成。
2、scanoops 查看類型
Java代碼里,執行到Test.fn()末尾為止應該創建了3個Test2的實例,它們必然在GC堆里,但都在哪里,可以用scanoops命令來看:
hsdb> scanoops 0x0000000110400000 0x0000000110e00000 com.lhx.cloud.javathread.MarkWord.Test2 0x00000001104a5ec0 com/lhx/cloud/javathread/MarkWord/Test2 0x00000001104a5ee8 com/lhx/cloud/javathread/MarkWord/Test2 0x00000001104a5ef8 com/lhx/cloud/javathread/MarkWord/Test2
scanoops接受兩個必選參數和一個可選參數:必選參數是要掃描的地址范圍,一個是起始地址一個是結束地址;可選參數用於指定要掃描什么類型的對象實例。實際掃描的時候會掃出指定的類型及其派生類的實例。
左邊是對象的起始地址,右邊是對象的實際類型
從它們所在的地址,對照前面universe命令看到的GC堆的地址范圍,可以知道它們都在eden里。
3、whatis命令可以進一步知道它們都在eden之中分配給main線程的thread-local allocation buffer (TLAB)中
hsdb> whatis 0x00000001104a5ec0 Address 0x00000001104a5ec0: In thread-local allocation buffer for thread "main" (6659) [0x000000011049db70,0x00000001104a5fd0,0x00000001104ab858,{0x00000001104ab870})
如果是用Parallel GC,其實稍微改造一下Serviceability Agent的Java部分就可以讓whatis正確顯示了,其實就是上文在啟動時設置下GC方式
hsdb> whatis 0x000000076ab7a5b8
Address 0x000000076ab7a5b8: In unknown section of Java heap
4、inspect命令來查看對象的內容:
hsdb> inspect 0x00000001104a5ec0 instance of Oop for com/lhx/cloud/javathread/MarkWord/Test2 @ 0x00000001104a5ec0 @ 0x00000001104a5ec0 (size = 16) _mark: 1 _metadata._klass: InstanceKlass for com/lhx/cloud/javathread/MarkWord/Test2
為了方便klass地址顯示,在mac上禁用掉指針壓縮,即jdb -XX:-UseCompressedOops ,但此處沒生效,可以使用下面的men查看
可見一個Test2的實例要16字節。因為Test2類沒有任何Java層的實例字段,這里就沒有任何Java實例字段可顯示。
5、mem命令來看實際內存里的數據格式:
hsdb> mem 0x00000001104a5ec0 2 0x00000001104a5ec0: 0x0000000000000001 0x00000001104a5ec8: 0x0000000111201028
mem命令接受的兩個參數都必選,一個是起始地址,另一個是以字寬為單位的“長度”。我們知道一個Test2實例有16字節,所以給定長度為2來看。
mem詳細含義:
0x00000001104a5ec0: _mark: 0x0000000000000001 0x00000001104a5ec8: _metadata._compressed_klass: 0x0000000111201028
對於一個空的Test2的實例包含2個給VM用的隱含字段作為對象頭,和0個Java字段。
對象頭的第一個字段是mark word,記錄該對象的GC狀態、同步狀態、identity hash code之類的多種信息
對象頭的第二個字段是個類型信息指針,klass pointer。這里因為默認開啟了壓縮指針,所以本來應該是64位的指針存在了32位字段里。
最后還有4個字節是為了滿足對齊需求而做的填充(padding)
6、Inspector工具界面
結合4、5可以通過,在菜單里選Tools -> Inspector,在地址里輸入前面看到的klass地址:【5中第二項左側的地址】
Oop【原InstanceKlass】存着Java類型的名字、繼承關系、實現接口關系,字段信息,方法信息,運行時常量池的指針,還有內嵌的虛方法表(vtable)、接口方法表(itable)和記錄對象里什么位置上有GC會關心的指針(oop map)等等。
是給VM內部用的,並不直接暴露給Java層;InstanceKlass不是java.lang.Class的實例。
在HotSpot VM里,java.lang.Class的實例被稱為“Java mirror”,意思是它是VM內部用的klass對象的“鏡像”,把klass對象包裝了一層來暴露給Java層使用。
在InstanceKlass里有個_java_mirror字段引用着它對應的Java mirror,而mirror里也有個隱藏字段指向其對應的InstanceKlass。所以當我們寫obj.getClass(),在HotSpot VM里實際上經過了兩層間接引用才能找到最終的Class對象:
obj->_klass->_java_mirror
在Oracle JDK7之前,Oracle/Sun JDK的HotSpot VM把Java類的靜態變量存在InstanceKlass結構的末尾;從Oracle JDK7開始,為了配合PermGen移除的工作,Java類的靜態變量被挪到Java mirror(Class對象)的末尾了。
在JDK7之前Java mirror存放在PermGen里,而從JDK7開始Java mirror默認也跟普通Java對象一樣先從eden開始分配而不放在PermGen里。到JDK8則進一步徹底移除了PermGen,把諸如klass之類的元數據都挪到GC堆之外管理,而Java mirror的處理則跟JDK7一樣。
7、revptrs 反向指針
HotSpot VM內部使用直接指針來實現Java引用。在64位環境中有可能啟用“壓縮指針”的功能把64位指針壓縮到只用32位來存。壓縮指針與非壓縮指針直接有非常簡單的1對1對應關系,前者可以看作后者的特例。
如果要找t1、t2、t3這三個變量,等同於找出存有指向上述3個Test2實例的地址的存儲位置。
“反向指針”——如果a變量引用着b對象,那么從b對象出發去找a變量就是找一個“反向指針”。
※、查詢第一個test2實例:
hsdb> revptrs 0x00000001104a5ec0 Computing reverse pointers... Done. null Oop for java/lang/Class @ 0x00000001104a35c8
找到了一個包含指向Test2實例的指針,在一個java.lang.Class的實例里。
用whatis命令來看看這個Class對象在哪里:
hsdb> whatis 0x00000001104a35c8 Address 0x00000001104a35c8: In thread-local allocation buffer for thread "main" (6659) [0x000000011049db70,0x00000001104a5fd0,0x00000001104ab858,{0x00000001104ab870})
可以看到這個Class對象也在eden里,具體來說在main線程的TLAB里。
這個Class對象是如何引用到Test2的實例的呢?再用inspect命令:
hsdb> inspect 0x00000001104a35c8 instance of Oop for java/lang/Class @ 0x00000001104a35c8 @ 0x00000001104a35c8 (size = 168) <<Reverse pointers>>: t1: Oop for com/lhx/cloud/javathread/MarkWord/Test2 @ 0x00000001104a5ec0 Oop for com/lhx/cloud/javathread/MarkWord/Test2 @ 0x00000001104a5ec0 hsdb>
可以看到,這個Class對象里存着Test類的靜態變量t1,指向着第一個Test2實例。 【這里沒有對象頭】
本來JVM規范里也沒明確規定靜態變量要存在哪里,通常認為它應該在概念中的“方法區”里;但現在在JDK7的HotSpot VM里它實質上也被放在Java heap里了。可以把這種特例看作是HotSpot VM把方法區的一部分數據也放在Java heap里了。
前面也已經提過,在JDK7之前的Oracle/Sun JDK里的HotSpot VM把靜態變量存在InstanceKlass末尾,存在PermGen里。那個時候的PermGen更接近於完整的方法區一些。
※、查詢第二個test2實例:
依次通過,revptrs,whatis,inspect命令查看
hsdb> revptrs 0x00000001104a5ee8 Oop for com/lhx/cloud/javathread/MarkWord/Test @ 0x00000001104a5ed0 hsdb> whatis 0x00000001104a5ed0 Address 0x00000001104a5ed0: In thread-local allocation buffer for thread "main" (6659) [0x000000011049db70,0x00000001104a5fd0,0x00000001104ab858,{0x00000001104ab870}) hsdb> inspect 0x00000001104a5ed0 instance of Oop for com/lhx/cloud/javathread/MarkWord/Test @ 0x00000001104a5ed0 @ 0x00000001104a5ed0 (size = 24) <<Reverse pointers>>: _mark: 1 _metadata._klass: InstanceKlass for com/lhx/cloud/javathread/MarkWord/Test t2: Oop for com/lhx/cloud/javathread/MarkWord/Test2 @ 0x00000001104a5ee8 Oop for com/lhx/cloud/javathread/MarkWord/Test2 @ 0x00000001104a5ee8
也在main線程的TLAB里,可以看到這個Test實例里有個成員字段t2,指向了第二個Test2實例。
※、查詢第三個test2實例:
hsdb> revptrs 0x00000001104a5ef8
no live references to 0x00000001104a5ef8
查看線程棧
更多使用:https://rednaxelafx.iteye.com/blog/1847971