【讀書會第12期】對于jvm運行時數(shù)據(jù)區(qū)域,我做了一些更深層次的解讀和理解
【讀書會第十二期】
假期借著華為云讀書會的活動,重讀了一遍《深入理解java虛擬機(jī)》, 發(fā)現(xiàn)第一遍讀“運行時數(shù)據(jù)區(qū)”相關(guān)內(nèi)容的時候,只關(guān)注了最簡單的概念部分,對于其中的細(xì)節(jié)部分沒有深入探究,覺得那些東西太底層了,沒啥用。
其實他們背后的原理,和我們平時運行進(jìn)程時的各種報錯息息相關(guān)。
另外如果能理解運行時數(shù)據(jù)區(qū),也能夠?qū)Α按a究竟是如何運行的”有更深的理解。
看來經(jīng)典書籍要多讀多總結(jié),是有道理的。
于是在閱讀這個章節(jié)時,針對每個結(jié)構(gòu),思考了非常多的問題,提出了很多QA,方便進(jìn)行深度的思考和學(xué)習(xí)。
歡迎關(guān)注一下我的華為云社區(qū)賬號或者社區(qū)讀書會活動。
歡迎點擊該鏈接報名參加讀書會,一起成長學(xué)習(xí)和交流!
報名鏈接
jvm全局結(jié)構(gòu)
java堆
程序計數(shù)器
虛擬機(jī)棧區(qū)域
操作數(shù)棧
棧幀
局部變量表
動態(tài)鏈接
返回地址
方法區(qū)
最后的感想
jvm全局結(jié)構(gòu)
首先是一張經(jīng)典的jvm運行時內(nèi)存區(qū)域劃分的圖,我自己畫了一張:
Q: 存在多個線程時,剛才提到的5個區(qū)域是怎么分布的?
A:
每個線程,都有自己獨立的虛擬機(jī)棧、獨立的程序計數(shù)器PC。
而方法區(qū)和堆是線程們共用的。
java堆
java堆的內(nèi)容比較多,這里不探究對象分配的原理,后面會補(bǔ)充新的文章。這里只討論堆的一些其他細(xì)節(jié)問題。
Q: 堆是線程之間共用的,但這樣會導(dǎo)致頻繁發(fā)生沖突,是否要考慮并發(fā)問題?怎么辦?
A:
線程分配堆空間時,會先根據(jù)TLAB進(jìn)行獨立分配。
TLAB ——Thread Local Allocation Buffer, 中文名為線程本地分配緩沖區(qū)。
啟用了 TLAB 之后(-XX:+UseTLAB, 默認(rèn)是開啟的),JVM 會針對每一個線程在 Java 堆中預(yù)留一個內(nèi)存區(qū)域。
一旦某個區(qū)域確定劃分給某個線程,之后該線程需要分配內(nèi)存的時候,會優(yōu)先在這片區(qū)域中申請。這個區(qū)域針對分配內(nèi)存這個動作而言是該線程私有的,因此在分配的時候不用進(jìn)行加鎖等保護(hù)性的操作
Q: 但是如果恰巧多個線程在試圖競爭同一個TLAB預(yù)留空間時(即都在試圖擴(kuò)容),發(fā)生沖突怎么辦?
A:
在預(yù)留這個動作發(fā)生的時候,需要進(jìn)行加鎖或者采用** CAS(compareAndSet) **等操作進(jìn)行保護(hù),避免多個線程預(yù)留同一個區(qū)域
Q: 分配的時候,在TLAB區(qū)域里,怎么知道放在哪個位置呢?
A:
具體的分配內(nèi)存有兩種情況(和垃圾回收機(jī)制有關(guān))
第一種情況是內(nèi)存空間絕對規(guī)整。(對應(yīng)使用回收-整理/復(fù)制算法的垃圾回收區(qū))
第二種情況是內(nèi)存空間是不連續(xù)的。(對應(yīng)使用回收-清除算法的垃圾回收區(qū))
對于內(nèi)存絕對規(guī)整的情況相對簡單一些,虛擬機(jī)只需要在被占用的內(nèi)存和可用空間之間移動指針即可,這種方式被稱為指針碰撞。
對于內(nèi)存不規(guī)整的情況稍微復(fù)雜一點,這時候虛擬機(jī)需要維護(hù)一個列表,來記錄哪些內(nèi)存是可用的。分配內(nèi)存的時候需要找到一個可用的內(nèi)存空間,然后在列表上記錄下已被分配,這種方式成為空閑列表。
程序計數(shù)器
Q: PC計數(shù)器是整個jvm共有的嗎?
A:
不是的,是每個線程各自有一個, 而且是java自己定義的線程PC, 和CPU里的PC寄存器不同。
Q: PC計數(shù)器有啥用? 那如果沒有PC寄存器呢? 我不是也能一條條執(zhí)行,遇到return指令,返回對應(yīng)地址即可,需要PC寄存器做啥?
A: PC寄存器的作用在于多線程切換的時候,能找到每個線程執(zhí)行的位置,所以它是線程私有的一個寄存器,知道當(dāng)前運行到哪了。如果沒有,一旦隨機(jī)切換就不知道咋辦了。你總需要一個地方存儲這個線程當(dāng)前執(zhí)行情況,但又要保持獨立性,所以不可能存到其他線程的空間里。
Q: 為什么native方法的程序計數(shù)器為0(undefine)?如果發(fā)生線程切換,怎么辦?
A:
注意,jvm內(nèi)存結(jié)構(gòu)里的PC計數(shù)器是jvm自己定義的“字節(jié)碼指令”執(zhí)行寄存器。
對于native方法,并不在字節(jié)碼的范圍,不指向方法區(qū)里的任何指令位置。
因此native方法其實不是由jvm管理的,如果線程切換,他執(zhí)行到哪邊,取決于OS的底層機(jī)器碼計數(shù)器實現(xiàn)。
以HotSpot VM的實現(xiàn)為例,它目前在大多數(shù)平臺上都使用1:1模型,也就是每個Java線程都直接映射到一個OS線程上執(zhí)行。此時,native方法就由原生平臺直接執(zhí)行,并不需要理會抽象的JVM層面上的“pc寄存器”概念——原生的CPU上真正的PC寄存器是怎樣就是怎樣。就像一個用C或C++寫的多線程程序,它在線程切換的時候是怎樣的,Java的native方法也就是怎樣的。
Q: PC計數(shù)器里存的到底是啥?是指令地址嗎?
A:
錯誤! 存的不是地址,而是這個方法的字節(jié)碼偏移。例如0、1、5、6這種。
Q: 那么怎么知道實際的字節(jié)碼位置?
A: 這個就要結(jié)合下文提到的棧幀中的動態(tài)鏈接,來聯(lián)合計算實際字節(jié)碼位置了。
虛擬機(jī)棧區(qū)域
Q: 什么是棧幀?
A: 每個線程有一個自己的棧幀,然后運行到每個方法時,每個方法中都會可以理解為是攝影里的一幀。
Q: 棧幀里包含什么?
A:
局部變量表
操作數(shù)棧
動態(tài)鏈接
方法返回地址
其實與上面這4樣配合的,還有個上文提到的“程序計數(shù)器”,才共同實現(xiàn)了jvm指令的執(zhí)行。
操作數(shù)棧
Q: 什么是操作數(shù)棧
A:
可以理解為jvm做計算時,需要一個臨時的寄存器,把需要計算的數(shù)據(jù)或者傳方法的參數(shù)放到棧中,然后做計算。
Q: 為什么一定要有操作數(shù)棧?如果要做a+b,我直接從變量表上取a的值和b的值,加起來不就好了?
A:
那我如果是 a + bc呢, 這個bc的值放哪里?
如果是a+b*(c+d)呢?
這時候如果你學(xué)習(xí)過數(shù)據(jù)結(jié)構(gòu)里棧的應(yīng)用 ,就會知道 模擬一個計算器,往往需要一個棧。
而操作數(shù)棧就是這個作用。
當(dāng)你學(xué)習(xí)jvm指令時,就會看到有專門的指令就是取棧頂或者把值推送到棧頂?shù)闹噶睢?/p>
這樣做加法的時候,也就不用關(guān)心變量的地址了,只要你把棧頂?shù)闹荡婧茫抑苯幽萌ゼ泳托小?/p>
棧幀
Q: 棧幀的大小什么時候確定?
A:
在編譯程序代碼的時候.
注意, 圖例提到的棧大小,并不是指線程堆棧的最大深度,
而是指“操作數(shù)棧”的最大深度。(注意這個深度存在類文件字節(jié)碼中對應(yīng)方法的屬性表中)
即jvm能夠通過分析代碼中可能存在多少個變量以及計算空間,來確定局部變量表和最大操作數(shù)棧的一個深度。
局部變量表
Q: 什么是局部變量表?
A:
每個線程所在棧幀都會有一個自己的局部變量表,里面存儲方法中使用到的局部變量。
Q: 局部變量的槽又是什么?
A:
returnAddress類型是為字節(jié)碼指令jsr、jsr_w和ret服務(wù)的,它指向了一條字節(jié)碼指令的地址。
局部變量表的容量以變量槽(Slot)為最小單位,32位虛擬機(jī)中一個Slot可以存放一個32位以內(nèi)的數(shù)據(jù)類型(boolean、byte、char、short、int、float、reference和returnAddress八種)
slot的長度可以隨著處理器、操作系統(tǒng)的不同而變化, 不是絕對的32位。
jvm概念中說的是”slot一定能存放下1個boolean\byte\int\引用地址\返回地址returnAddress“等不包括long在內(nèi)的內(nèi)容。
如果要訪問long,需要做2次局部變量slot的讀取,讀取n和n+1,不允許單獨訪問,如果有問題會在字節(jié)碼校驗中報錯。
Q: 局部變量表里的returnAddress和棧幀里的返回地址returnAddress有啥區(qū)別?
A:
局部變量表里的的returnAddress,是老版本jvm用于處理異常跳轉(zhuǎn)的(jsr\jsr_w\ret指令,新版本基本都用code里的異常表來代替),而棧幀里的返回地址,是返回到上一層棧幀的代碼調(diào)用位置,更新PC計數(shù)器用的。
Q: 局部變量表的slot可以被覆蓋嗎?這個設(shè)計有什么好處
A:
可以減少局部變量表的空間,通過分析每個局部變量的使用生命周期,在某變量不再被使用后,讓其他變量可以覆蓋這個槽的位置。
另一方面,覆蓋的機(jī)制,可以將一些局部變量上已經(jīng)不使用的大對象解除引用,例如對一些大的變量做=null的操作,那么可以盡早進(jìn)行垃圾回收(因為棧幀的局部變量表里的每個slot都是一個gcRoot)
Q: 設(shè)置null值,就一定會覆蓋slot嗎?
A:
不一定,有時候JIT編譯優(yōu)化,可能會處理掉這個無用的=null的操作,且能正確處理slot中已經(jīng)不被使用的變量。
按照書里的說法,正好有大對象,然后還停留在局部變量表里的概率是比較低的。不建議那么做了
Q:為什么java中局部變量沒有默認(rèn)初始?
A:
我的理解,局部變量在局部變量表中,而局部變量表是運行時生成的, 而非在堆上生成,因此不會有堆對象創(chuàng)建時的那個默認(rèn)值賦值操作。 即jvm定義上, 就是局部變量沒有初始化前的’準(zhǔn)備‘這個階段的,也就不存在默認(rèn)賦值的指令行為。
如果硬要說為什么,如果每個局部變量都復(fù)制,肯定會影響執(zhí)行效率,因此不如不賦值。,所以必須通過賦值指令在運行時給他賦值。(沒找到很好的解釋,有更好理解的可以幫忙回答一下,其實就是)
另外如果每個局部變量都有,那可能指令數(shù)量就會變多,因為你需要放入很多賦值指令?
閱讀JMM內(nèi)存模型時的另一個解釋:
對于未同步或未正確同步的多線程程序,JMM只提供最小安全性.
線程執(zhí)行時讀取到的值,要么是之前某個線程寫入的值,要么是默認(rèn)值(0,Null,F(xiàn)alse),JMM保證線程讀操作讀取到的值不會無中生有(Out Of Thin Air)的冒出來
對于全局變量(類對象成員),必須有默認(rèn)初始化,為了滿足多線程環(huán)境下的最小安全性。
但對于局部變量,不存在被多線程使用,因此一定后面可以拼接一個指令,所以不需要默認(rèn)初始化的動作。
Q: 執(zhí)行多次方法,一個棧上有多個棧幀,每個棧幀都有各自的局部變量表和操作數(shù)棧,上下的棧幀之間可能存在共享的情況嗎?
A:
可能存在。即上下兩個棧幀之間, 可能有操作數(shù)棧可以直接操作另一個棧幀局部變量的情況。這樣可以避免額外的參數(shù)復(fù)制傳遞。
什么時候觸發(fā)?不清楚
動態(tài)鏈接
Q : 棧幀里的動態(tài)鏈接又是啥?
A:
首先明確一點, 每一個棧幀,不一定是”動態(tài)”鏈接,但一定會有一個指向常量池中方法的引用。
為什么棧幀里需要存這個指向方法的引用?
首先,當(dāng)你進(jìn)入一個方法,準(zhǔn)備生成一個棧幀,放到線程上時,你需要知道你這個代碼執(zhí)行的是什么代碼,才能進(jìn)行后面的操作。
如果是構(gòu)造方法、final方法,則會編譯器進(jìn)行靜態(tài)鏈接。
如果是虛方法,則會進(jìn)行動態(tài)鏈接,運行期只是從類對象中,拿到了一個符號引用,
但是這個引用指向哪個方法?則通過下面的過程進(jìn)行定位和尋找,把符號引用轉(zhuǎn)成實際方法的直接引用。
因此要提供一個引用,指向常量池里的方法。指向后,就能知道程序位置。
然后字節(jié)碼實際引用位置 + PC計數(shù)器偏移,就能知道當(dāng)前線程執(zhí)行到哪個方法的哪一步指令上了。
(關(guān)于動態(tài)鏈接這個名稱的由來,是因為“動態(tài)分派”的存在,你這個方法位置是不確定的,和實際對象+方法名有關(guān), 所以稱為動態(tài)鏈接。)
返回地址
Q: 既然有PC寄存器,棧幀里的返回地址的作用是什么?
A:
方法A調(diào)用方法B的時候,PC寄存器會跟著移動到B方法去。當(dāng)B執(zhí)行完后,要能返回A繼續(xù)執(zhí)行,就需要A當(dāng)時執(zhí)行到的那條指令的地址。所以,在B的棧幀中保存A當(dāng)時的指令地址(當(dāng)時PC寄存器的值),當(dāng)B執(zhí)行完后,根據(jù)此返回地址跳回A。通過返回地址,從而知道當(dāng)前線程的上一級應(yīng)該從PC的第幾行偏移開始。
另外除了正常通過ret指令退出,還可能是出現(xiàn)異常時,如果沒有在異常表里被捕捉并處理,也會通過異常完成出口, 使用返回地址返回到上一層。
Q: 棧幀中的方法退出時,會觸發(fā)哪些動作?
A:
當(dāng)前棧幀出棧
恢復(fù)上層方法的局部變量表和操作數(shù)棧
如果有返回值,把返回值壓入操作數(shù)棧的棧頂(因為馬上就要被調(diào)用了)
調(diào)整這個線程棧的PC計數(shù)器,改成returnAddress對應(yīng)的那個指令位置地址,然后繼續(xù)往下調(diào)用執(zhí)行。
Q: 棧幀除了上面提到的幾個,還有其他的信息嗎?
A:
有些支持調(diào)試的虛擬機(jī),可能會補(bǔ)充很多調(diào)試相關(guān)的信息。
方法區(qū)
Q: 方法區(qū)里存的是class字節(jié)碼嗎?
A:
不是。經(jīng)過類的加載、鏈接、初始化之后, class字節(jié)碼對于進(jìn)程來說就沒用了。
存了以下內(nèi)容:
每個類的類型信息:類名、父類類名、修飾符、接口
字段信息field(域信息):字段名、字段類型、字段修飾符
方法信息,包括方法名、類型、參數(shù)、修飾符、字節(jié)碼、一場表
如下:
類的靜態(tài)變量
常量池,存儲常量
注意,符號引用、類引用、實際類名等信息等都是放在常量池中的。
Q: 元空間與永久代到底是怎么回事?
A:
方法區(qū)和“PermGen space”又有著本質(zhì)的區(qū)別。
前者是 JVM 的規(guī)范,而后者則是 JVM 規(guī)范的一種實現(xiàn)
并且只有 HotSpot 才有 “PermGen space”,而對于其他類型的虛擬機(jī),如 JRockit(Oracle)、J9(IBM) 并沒有“PermGen space”。
元空間與永久代之間最大的區(qū)別在于:元空間并不在虛擬機(jī)中,而是使用本地內(nèi)存。因此,默認(rèn)情況下,元空間的大小僅受本地內(nèi)存限制,但可以通過以下參數(shù)來指定元空間的大小
-XX:MetaspaceSize和-XX:MaxMetaspaceSize
Q: 為什么要替換永久代
A:
替換永久代的其他原因:
字符串存在永久代中,容易出現(xiàn)性能問題和內(nèi)存溢出。
類及方法的信息等比較難確定其大小,因此對于永久代的大小指定比較困難,太小容易出現(xiàn)永久代溢出,太大則容易導(dǎo)致老年代溢出。
永久代會為 GC 帶來不必要的復(fù)雜度,并且回收效率偏低。
最后的感想
好累,終于寫完了,感覺能看到最后的人不會太多,但一通詳細(xì)地分析和解決中間發(fā)現(xiàn)的問題,還是收獲了不少。
關(guān)于jvm運行時數(shù)據(jù)區(qū),最重要的不是去死記硬背,而是試圖在腦中構(gòu)建一個指令運行的邏輯流程。
且對于很多沒有學(xué)習(xí)過計算機(jī)底層原理(例如CSAPP這本書) 的人來說, 是很難接觸到計算機(jī)是如何執(zhí)行機(jī)器碼指令的。
而java虛擬機(jī)棧可以更好理解 指令是如何運行的(雖然這不是機(jī)器碼,而是jvm字節(jié)碼)。
但是通過運行時數(shù)據(jù)區(qū)的各種行為和概念, 我們可以快速對應(yīng)到j(luò)ava中常見的各種操作。
這對于很多入門時直奔刪減改查的同學(xué)來說, 是不可多得的學(xué)習(xí)底層的機(jī)會。
圖解筆記系列也會持續(xù)更新下去,爭取做全網(wǎng)最細(xì)又最大的java分享文章。如果感覺不錯,歡迎掃描文末的二維碼,參加社區(qū)的活動并抽獎!
EI企業(yè)智能 Java JVM 可信智能計算服務(wù) TICS 智能數(shù)據(jù)
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內(nèi)容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網(wǎng)站將在24小時內(nèi)刪除侵權(quán)內(nèi)容。