(未經(jīng)許可請勿轉(zhuǎn)載。希望轉(zhuǎn)載請與我聯(lián)系。)
(如果打開此頁面時瀏覽器有點(diǎn)卡住的話請耐心等待片刻。大概是ItEye的代碼高亮太耗時了…)
幾天前在
HLLVM群組 有人
問了個小問題 ,說
public class Test { static Test2 t1 = new Test2(); Test2 t2 = new Test2(); public void fn() { Test2 t3 = new Test2(); } } class Test2 { }
這個程序的t1、t2、t3三個變量本身(而不是這三個變量所指向的對象)到底在哪里。
TL;DR版回答是:
- t1在存Java靜態(tài)變量的地方,概念上在JVM的方法區(qū)(method area)里
- t2在Java堆里,作為Test的一個實(shí)例的字段存在
- t3在Java線程的調(diào)用棧里,作為Test.fn()的一個局部變量存在
不過就這么簡單的回答大家都會,滿足不了對JVM的實(shí)現(xiàn)感興趣的同學(xué)們的好奇心。說到底,這“方法區(qū)”到底是啥?Java堆在哪里?Java線程的調(diào)用棧又是啥樣的?
那就讓我們跑點(diǎn)例子,借助調(diào)試器來看看在一個實(shí)際運(yùn)行中的JVM里是啥狀況。
(下文中代碼也傳了一份到
https://gist.github.com/rednaxelafx/5392451 )
=============================================================================
寫個啟動類來跑上面問題中的代碼:
public class Main { public static void main(String[] args) { Test test = new Test(); test.fn(); } }
(編譯這個Main.java和上面的Test.java時最好加上-g參數(shù)生成LocalVariableTable等調(diào)試信息,以便后面某些情況下可以用到)
接下來如無特別說明本文將使用Windows 7 64-bit, Oracle JDK 1.7.0_09 Server VM, Serial GC的環(huán)境中運(yùn)行所有例子。
之前在GreenTeaJUG在杭州的活動
演示Serviceability Agent 的時候也講到過這是個非常便于探索HotSpot VM內(nèi)部實(shí)現(xiàn)的API,而HSDB則是在SA基礎(chǔ)上包裝起來的一個調(diào)試器。這次我們就用HSDB來做實(shí)驗。
SA的一個限制是它只實(shí)現(xiàn)了調(diào)試snapshot的功能:要么要讓被調(diào)試的目標(biāo)進(jìn)程完全暫停,要么就調(diào)試core dump。所以我們在用HSDB做實(shí)驗前,得先讓我們的Java程序運(yùn)行到我們關(guān)注的點(diǎn)上才行。
理想情況下我們會希望讓這Java程序停在Test.java的第6行,也就是Test.fn()中t3局部變量已經(jīng)進(jìn)入作用域,而該方法又尚未返回的地方。怎樣才能停在這里呢?
其實(shí)用個Java層的調(diào)試器即可。大家平時可能習(xí)慣了在Eclipse、IntelliJ IDEA、NetBeans等Java IDE里使用Java層調(diào)試器,但為了減少對外部工具的依賴,本文將使用Oracle JDK自帶的jdb工具來完成此任務(wù)。
jdb 跟上面列舉的IDE里包含的調(diào)試器底下依賴著同一套調(diào)試API,也就是
Java Platform Debugger Architecture (JPDA) 。功能也類似,只是界面是命令行的,表明上看起來不太一樣而已。
為了方便后續(xù)步驟,啟動jdb的時候可以設(shè)定讓目標(biāo)Java程序使用serial GC和10MB的Java heap。
啟動jdb之后可以用stop in命令在指定的Java方法入口處設(shè)置斷點(diǎn),
然后用run命令指定主類名稱來啟動Java程序,
等跑到斷點(diǎn)看看位置是否已經(jīng)到滿足需求,還沒到的話可以用step、next之類的命令來向前進(jìn)。
對jdb命令不熟悉的同學(xué)可以在啟動jdb之后使用help命令來查看命令列表和說明。
具體步驟如下:
D:\test>jdb -XX:+UseSerialGC -Xmx10m Initializing jdb ... > stop in Test.fn Deferring breakpoint Test.fn. It will be set after the class is loaded. > run Main run Main Set uncaught java.lang.Throwable Set deferred uncaught java.lang.Throwable > VM Started: Set deferred breakpoint Test.fn Breakpoint hit: "thread=main", Test.fn(), line=5 bci=0 5 Test2 t3 = new Test2(); main[1] next Step completed: > "thread=main", Test.fn(), line=6 bci=8 6 } main[1]
按照上述步驟執(zhí)行完最后一個next命令之后,我們就來到了最初想要的Test.java的第6行,也就是Test.fn()返回前的位置。
接下來把這個jdb窗口放一邊,另開一個命令行窗口用
jps 命令看看我們要調(diào)試的Java進(jìn)程的pid是多少:
D:\test>jps 4328 Main 9064 Jps 7716 TTY
可以看到是4328。把這個pid記下來待會兒用。
然后啟動HSDB:
D:\test>java -cp .;%JAVA_HOME%/lib/sa-jdi.jar sun.jvm.hotspot.HSDB
(要留意Linux和Solaris在Oracle/Sun JDK6就可以使用HSDB了,但Windows上要到Oracle JDK7才可以用HSDB)
啟動HSDB之后,把它連接到目標(biāo)進(jìn)程上。從菜單里選擇File -> Attach to HotSpot process:

