解讀HSDB

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界面
可視化線程棧

image.png

對象直方圖
Tools -> Object Histogram,我們可以通過對象直方圖快速定位某個類型的對象的地址以供我們進一步分析

image.png
image.png

OOP信息
我們可以根據對象地址在 Tools -> Inspector 獲取對象的在 JVM 層的實例 instanceOopDesc 對象,它包括對象頭 _mark 和 _metadata 以及實例信息

image.png

元數據區(qū)
HotSpot VM 里有一套對象專門用來存放元數據稽犁,它們包括:

  • Klass 系對象,用于描述類型的總體信息【通過 OOP 信息(inspect)可以看到 instanceKlass 對象】
  • ConstantPool/ConstantPoolCache 對象:每個 InstanceKlass 關聯著一個 ConstantPool骚亿,作為該類型的運行時常量池已亥。這個常量池的結構跟 Class 文件里的常量池基本上是對應的
image.png
  • Method 對象,用來描述 Java 方法的總體信息来屠,如方法入口地址虑椎、調用/循環(huán)計數器等等
    • ConstMethod 對象,記錄著 Java 方法的不變的描述信息俱笛,包括方法名捆姜、方法的訪問修飾符、字節(jié)碼迎膜、行號表泥技、局部變量表等等。注意磕仅,字節(jié)碼指令被分配在 constMethodOop 對象的內存區(qū)域的末尾
      • MethodData 對象珊豹,記錄著 Java 方法執(zhí)行時的 profile 信息,例如某方法里的某個字節(jié)碼之類是否從來沒遇到過 null榕订,某個條件跳轉是否總是走同一個分支店茶,等等。這些信息在解釋器(多層編譯模式下也在低層的編譯生成的代碼里)收集劫恒,然后供給 HotSpot Server Compiler 用于做激進優(yōu)化贩幻。
image.png
image.png
  • 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:


image.png

這會兒就連接到目標進程了:


image.png

剛開始打開的窗口是Java Threads熟吏,里面有個線程列表距糖。雙擊代表線程的行會打開一個Oop Inspector窗口顯示HotSpot VM里記錄線程的一些基本信息的C++對象的內容玄窝。
不過這里我們更可能會關心的是線程棧的內存數據。先選擇main線程悍引,然后點擊Java Threads窗口里的工具欄按鈕從左數第2個可以打開Stack Memory窗口來顯示main線程的棧:


image.png

Stack Memory窗口的內容有三欄:
左起第1欄是內存地址恩脂,請讓我提醒一下本文里提到“內存地址”的地方都是指虛擬內存意義上的地址,不是“物理內存地址”趣斤,請不要弄混了這倆概念俩块;
第2欄是該地址上存的數據,以字寬為單位
第3欄是對數據的注釋唬渗,豎線表示范圍,橫線或斜線連接范圍與注釋文字奋渔。

現在讓我們打開HSDB里的控制臺镊逝,以便用命令來了解更多信息。
在菜單里選擇Windows -> Console:


image.png

然后會得到一個空白的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來看實際內存里的數據長啥樣:


image.png

上面的數字都是啥來的呢?

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地址:


image.png

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這數字屈溉,


image.png

如果圖里看得不清楚的話塞关,我再用文字重新寫一遍(兩道橫線之間的是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) ]


image.png

再跟HotSpot VM的解釋器所使用的棧幀布局對比看看帆赢,是不是正好能對應上?局部變量區(qū)(locals)有了线梗,VM所需的棧幀信息也有了椰于;執(zhí)行到這個位置operand stack正好是空的所以看不到它。
(HotSpot VM里把operand stack叫做expression stack仪搔。這是因為operand stack通常只在表達式求值過程中才有內容)
從Test.fn()的棧幀中我們可以看到t3變量就在locals[1]的位置上瘾婿。t3變量也找到了!大功告成烤咧!

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末偏陪,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子煮嫌,更是在濱河造成了極大的恐慌笛谦,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件昌阿,死亡現場離奇詭異饥脑,居然都是意外死亡恳邀,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進店門好啰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來轩娶,“玉大人,你說我怎么就攤上這事框往■悖” “怎么了?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵椰弊,是天一觀的道長许溅。 經常有香客問我,道長秉版,這世上最難降的妖魔是什么贤重? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮清焕,結果婚禮上并蝗,老公的妹妹穿的比我還像新娘。我一直安慰自己秸妥,他們只是感情好滚停,可當我...
    茶點故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著粥惧,像睡著了一般键畴。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上突雪,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天起惕,我揣著相機與錄音,去河邊找鬼咏删。 笑死惹想,一個胖子當著我的面吹牛,可吹牛的內容都是我干的督函。 我是一名探鬼主播嘀粱,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼侨核!你這毒婦竟也來了草穆?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤搓译,失蹤者是張志新(化名)和其女友劉穎悲柱,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體些己,經...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡豌鸡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年嘿般,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片涯冠。...
    茶點故事閱讀 40,115評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡炉奴,死狀恐怖,靈堂內的尸體忽然破棺而出蛇更,到底是詐尸還是另有隱情瞻赶,我是刑警寧澤,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布派任,位于F島的核電站砸逊,受9級特大地震影響,放射性物質發(fā)生泄漏掌逛。R本人自食惡果不足惜师逸,卻給世界環(huán)境...
    茶點故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望豆混。 院中可真熱鬧篓像,春花似錦、人聲如沸皿伺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽心傀。三九已至屈暗,卻和暖如春拆讯,著一層夾襖步出監(jiān)牢的瞬間脂男,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工种呐, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留宰翅,地道東北人。 一個月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓爽室,卻偏偏與公主長得像汁讼,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子阔墩,可洞房花燭夜當晚...
    茶點故事閱讀 45,055評論 2 355

推薦閱讀更多精彩內容