HSDB(Hotspot Debugger)酝枢,是一款內置于 SA 中的 GUI 調試工具匈仗,可用于調試 JVM 運行時數據瓢剿,從而進行故障排除
啟動HSDB
檢測不同 JDK 版本需要使用不同的 HSDB 版本,否則容易出現無法掃描到對象等莫名其妙的問題
- Mac:JDK7 和 JDK8 均可以采用以下的方式
$ sudo java -cp ,:/Library/Java/JavaVirtualMachines/jdk1.7.0_80.jdk/Contents/Home/lib/sa-jdi.jar sun.jvm.hotspot.HSDB
發(fā)現在我的mac上jdk版本各種報錯悠轩,最后無解间狂,直接安裝了jdk11, mac上配置多版本jdk
而 JDK11 的啟動方式有些區(qū)別
$/Library/Java/JavaVirtualMachines/jdk-11.0.6.jdk/Contents/Home/bin/jhsdb hsdb
其中啟動版本可以使用 /usr/libexec/java_home -V 獲取
HSDB 對 Serial GC 支持的較好,因此 Debug 時增加參數 -XX:+UseSerialGC火架。注意運行程序java的版本和hsdb的java版本要一致才行鉴象。
獲取應用進程id
jps 僅查找當前用戶的 Java 進程,而不是當前系統(tǒng)中的所有進程
jps
- 默認顯示 pid 以及 main 方法對應的 class 名稱
- -v:輸出傳遞給 JVM 的參數
- -l: 輸出 main 方法對應的 class 的完整 package 名
CLHSDB常用指令
universe:查看堆空間信息
scanoops start end [type]:掃描指定空間中的 type 類型及其子類的實例
JDK8 版本的 HSDB 的 scanoops 會無法掃描到對象何鸡,但可以通過 GUI 界面的 Tools -> Object Histogram纺弊,輸入想要查詢的對象,之后雙擊來獲取對象的地址骡男,也可以繼續(xù)在里面點擊 inspect 來查看對象信息
- inspect:查看對象(OOP)信息【使用 tools->inspect淆游,輸入對象地址有更詳細的信息哦】
HSDB GUI界面
可視化線程棧
對象直方圖
Tools -> Object Histogram,我們可以通過對象直方圖快速定位某個類型的對象的地址以供我們進一步分析
OOP信息
我們可以根據對象地址在 Tools -> Inspector 獲取對象的在 JVM 層的實例 instanceOopDesc 對象,它包括對象頭 _mark 和 _metadata 以及實例信息
元數據區(qū)
HotSpot VM 里有一套對象專門用來存放元數據稽犁,它們包括:
- Klass 系對象,用于描述類型的總體信息【通過 OOP 信息(inspect)可以看到 instanceKlass 對象】
- ConstantPool/ConstantPoolCache 對象:每個 InstanceKlass 關聯著一個 ConstantPool骚亿,作為該類型的運行時常量池已亥。這個常量池的結構跟 Class 文件里的常量池基本上是對應的
- Method 對象,用來描述 Java 方法的總體信息来屠,如方法入口地址虑椎、調用/循環(huán)計數器等等
- ConstMethod 對象,記錄著 Java 方法的不變的描述信息俱笛,包括方法名捆姜、方法的訪問修飾符、字節(jié)碼迎膜、行號表泥技、局部變量表等等。注意磕仅,字節(jié)碼指令被分配在 constMethodOop 對象的內存區(qū)域的末尾
- MethodData 對象珊豹,記錄著 Java 方法執(zhí)行時的 profile 信息,例如某方法里的某個字節(jié)碼之類是否從來沒遇到過 null榕订,某個條件跳轉是否總是走同一個分支店茶,等等。這些信息在解釋器(多層編譯模式下也在低層的編譯生成的代碼里)收集劫恒,然后供給 HotSpot Server Compiler 用于做激進優(yōu)化贩幻。
- ConstMethod 對象,記錄著 Java 方法的不變的描述信息俱笛,包括方法名捆姜、方法的訪問修飾符、字節(jié)碼迎膜、行號表泥技、局部變量表等等。注意磕仅,字節(jié)碼指令被分配在 constMethodOop 對象的內存區(qū)域的末尾
- Symbol 對象,對應 Class 文件常量池里的 JVM_CONSTANT_Utf8 類型的常量两嘴。有一個 VM 全局的 SymbolTable 管理著所有 Symbol丛楚。Symbol 由所有 Java 類所共享。
實例分析
public class Test {
static Test2 t1 = new Test2();
Test2 t2 = new Test2();
public void fn() {
Test2 t3 = new Test2();
}
}
class Test2 {
}
這個程序的t1憔辫、t2鸯檬、t3三個變量本身(而不是這三個變量所指向的對象)到底在哪里。
t1在存Java靜態(tài)變量的地方螺垢,概念上在JVM的方法區(qū)(method area)里
t2在Java堆里喧务,作為Test的一個實例的字段存在
t3在Java線程的調用棧里,作為Test.fn()的一個局部變量存在
不過就這么簡單的回答大家都會枉圃,滿足不了對JVM的實現感興趣的同學們的好奇心功茴。說到底,這“方法區(qū)”到底是啥孽亲?Java堆在哪里坎穿?Java線程的調用棧又是啥樣的?
那就讓我們跑點例子,借助調試器來看看在一個實際運行中的JVM里是啥狀況玲昧。
寫個啟動類來跑上面問題中的代碼:
public class Main {
public static void main(String[] args) {
Test test = new Test();
test.fn();
}
}
Serviceability Agent是個非常便于探索HotSpot VM內部實現的API, 而HSDB則是在SA基礎上包裝起來的一個調試器栖茉。這次我們就用HSDB來做實驗。 SA的一個限制是它只實現了調試snapshot的功能:要么要讓被調試的目標進程完全暫停孵延,要么就調試core dump吕漂。所以我們在用HSDB做實驗前,得先讓我們的Java程序運行到我們關注的點上才行尘应。 理想情況下我們會希望讓這Java程序停在Test.java的第6行惶凝,也就是Test.fn()中t3局部變量已經進入作用域,而該方法又尚未返回的地方犬钢。怎樣才能停在這里呢苍鲜?
其實用個Java層的調試器即可。大家平時可能習慣了在Eclipse玷犹、IntelliJ IDEA混滔、NetBeans等Java IDE里使用Java層調試器,但為了減少對外部工具的依賴歹颓,本文將使用Oracle JDK自帶的jdb工具來完成此任務遍坟。
jdb跟上面列舉的IDE里包含的調試器底下依賴著同一套調試API,也就是Java Platform Debugger Architecture (JPDA)功能也類似晴股,只是界面是命令行的愿伴,表明上看起來不太一樣而已。
為了方便后續(xù)步驟电湘,啟動jdb的時候可以設定讓目標Java程序使用serial GC和10MB的Java heap隔节。
啟動jdb之后可以用stop in命令在指定的Java方法入口處設置斷點,
然后用run命令指定主類名稱來啟動Java程序寂呛,
等跑到斷點看看位置是否已經到滿足需求怎诫,還沒到的話可以用step、next之類的命令來向前進贷痪。
具體步驟如下:
> stop in jvm.hsdb.Test.fn
正在延遲斷點jvm.hsdb.Test.fn幻妓。
將在加載類后設置。
> run jvm.hsdb.Main
運行 jvm.hsdb.Main
設置未捕獲的java.lang.Throwable
設置延遲的未捕獲的java.lang.Throwable
>
VM 已啟動: 設置延遲的斷點jvm.hsdb.Test.fn
斷點命中: "線程=main", jvm.hsdb.Test.fn(), 行=7 bci=0
7 Test2 t3 = new Test2();
main[1] next
>
已完成的步驟: "線程=main", jvm.hsdb.Test.fn(), 行=8 bci=8
8 }
main[1]
按照上述步驟執(zhí)行完最后一個next命令之后劫拢,我們就來到了最初想要的Test.java的第8行肉津,也就是Test.fn()返回前的位置。
接下來把這個jdb窗口放一邊舱沧,另開一個命令行窗口用jps命令看看我們要調試的Java進程的pid是多少: 4981
4994 SALauncher
5266 Jps
4981 Main
1734 Launcher
4972 TTY
然后啟動HSDB:
$/Library/Java/JavaVirtualMachines/jdk-11.0.6.jdk/Contents/Home/bin/jhsdb hsdb
啟動HSDB之后妹沙,把它連接到目標進程上。從菜單里選擇File -> Attach to HotSpot process:
在彈出的對話框里輸入剛才記下的pid然后按OK:
這會兒就連接到目標進程了:
剛開始打開的窗口是Java Threads熟吏,里面有個線程列表距糖。雙擊代表線程的行會打開一個Oop Inspector窗口顯示HotSpot VM里記錄線程的一些基本信息的C++對象的內容玄窝。
不過這里我們更可能會關心的是線程棧的內存數據。先選擇main線程悍引,然后點擊Java Threads窗口里的工具欄按鈕從左數第2個可以打開Stack Memory窗口來顯示main線程的棧:
Stack Memory窗口的內容有三欄:
左起第1欄是內存地址恩脂,請讓我提醒一下本文里提到“內存地址”的地方都是指虛擬內存意義上的地址,不是“物理內存地址”趣斤,請不要弄混了這倆概念俩块;
第2欄是該地址上存的數據,以字寬為單位
第3欄是對數據的注釋唬渗,豎線表示范圍,橫線或斜線連接范圍與注釋文字奋渔。
現在讓我們打開HSDB里的控制臺镊逝,以便用命令來了解更多信息。
在菜單里選擇Windows -> Console:
然后會得到一個空白的Command Line窗口嫉鲸。在里面敲一下回車就會出現hsdb>提示符撑蒜。
(用過CLHSDB的同學可能會發(fā)現這就是把CLHSDB嵌入在了HSDB的圖形界面里)
不知道有什么命令可用的同學可以先用help命令看看命令列表。
可以用universe命令來查看GC堆的地址范圍和使用情況:
sdb> universe
Heap Parameters:
Gen 0: eden [0x00000007ff600000,0x00000007ff6c4de8,0x00000007ff8b0000) space capacity = 2818048, 28.61470067223837 used
from [0x00000007ff8b0000,0x00000007ff8b0000,0x00000007ff900000) space capacity = 327680, 0.0 used
to [0x00000007ff900000,0x00000007ff900000,0x00000007ff950000) space capacity = 327680, 0.0 usedInvocations: 0
Gen 1: old [0x00000007ff950000,0x00000007ff950000,0x0000000800000000) space capacity = 7012352, 0.0 usedInvocations: 0
在我們的Java代碼里玄渗,執(zhí)行到Test.fn()末尾為止應該創(chuàng)建了3個Test2的實例座菠。它們必然在GC堆里,但都在哪里呢藤树?用scanoops命令來看:
hsdb> scanoops 0x00000007ff600000 0x00000007ff6c4de8 jvm.hsdb.Test2
0x00000007ff6b42d0 jvm/hsdb/Test2
0x00000007ff6b42f0 jvm/hsdb/Test2
0x00000007ff6b4300 jvm/hsdb/Test2
hsdb>
scanoops接受兩個必選參數和一個可選參數:必選參數是要掃描的地址范圍浴滴,一個是起始地址一個是結束地址;可選參數用于指定要掃描什么類型的對象實例岁钓。實際掃描的時候會掃出指定的類型及其派生類的實例升略。
這里可以看到確實掃出了3個Test2的實例。內容有兩列:左邊是對象的起始地址屡限,右邊是對象的實際類型品嚣。 從它們所在的地址,對照前面universe命令看到的GC堆的地址范圍钧大,可以知道它們都在eden里翰撑。
還可以用inspect命令來查看對象的內容:
hsdb> inspect 0x00000007ff6b42d0
instance of Oop for jvm/hsdb/Test2 @ 0x00000007ff6b42d0 @ 0x00000007ff6b42d0 (size = 16)
_mark: 5
_metadata._compressed_klass: InstanceKlass for jvm/hsdb/Test2
hsdb>
可見一個Test2的實例要16字節(jié)。因為Test2類沒有任何Java層的實例字段啊央,這里就沒有任何Java實例字段可顯示眶诈。
還想看到更裸的數據的同學可以在MemoryViewer來看實際內存里的數據長啥樣:
上面的數字都是啥來的呢?
0x00000007ff6b42d0: _mark: 0x0000000000000001
0x00000007ff6b42d8: _metadata._compressed_klass: 0x0000000000060460
一個Test2的實例包含2個給VM用的隱含字段作為對象頭瓜饥,和0個Java字段册养。
對象頭的第一個字段是mark word,記錄該對象的GC狀態(tài)压固、同步狀態(tài)球拦、identity hash code之類的多種信息。
對象頭的第二個字段是個類型信息指針,klass pointer坎炼。
順帶發(fā)張Inspector的截圖來展示HotSpot VM里描述Test2類的VM對象長啥樣吧愧膀。
在菜單里選Tools -> Inspector,在地址里輸入前面看到的klass地址:
InstanceKlass存著Java類型的名字谣光、繼承關系檩淋、實現接口關系,字段信息萄金,方法信息蟀悦,運行時常量池的指針,還有內嵌的虛方法表(vtable)氧敢、接口方法表(itable)和記錄對象里什么位置上有GC會關心的指針(oop map)等等日戈。
留意到這個InstanceKlass是給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
前面對HSDB的操作和HotSpot VM里的一些內部數據結構有了一定的了解蔬顾,現在讓我們回到主題:找指針酌住!
于是我們要找t1、t2阎抒、t3這三個變量酪我,等同于找出存有指向上述3個Test2實例的地址的存儲位置。
不嫌麻煩的話手工掃描內存去找也能找到且叁,不過幸好HSDB內建了revptrs命令都哭,可以找出“反向指針”——如果a變量引用著b對象,那么從b對象出發(fā)去找a變量就是找一個“反向指針”逞带。
先拿第一個Test2的實例試試看:
hsdb> revptrs 0x00000007ff6b42d0
Computing reverse pointers...
Done.
null
Oop for java/lang/Class @ 0x00000007ff6b3440
還真的找到了一個包含指向Test2實例的指針欺矫,在一個java.lang.Class的實例里。
可以看到這個Class對象也在eden里展氓,具體來說在main線程的TLAB里穆趴。
這個Class對象是如何引用到Test2的實例的呢?再用inspect命令:
inspect 0x00000007ff6b3440
instance of Oop for java/lang/Class @ 0x00000007ff6b3440 @ 0x00000007ff6b3440 (size = 120)
<<Reverse pointers>>:
t1: Oop for jvm/hsdb/Test2 @ 0x00000007ff6b42d0 Oop for jvm/hsdb/Test2 @ 0x00000007ff6b42d0
可以看到遇汞,這個Class對象里存著Test類的靜態(tài)變量t1未妹,指向著第一個Test2實例簿废。
成功找到t1了!這個有點特別络它,本來JVM規(guī)范里也沒明確規(guī)定靜態(tài)變量要存在哪里族檬,通常認為它應該在概念中的“方法區(qū)”里;但現在在JDK11的HotSpot VM里它實質上也被放在Java heap里了化戳〉チ希可以把這種特例看作是HotSpot VM把方法區(qū)的一部分數據也放在Java heap里了。
再接再厲点楼,用revptrs看看第二個Test2實例有誰引用:
revptrs 0x00000007ff6b42f0
Oop for jvm/hsdb/Test @ 0x00000007ff6b42e0
hsdb> inspect 0x00000007ff6b42e0
instance of Oop for jvm/hsdb/Test @ 0x00000007ff6b42e0 @ 0x00000007ff6b42e0 (size = 16)
<<Reverse pointers>>:
_mark: 5
_metadata._compressed_klass: InstanceKlass for jvm/hsdb/Test
t2: Oop for jvm/hsdb/Test2 @ 0x00000007ff6b42f0 Oop for jvm/hsdb/Test2 @ 0x00000007ff6b42f0
hsdb>
可以看到這個Test實例里有個成員字段t2扫尖,指向了第二個Test2實例。
于是t2也找到了掠廓!在Java堆里换怖,作為Test的實例的成員字段存在。
那么趕緊試試用revptrs命令看第三個Test2實例:
revptrs 0x00000007ff6b4300
null
啥却盘?沒找到狰域?媳拴!SA這也太弱小了吧黄橘。明明就在那里…
0x00000007ff6b4300 回到前面打開的Stack Memory窗口看,仔細看會發(fā)現那個窗口里正好就有0x00000007ff6b4300這數字屈溉,
如果圖里看得不清楚的話塞关,我再用文字重新寫一遍(兩道橫線之間的是Test.fn()的棧幀內容,前后的則是別的東西):
-------------------------------------------------------------------------------------------------------------
Stack frame for Test.fn() @bci=8, line=6, pc=0x0000000002893ca5, methodOop=0x00000000fb077f78 (Interpreted frame)
0x000000000287f808: 0x000000000287f808 expression stack bottom <- rsp
0x000000000287f810: 0x00000000fb077f58 bytecode pointer = 0x00000000fb077f50 (base) + 8 (bytecode index) in PermGen
0x000000000287f818: 0x000000000287f860 pointer to locals
0x000000000287f820: 0x00000000fb078360 constant pool cache = ConstantPoolCache for Test in PermGen
0x000000000287f828: 0x0000000000000000 method data oop = null
0x000000000287f830: 0x00000000fb077f78 method oop = Method for Test.fn()V in PermGen
0x000000000287f838: 0x0000000000000000 last Java stack pointer (not set)
0x000000000287f840: 0x000000000287f860 old stack pointer (saved rsp)
0x000000000287f848: 0x000000000287f8a8 old frame pointer (saved rbp) <- rbp
0x000000000287f850: 0x0000000002886298 return address = in interpreter codelet "return entry points" [0x00000000028858b8, 0x00000000028876c0) 7688 bytes
0x000000000287f858: 0x00000000fa49a740 local[1] "t3" = Oop for Test2 in NewGen
0x000000000287f860: 0x00000000fa49a720 local[0] "this" = Oop for Test in NewGen
-------------------------------------------------------------------------------------------------------------
0x000000000287f868: 0x000000000287f868
0x000000000287f870: 0x00000000fb077039
0x000000000287f878: 0x000000000287f8c0
0x000000000287f880: 0x00000000fb077350
0x000000000287f888: 0x0000000000000000
0x000000000287f890: 0x00000000fb077060
0x000000000287f898: 0x000000000287f860
0x000000000287f8a0: 0x000000000287f8c0
0x000000000287f8a8: 0x000000000287f9a0
0x000000000287f8b0: 0x000000000288062a
0x000000000287f8b8: 0x00000000fa49a720
0x000000000287f8c0: 0x00000000fa498ea8
0x000000000287f8c8: 0x0000000000000000
0x000000000287f8d0: 0x0000000000000000
0x000000000287f8d8: 0x0000000000000000
回顧JVM規(guī)范里所描述的Java棧幀結構子巾,包括:
[ 操作數棧 (operand stack) ]
[ 棧幀信息 (dynamic linking) ]
[ 局部變量區(qū) (local variables) ]
再跟HotSpot VM的解釋器所使用的棧幀布局對比看看帆赢,是不是正好能對應上?局部變量區(qū)(locals)有了线梗,VM所需的棧幀信息也有了椰于;執(zhí)行到這個位置operand stack正好是空的所以看不到它。
(HotSpot VM里把operand stack叫做expression stack仪搔。這是因為operand stack通常只在表達式求值過程中才有內容)
從Test.fn()的棧幀中我們可以看到t3變量就在locals[1]的位置上瘾婿。t3變量也找到了!大功告成烤咧!