在彈出的對話框里輸入剛才記下的pid然后按OK:

這會兒就連接到目標(biāo)進(jìn)程了:

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

Stack Memory窗口的內(nèi)容有三欄:
左起第1欄是內(nèi)存地址,請讓我提醒一下本文里提到“內(nèi)存地址”的地方都是指虛擬內(nèi)存意義上的地址,
不是 “物理內(nèi)存地址” ,請不要弄混了這倆概念;
第2欄是該地址上存的數(shù)據(jù),以字寬為單位,本文例子中我是在Windows 7 64-bit上跑64位的JDK7的HotSpot VM,字寬是64位(8字節(jié));
第3欄是對數(shù)據(jù)的注釋,豎線表示范圍,橫線或斜線連接范圍與注釋文字。
現(xiàn)在看不懂這個窗口里的數(shù)據(jù)沒關(guān)系,先放一邊,后面再回過頭來看。
現(xiàn)在讓我們打開HSDB里的控制臺,以便用命令來了解更多信息。
在菜單里選擇Windows -> Console:
然后會得到一個空白的Command Line窗口。在里面敲一下回車就會出現(xiàn)hsdb>提示符。
(用過CLHSDB的同學(xué)可能會發(fā)現(xiàn)這就是把CLHSDB嵌入在了HSDB的圖形界面里)
不知道有什么命令可用的同學(xué)可以先用help命令看看命令列表。
可以用universe命令來查看GC堆的地址范圍和使用情況:
hsdb> universe Heap Parameters: Gen 0: eden [0x00000000fa400000,0x00000000fa4aad68,0x00000000fa6b0000) space capacity = 2818048, 24.831088753633722 used from [0x00000000fa6b0000,0x00000000fa6b0000,0x00000000fa700000) space capacity = 327680, 0.0 used to [0x00000000fa700000,0x00000000fa700000,0x00000000fa750000) space capacity = 327680, 0.0 usedInvocations: 0 Gen 1: old [0x00000000fa750000,0x00000000fa750000,0x00000000fae00000) space capacity = 7012352, 0.0 usedInvocations: 0 perm [0x00000000fae00000,0x00000000fb078898,0x00000000fc2c0000) space capacity = 21757952, 11.90770160721009 usedInvocations: 0
這里用的是HotSpot VM的serial GC。GC堆由young gen = DefNewGeneration(包括eden和兩個survivor space)、old gen = TenuredGeneration和perm gen = PermGen構(gòu)成。
其中young gen和old gen構(gòu)成了這種配置下HotSpot VM里的Java堆(Java heap),而perm gen不屬于Java heap的一部分,它存儲的主要是元數(shù)據(jù)或者叫反射信息,主要用于實(shí)現(xiàn)JVM規(guī)范里的“方法區(qū)”概念。
在我們的Java代碼里,執(zhí)行到Test.fn()末尾為止應(yīng)該創(chuàng)建了3個Test2的實(shí)例。它們必然在GC堆里,但都在哪里呢?用scanoops命令來看:
hsdb> scanoops 0x00000000fa400000 0x00000000fc2c0000 Test2 0x00000000fa49a710 Test2 0x00000000fa49a730 Test2 0x00000000fa49a740 Test2
scanoops接受兩個必選參數(shù)和一個可選參數(shù):必選參數(shù)是要掃描的地址范圍,一個是起始地址一個是結(jié)束地址;可選參數(shù)用于指定要掃描什么類型的對象實(shí)例。實(shí)際掃描的時候會掃出指定的類型及其派生類的實(shí)例。
這里可以看到確實(shí)掃出了3個Test2的實(shí)例。內(nèi)容有兩列:左邊是對象的起始地址,右邊是對象的實(shí)際類型。
從它們所在的地址,對照前面universe命令看到的GC堆的地址范圍,可以知道它們都在eden里。
通過whatis命令可以進(jìn)一步知道它們都在eden之中分配給main線程的thread-local allocation buffer (TLAB)中:
hsdb> whatis 0x00000000fa49a710 Address 0x00000000fa49a710: In thread-local allocation buffer for thread "main" (1) [0x00000000fa48f490,0x00000000fa49a750,0x00000000fa49d118) hsdb> whatis 0x00000000fa49a730 Address 0x00000000fa49a730: In thread-local allocation buffer for thread "main" (1) [0x00000000fa48f490,0x00000000fa49a750,0x00000000fa49d118) hsdb> whatis 0x00000000fa49a740 Address 0x00000000fa49a740: In thread-local allocation buffer for thread "main" (1) [0x00000000fa48f490,0x00000000fa49a750,0x00000000fa49d118) hsdb>
還可以用inspect命令來查看對象的內(nèi)容:
hsdb> inspect 0x00000000fa49a710 instance of Oop for Test2 @ 0x00000000fa49a710 @ 0x00000000fa49a710 (size = 16) _mark: 1
可見一個Test2的實(shí)例要16字節(jié)。因為Test2類沒有任何Java層的實(shí)例字段,這里就沒有任何Java實(shí)例字段可顯示。不過本來這里還應(yīng)該顯示一行:
_metadata._compressed_klass: InstanceKlass for Test2 @ 0x00000000fb078608
不幸因為這個版本的HotSpot VM里帶的SA有bug所以沒顯示出來。此bug在新版里已修。
還想看到更裸的數(shù)據(jù)的同學(xué)可以用mem命令來看實(shí)際內(nèi)存里的數(shù)據(jù)長啥樣:
hsdb> mem 0x00000000fa49a710 2 0x00000000fa49a710: 0x0000000000000001 0x00000000fa49a718: 0x00000000fb078608
mem命令接受的兩個參數(shù)都必選,一個是起始地址,另一個是以字寬為單位的“長度”。我們知道一個Test2實(shí)例有16字節(jié),所以給定長度為2來看。
上面的數(shù)字都是啥來的呢?
0x00000000fa49a710: _mark: 0x0000000000000001 0x00000000fa49a718: _metadata._compressed_klass: 0xfb078608 0x00000000fa49a71c: (padding): 0x00000000
一個Test2的實(shí)例包含2個給VM用的隱含字段作為對象頭,和0個Java字段。
對象頭的第一個字段是mark word,記錄該對象的GC狀態(tài)、同步狀態(tài)、identity hash code之類的多種信息。
對象頭的第二個字段是個類型信息指針,klass pointer。這里因為默認(rèn)開啟了壓縮指針,所以本來應(yīng)該是64位的指針存在了32位字段里。
最后還有4個字節(jié)是為了滿足對齊需求而做的填充(padding)。
以前在另一帖里也介紹過這部分內(nèi)容,可以參考:
借助HotSpot SA來一窺PermGen上的對象 順帶發(fā)張Inspector的截圖來展示HotSpot VM里描述Test2類的VM對象長啥樣吧。
在菜單里選Tools -> Inspector,在地址里輸入前面看到的klass地址:

