讀書會第12期對于jvm運行時數(shù)據(jù)區(qū)域,我做了一些更深層次的解讀和理解

      網(wǎng)友投稿 675 2022-05-30

      【讀書會第十二期】

      假期借著華為云讀書會的活動,重讀了一遍《深入理解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ù)棧就是這個作用。

      【讀書會第12期】對于jvm運行時數(shù)據(jù)區(qū)域,我做了一些更深層次的解讀和理解

      當(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)容。

      上一篇:前端開發(fā)中常用的幾種設(shè)計模式(前端開發(fā)中用到哪些設(shè)計模式)
      下一篇:Excel如何批量修改數(shù)據(jù)標(biāo)簽(excel工作表標(biāo)簽批量修改)
      相關(guān)文章
      国产精品亚洲专一区二区三区| 色噜噜综合亚洲av中文无码| 久久久亚洲欧洲日产国码二区| 亚洲人午夜射精精品日韩| 亚洲视频在线观看2018| 久久av无码专区亚洲av桃花岛| 亚洲男人的天堂一区二区| 精品久久久久久久久亚洲偷窥女厕| 国产成人精品亚洲日本在线| 91亚洲精品视频| 亚洲尹人九九大色香蕉网站| 亚洲精品无码成人AAA片| 中文字幕精品亚洲无线码一区| 亚洲a∨国产av综合av下载| 亚洲国产精品嫩草影院| 亚洲爆乳无码专区www| 亚洲精品无码成人片久久不卡| 亚洲乱妇熟女爽到高潮的片| 亚洲精品无码久久久久APP | 亚洲fuli在线观看| 亚洲人成网站色在线观看| 亚洲中文字幕AV每天更新| 亚洲人av高清无码| 国产成人人综合亚洲欧美丁香花| 亚洲AV无码AV男人的天堂不卡| 国产精品亚洲а∨天堂2021 | 久久久久亚洲精品无码网址色欲| 激情小说亚洲图片| 亚洲高清免费视频| 国产偷国产偷亚洲清高动态图| 亚洲乳大丰满中文字幕| 久久国产亚洲电影天堂| 亚洲精品视频免费在线观看| 亚洲自国产拍揄拍| 亚洲aⅴ无码专区在线观看| 亚洲无线一二三四区手机| 亚洲熟妇丰满多毛XXXX| 亚洲精品无码不卡| 亚洲伊人久久大香线蕉| 亚洲狠狠色丁香婷婷综合| 亚洲精品高清在线|