InstanceKlass存著Java類型的名字、繼承關(guān)系、實(shí)現(xiàn)接口關(guān)系,字段信息,方法信息,運(yùn)行時常量池的指針,還有內(nèi)嵌的虛方法表(vtable)、接口方法表(itable)和記錄對象里什么位置上有GC會關(guān)心的指針(oop map)等等。
留意到這個InstanceKlass是給VM內(nèi)部用的,并不直接暴露給Java層;InstanceKlass不是java.lang.Class的實(shí)例。
在HotSpot VM里,java.lang.Class的實(shí)例被稱為“Java mirror”,意思是它是VM內(nèi)部用的klass對象的“鏡像”,把klass對象包裝了一層來暴露給Java層使用。
在InstanceKlass里有個_java_mirror字段引用著它對應(yīng)的Java mirror,而mirror里也有個隱藏字段指向其對應(yīng)的InstanceKlass。
所以當(dāng)我們寫obj.getClass(),在HotSpot VM里實(shí)際上經(jīng)過了兩層間接引用才能找到最終的Class對象:
obj->_klass->_java_mirror
在Oracle JDK7之前,Oracle/Sun JDK的HotSpot VM把Java類的靜態(tài)變量存在InstanceKlass結(jié)構(gòu)的末尾;從Oracle JDK7開始,為了配合PermGen移除的工作,Java類的靜態(tài)變量被挪到Java mirror(Class對象)的末尾了。
還有就是,在JDK7之前Java mirror存放在PermGen里,而從JDK7開始Java mirror默認(rèn)也跟普通Java對象一樣先從eden開始分配而不放在PermGen里。到JDK8則進(jìn)一步徹底移除了PermGen,把諸如klass之類的元數(shù)據(jù)都挪到GC堆之外管理,而Java mirror的處理則跟JDK7一樣。
=============================================================================
前面對HSDB的操作和HotSpot VM里的一些內(nèi)部數(shù)據(jù)結(jié)構(gòu)有了一定的了解,現(xiàn)在讓我們回到主題:找指針!
HotSpot VM內(nèi)部使用直接指針來實(shí)現(xiàn)Java引用。在64位環(huán)境中有可能啟用“壓縮指針”的功能把64位指針壓縮到只用32位來存。壓縮指針與非壓縮指針直接有非常簡單的1對1對應(yīng)關(guān)系,前者可以看作后者的特例。
于是我們要找t1、t2、t3這三個變量,等同于找出存有指向上述3個Test2實(shí)例的地址的存儲位置。
不嫌麻煩的話手工掃描內(nèi)存去找也能找到,不過幸好HSDB內(nèi)建了revptrs命令,可以找出“反向指針”——如果a變量引用著b對象,那么對b對象來說a就是一個“反向指針”。
先拿第一個Test2的實(shí)例試試看:
hsdb> revptrs 0x00000000fa49a710 Computing reverse pointers... Done. null Oop for java/lang/Class @ 0x00000000fa499b00
還真的找到了一個包含指向Test2實(shí)例的指針,在一個java.lang.Class的實(shí)例里。
用whatis命令來看看這個Class對象在哪里:
hsdb> whatis 0x00000000fa499b00 Address 0x00000000fa499b00: In thread-local allocation buffer for thread "main" (1) [0x00000000fa48f490,0x00000000fa49a750,0x00000000fa49d118)
可以看到這個Class對象也在eden里,具體來說在main線程的TLAB里。
這個Class對象是如何引用到Test2的實(shí)例的呢?再用inspect命令:
hsdb> inspect 0x00000000fa499b00 instance of Oop for java/lang/Class @ 0x00000000fa499b00 @ 0x00000000fa499b00 (size = 120) <<Reverse pointers>>: t1: Oop for Test2 @ 0x00000000fa49a710 Oop for Test2 @ 0x00000000fa49a710
可以看到,這個Class對象里存著Test類的靜態(tài)變量t1,指向著第一個Test2實(shí)例。
成功找到t1了!這個有點(diǎn)特別,本來JVM規(guī)范里也沒明確規(guī)定靜態(tài)變量要存在哪里,通常認(rèn)為它應(yīng)該在概念中的“方法區(qū)”里;但現(xiàn)在在JDK7的HotSpot VM里它實(shí)質(zhì)上也被放在Java heap里了??梢园堰@種特例看作是HotSpot VM把方法區(qū)的一部分?jǐn)?shù)據(jù)也放在Java heap里了。
前面也已經(jīng)提過,在JDK7之前的Oracle/Sun JDK里的HotSpot VM把靜態(tài)變量存在InstanceKlass末尾,存在PermGen里。那個時候的PermGen更接近于完整的方法區(qū)一些。 關(guān)于PermGen移除計劃的一些零星筆記可以參考
我以前一老帖 。
再接再厲,用revptrs看看第二個Test2實(shí)例有誰引用:
hsdb> revptrs 0x00000000fa49a730 Oop for Test @ 0x00000000fa49a720
找到了一個Test實(shí)例。同樣用whatis來看看它在哪兒:
hsdb> whatis 0x00000000fa49a720 Address 0x00000000fa49a720: In thread-local allocation buffer for thread "main" (1) [0x00000000fa48f490,0x00000000fa49a750,0x00000000fa49d118)
果然也在main線程的TLAB里。
然后看這個Test實(shí)例的內(nèi)容:
hsdb> inspect 0x00000000fa49a720 instance of Oop for Test @ 0x00000000fa49a720 @ 0x00000000fa49a720 (size = 16) <<Reverse pointers>>: _mark: 1 t2: Oop for Test2 @ 0x00000000fa49a730 Oop for Test2 @ 0x00000000fa49a730
可以看到這個Test實(shí)例里有個成員字段t2,指向了第二個Test2實(shí)例。
于是t2也找到了!在Java堆里,作為Test的實(shí)例的成員字段存在。 那么趕緊試試用revptrs命令看第三個Test2實(shí)例:
hsdb> revptrs 0x00000000fa49a740 null
啥?沒找到?!SA這也太弱小了吧。明明就在那里…
回頭我會做個補(bǔ)丁讓新版HotSpot VM的SA能處理這種情況。
這個時候的HSDB界面全貌:

0x00000000fa49a740看起來有沒有點(diǎn)眼熟?
回到前面打開的Stack Memory窗口看,仔細(xì)看會發(fā)現(xiàn)那個窗口里正好就有0x00000000fa49a740這數(shù)字,位于0x000000000287f858地址上。
實(shí)際情況是,下面這張圖里紅色框住的部分就是main線程上Test.fn()的調(diào)用對應(yīng)的棧幀:

如果圖里看得不清楚的話,我再用文字重新寫一遍(兩道橫線之間的是Test.fn()的棧幀內(nèi)容,前后的則是別的東西):
0x000000000287f7f0: 0x0000000002886298 0x000000000287f7f8: 0x0000000002893ca5 0x000000000287f800: 0x0000000002893ca5 ------------------------------------------------------------------------------------------------------------- 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棧幀結(jié)構(gòu) ,包括:
[ 操作數(shù)棧 (operand stack) ] [ 棧幀信息 (dynamic linking) ] [ 局部變量區(qū) (local variables) ]
上張
我以前做的投影稿 里的圖:

再跟HotSpot VM的解釋器所使用的棧幀布局對比看看,是不是正好能對應(yīng)上?局部變量區(qū)(locals)有了,VM所需的棧幀信息也有了;執(zhí)行到這個位置operand stack正好是空的所以看不到它。
(HotSpot VM里把operand stack叫做expression stack。這是因為operand stack通常只在表達(dá)式求值過程中才有內(nèi)容)
從Test.fn()的棧幀中我們可以看到t3變量就在locals[1]的位置上。t3變量也找到了!大功告成! 棧幀信息里具體都是些啥,以后有機(jī)會再展開講吧。
都看到這里了,干脆把main方法的棧幀也如法炮制分析一下。先上圖:

然后再用文字寫一次:
0x000000000287f7f0: 0x0000000002886298 0x000000000287f7f8: 0x0000000002893ca5 0x000000000287f800: 0x0000000002893ca5 0x000000000287f808: 0x000000000287f808 0x000000000287f810: 0x00000000fb077f58 0x000000000287f818: 0x000000000287f860 0x000000000287f820: 0x00000000fb078360 0x000000000287f828: 0x0000000000000000 0x000000000287f830: 0x00000000fb077f78 0x000000000287f838: 0x0000000000000000 0x000000000287f840: 0x000000000287f860 0x000000000287f848: 0x000000000287f8a8 0x000000000287f850: 0x0000000002886298 0x000000000287f858: 0x00000000fa49a740 ------------------------------------------------------------------------------------------------------------- Stack frame for Main.main(java.lang.String[]) @bci=9, line=4, pc=0x0000000002886298, methodOop=0x00000000fb077060 (Interpreted frame) 0x000000000287f860: 0x00000000fa49a720 expression stack[0] = Oop for Test in NewGen 0x000000000287f868: 0x000000000287f868 expression stack bottom 0x000000000287f870: 0x00000000fb077039 bytecode pointer = 0x00000000fb077030 (base) + 9 (bytecode index) in PermGen 0x000000000287f878: 0x000000000287f8c0 pointer to locals 0x000000000287f880: 0x00000000fb077350 constant pool cache = ConstantPoolCache for Main in PermGen 0x000000000287f888: 0x0000000000000000 method data oop = null 0x000000000287f890: 0x00000000fb077060 method oop = Method for Main.main([Ljava/lang/String;)V in PermGen 0x000000000287f898: 0x000000000287f860 last Java stack pointer 0x000000000287f8a0: 0x000000000287f8c0 old stack pointer 0x000000000287f8a8: 0x000000000287f9a0 old frame pointer 0x000000000287f8b0: 0x000000000288062a return address = in StubRoutines 0x000000000287f8b8: 0x00000000fa49a720 local[1] "test" = Oop for Test in NewGen 0x000000000287f8c0: 0x00000000fa498ea8 local[0] "args" = Oop for java.lang.String[] in NewGen ------------------------------------------------------------------------------------------------------------- 0x000000000287f8c8: 0x0000000000000000 0x000000000287f8d0: 0x0000000000000000 0x000000000287f8d8: 0x0000000000000000
main的棧幀的operand stack就不是空的了,有一個元素,用來傳遞參數(shù)給其調(diào)用的Test.fn()方法(作為“this”)。
仔細(xì)的同學(xué)可能發(fā)現(xiàn)了,0x000000000287f860這個地址前面不是說是調(diào)用Test.fn()產(chǎn)生的棧幀么?怎么這里又變成調(diào)用main()方法的棧幀的一部分了呢?
其實(shí)棧幀直接可以有重疊:(再上一張以前做的投影稿里的圖)

這樣可以減少傳遞參數(shù)所需的數(shù)據(jù)拷貝,也節(jié)省了空間。
回到HSDB,我們換個方式來把t3變量找出來。這里就需要編譯Test.java時給的-g參數(shù)所生成的LocalVariableTable的信息了:
hsdb> jseval "ts = jvm.threads" [Thread (address=0x00000000fa48fb38, name=Service Thread), Thread (address=0x00000000fa48fa18, name=C2 CompilerThread1), Thread (address=0x00000000fa48f8f8, name=C2 CompilerThread0), Thread (address=0x00000000fa49d178, name=JDWP Command Reader), Thread (address=0x00000000fa48f820, name=JDWP Event Helper Thread), Thread (address=0x00000000fa48f6d8, name=JDWP Transport Listener: dt_shmem), Thread (address=0x00000000fa48dc88, name=Attach Listener), Thread (address=0x00000000fa48db68, name=Signal Dispatcher), Thread (address=0x00000000fa405828, name=Finalizer), Thread (address=0x00000000fa4053a0, name=Reference Handler), Thread (address=0x00000000fa404860, name=main)] hsdb> jseval "t = ts[ts.length - 1]" Thread (address=0x00000000fa404860, name=main) hsdb> jseval "fs = t.frames" [Frame (method=Test.fn(), bci=8, line=6), Frame (method=Main.main(java.lang.String[]), bci=9, line=4)] hsdb> jseval "f0 = fs[0]" Frame (method=Test.fn(), bci=8, line=6) hsdb> jseval "f1 = fs[1]" Frame (method=Main.main(java.lang.String[]), bci=9, line=4) hsdb> jseval "f0.locals" {t3=Object 0x00000000fa49a740} hsdb>
=============================================================================
上面講棧幀布局的時候出現(xiàn)了“bytecode pointer”字眼。既然之前被不少好奇的同學(xué)問過“JVM里字節(jié)碼存在哪里”,這里就一并回答掉好了。
強(qiáng)調(diào)一點(diǎn):“字節(jié)碼”只是元數(shù)據(jù)的一部分。它只負(fù)責(zé)描述運(yùn)行邏輯,而其它信息像是類型名、成員的個數(shù)、類型、名字等等都
不是字節(jié)碼 。在Class文件里是如此,到運(yùn)行時在JVM里仍然是如此。
HotSpot VM里有一套對象專門用來存放元數(shù)據(jù),它們包括:
- Klass系對象。元數(shù)據(jù)的最主要入口。用于描述類型的總體信息
- ConstantPool/ConstantPoolCache對象。每個InstanceKlass關(guān)聯(lián)著一個ConstantPool,作為該類型的運(yùn)行時常量池。這個常量池的結(jié)構(gòu)跟Class文件里的常量池基本上是對應(yīng)的。可以參考 我以前的一個回帖 。ConstantPoolCache主要用于存儲某些字節(jié)碼指令所需的解析(resolve)好的常量項,例如給[get|put]static、[get|put]field、invoke[static|special|virtual|interface|dynamic]等指令對應(yīng)的常量池項用。
- Method對象,用來描述Java方法的總體信息,像是方法入口地址、調(diào)用/循環(huán)計數(shù)器等等
- ConstMethod對象,記錄著Java方法的不變的描述信息,包括方法名、方法的訪問修飾符、 字節(jié)碼 、行號表、局部變量表等等。注意了,字節(jié)碼就嵌在這ConstMethod對象里面。
- Symbol對象,對應(yīng)Class文件常量池里的JVM_CONSTANT_Utf8類型的常量。有一個VM全局的SymbolTable管理著所有Symbol。Symbol由所有Java類所共享。
- MethodData對象,記錄著Java方法執(zhí)行時的profile信息,例如某方法里的某個字節(jié)碼之類是否從來沒遇到過null,某個條件跳轉(zhuǎn)是否總是走同一個分支,等等。這些信息在解釋器(多層編譯模式下也在低層的編譯生成的代碼里)收集,然后供給HotSpot Server Compiler用于做激進(jìn)優(yōu)化。
在PermGen移除前,上述元數(shù)據(jù)對象都在PermGen里,直接被GC管理著。
JDK8徹底移除PermGen后,這些對象被挪到GC堆外的一塊叫做Metaspace的空間里做特殊管理,仍然間接的受GC管理。
介紹了背景,讓我們回到HSDB里。前面不是說“bytecode pointer (bcp)”嘛,從背景介紹可以知道字節(jié)碼存在ConstMethod對象里,那就讓我們用Test.fn()棧幀里存的bcp來驗證一下是否真的如此。
還是用whatis命令:
hsdb> whatis 0x00000000fb077f58 Address 0x00000000fb077f58: In perm generation perm [0x00000000fae00000,0x00000000fb078898,0x00000000fc2c0000) space capacity = 21757952, 11.90770160721009 used
這地址確實(shí)在PermGen里了。那么inspect一下看看?
hsdb> inspect 0x00000000fb077f58 Error: sun.jvm.hotspot.debugger.UnalignedAddressException: 100011
呃,這樣不行。inspect命令只能接受對象的起始地址,但字節(jié)碼是嵌在ConstMethod對象中間的。
那換條路子。棧幀里還有method oop,指向該棧幀對應(yīng)的Method對象。先從它入手:
hsdb> inspect 0x00000000fb077f78 instance of Method fn()V@0x00000000fb077f78 @ 0x00000000fb077f78 @ 0x00000000fb077f78 (size = 136) _mark: 1 _constMethod: ConstMethod fn()V@0x00000000fb077f08 @ 0x00000000fb077f08 Oop @ 0x00000000fb077f08 _constants: ConstantPool for Test @ 0x00000000fb077c68 Oop @ 0x00000000fb077c68 _method_size: 17 _max_stack: 2 _max_locals: 2 _size_of_parameters: 1 _access_flags: 1
這樣就找到了Test.fn()的Method對象,看到里面的_constMethod字段所指向的ConstMethod對象:
hsdb> inspect 0x00000000fb077f08 instance of ConstMethod fn()V@0x00000000fb077f08 @ 0x00000000fb077f08 @ 0x00000000fb077f08 (size = 112) _mark: 1 _method: Method fn()V@0x00000000fb077f78 @ 0x00000000fb077f78 Oop @ 0x00000000fb077f78 _exception_table: [I @ 0x00000000fae01d50 Oop for [I @ 0x00000000fae01d50 _constMethod_size: 14 _flags: 5 _code_size: 9 _name_index: 18 _signature_index: 12 _generic_signature_index: 0 _code_size: 9
這個ConstMethod對象從0x00000000fb077f08開始,長度112字節(jié),也就是這個對象的范圍是[0x00000000fb077f08, 0x00000000fb077f78)。bcp指向0x00000000fb077f58,確實(shí)在這個ConstMethod范圍內(nèi)。
通過經(jīng)驗可以知道實(shí)際上這里字節(jié)碼的起始地址是0x00000000fb077f50。通過ConstMethod的_code_size字段可以知道該方法的字節(jié)碼有9字節(jié)。找出來用mem命令看看內(nèi)存里的數(shù)據(jù):
hsdb> mem 0x00000000fb077f50 2 0x00000000fb077f50: 0x4c0001b7590200ca 0x00000000fb077f58: 0x00000000004105b1
這串?dāng)?shù)字是什么東西呢?展開來寫清楚一點(diǎn)就是:
0x00000000fb077f50: bb 00 02 new <cp index #2> [Class Test2] 0x00000000fb077f53: 59 dup 0x00000000fb077f54: b7 01 00 invokespecial <cp cache index #1> [Method Test2.<init>()V] 0x00000000fb077f57: 4c astore_1 0x00000000fb077f58: b1 return
眼尖的同學(xué)要吐槽了:在0x00000000fb077f50的字節(jié)不是0xca么,怎么變成0xbb了?
其實(shí)0xca是JVM規(guī)范里有描述的一個可選字節(jié)碼指令,
breakpoint0x00000000fb077f50: ca 00 02 breakpoint // 00 02 not used
還記得本文的實(shí)驗一開始用了jdb在Test.fn()的入口設(shè)置了斷點(diǎn)嗎?這就是結(jié)果——入口處的字節(jié)碼指令被改寫為breakpoint了。當(dāng)然,原本的字節(jié)碼指令也還在別的地方存著,等斷點(diǎn)解除之后這個位置就會被恢復(fù)成原本的0xbb指令。
把ConstMethod里存的字節(jié)碼跟Class文件里存的比較一下看看。用
javap 工具來看Class文件的內(nèi)容:
public void fn(); Code: stack=2, locals=2, args_size=1 0: bb 00 02 new #2 // class Test2 3: 59 dup 4: b7 00 03 invokespecial #3 // Method Test2."<init>":()V 7: 4c astore_1 8: b1 return
幾乎一模一樣。唯一的不同也是個有趣的小細(xì)節(jié):invokespecial的參數(shù)的常量池號碼不一樣了。HotSpot VM執(zhí)行new指令的時候用的還是Class文件里的常量池號和字節(jié)序。而在執(zhí)行invokespecial時,光是ConstantPool里的的常量項不夠地方放解析(resolve)出來的信息,所以把這些信息放在ConstantPoolCache里,然后也把invokespecial指令里的參數(shù)改寫過來,順帶變成了平臺相關(guān)的字節(jié)序。
同樣也看看Main.main()方法。內(nèi)存內(nèi)容:
hsdb> mem 0x00000000fb077030 2 0x00000000fb077030: 0x4c0001b7590200bb 0x00000000fb077038: 0x214103b10002b62b
展開來注解:
0x00000000fb077030: bb 00 02 new <cp index #2> [Class Test] 0x00000000fb077033: 59 dup 0x00000000fb077034: b7 01 00 invokespecial <cp cache index #1> [Method Test.<init>()V] 0x00000000fb077037: 4c astore_1 0x00000000fb077038: 2b aload_1 0x00000000fb077039: b6 02 00 invokevirtual <cp cache index #2> [Method Test.fn()V] 0x00000000fb07703c: b1 return
對應(yīng)的javap輸出:
public static void main(java.lang.String[]); Code: stack=2, locals=2, args_size=1 0: bb 00 02 new #2 // class Test 3: 59 dup 4: b7 00 03 invokespecial #3 // Method Test."<init>":()V 7: 4c astore_1 8: 2b aload_1 9: b6 00 04 invokevirtual #4 // Method Test.fn:()V 12: b1 return
好,今天就寫到這里吧~