JVM 基礎(chǔ)面試題總結(jié)
hey guys, 各位小伙伴們大家好,這里是程序員cxuan,歡迎你收看我新一期的文章,這篇文章我花了幾天時(shí)間給你匯總了一波 JVM 的基礎(chǔ)知識(shí)和面試題,內(nèi)容還不是很全,我還在連載中,這篇文章相當(dāng)于是第一彈,廢話不多說(shuō),走起!
JVM 的主要作用是什么?
JVM 就是 Java Virtual Machine(Java虛擬機(jī))的縮寫,JVM 屏蔽了與具體操作系統(tǒng)平臺(tái)相關(guān)的信息,使 Java 程序只需生成在 Java 虛擬機(jī)上運(yùn)行的目標(biāo)代碼 (字節(jié)碼),就可以在不同的平臺(tái)上運(yùn)行。
請(qǐng)你描述一下 Java 的內(nèi)存區(qū)域?
JVM 在執(zhí)行 Java 程序的過(guò)程中會(huì)把它管理的內(nèi)存分為若干個(gè)不同的區(qū)域,這些組成部分有些是線程私有的,有些則是線程共享的,Java 內(nèi)存區(qū)域也叫做運(yùn)行時(shí)數(shù)據(jù)區(qū),它的具體劃分如下:

虛擬機(jī)棧: Java 虛擬機(jī)棧是線程私有的數(shù)據(jù)區(qū),Java 虛擬機(jī)棧的生命周期與線程相同,虛擬機(jī)棧也是局部變量的存儲(chǔ)位置。方法在執(zhí)行過(guò)程中,會(huì)在虛擬機(jī)棧中創(chuàng)建一個(gè)棧幀(stack frame)。每個(gè)方法執(zhí)行的過(guò)程就對(duì)應(yīng)了一個(gè)入棧和出棧的過(guò)程。

本地方法棧: 本地方法棧也是線程私有的數(shù)據(jù)區(qū),本地方法棧存儲(chǔ)的區(qū)域主要是 Java 中使用native關(guān)鍵字修飾的方法所存儲(chǔ)的區(qū)域。程序計(jì)數(shù)器:程序計(jì)數(shù)器也是線程私有的數(shù)據(jù)區(qū),這部分區(qū)域用于存儲(chǔ)線程的指令地址,用于判斷線程的分支、循環(huán)、跳轉(zhuǎn)、異常、線程切換和恢復(fù)等功能,這些都通過(guò)程序計(jì)數(shù)器來(lái)完成。方法區(qū):方法區(qū)是各個(gè)線程共享的內(nèi)存區(qū)域,它用于存儲(chǔ)虛擬機(jī)加載的 類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。堆:堆是線程共享的數(shù)據(jù)區(qū),堆是 JVM 中最大的一塊存儲(chǔ)區(qū)域,所有的對(duì)象實(shí)例都會(huì)分配在堆上。JDK 1.7后,字符串常量池從永久代中剝離出來(lái),存放在堆中。堆空間的內(nèi)存分配(默認(rèn)情況下):
命令行上執(zhí)行如下命令,會(huì)查看默認(rèn)的 JVM 參數(shù)。
java -XX:+PrintFlagsFinal -version輸出的內(nèi)容非常多,但是只有兩行能夠反映出上面的內(nèi)存分配結(jié)果



eden 區(qū):8/10 的年輕代空間 survivor 0 : 1/10 的年輕代空間 survivor 1 : 1/10 的年輕代空間 老年代 :三分之二的堆空間 年輕代 :三分之一的堆空間 運(yùn)行時(shí)常量池:運(yùn)行時(shí)常量池又被稱為Runtime Constant Pool,這塊區(qū)域是方法區(qū)的一部分,它的名字非常有意思,通常被稱為非堆。它并不要求常量一定只有在編譯期才能產(chǎn)生,也就是并非編譯期間將常量放在常量池中,運(yùn)行期間也可以將新的常量放入常量池中,String 的 intern 方法就是一個(gè)典型的例子。
請(qǐng)你描述一下 Java 中的類加載機(jī)制?
Java 虛擬機(jī)負(fù)責(zé)把描述類的數(shù)據(jù)從 Class 文件加載到系統(tǒng)內(nèi)存中,并對(duì)類的數(shù)據(jù)進(jìn)行校驗(yàn)、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機(jī)直接使用的 Java 類型,這個(gè)過(guò)程被稱之為 Java 的類加載機(jī)制。
一個(gè)類從被加載到虛擬機(jī)內(nèi)存開始,到卸載出內(nèi)存為止,一共會(huì)經(jīng)歷下面這些過(guò)程。

類加載機(jī)制一共有五個(gè)步驟,分別是加載、鏈接、初始化、使用和卸載階段,這五個(gè)階段的順序是確定的。
其中鏈接階段會(huì)細(xì)分成三個(gè)階段,分別是驗(yàn)證、準(zhǔn)備、解析階段,這三個(gè)階段的順序是不確定的,這三個(gè)階段通常交互進(jìn)行。解析階段通常會(huì)在初始化之后再開始,這是為了支持 Java 語(yǔ)言的運(yùn)行時(shí)綁定特性(也被稱為動(dòng)態(tài)綁定)。
下面我們就來(lái)聊一下這幾個(gè)過(guò)程。
加載
關(guān)于什么時(shí)候開始加載這個(gè)過(guò)程,《Java 虛擬機(jī)規(guī)范》并沒有強(qiáng)制約束,所以這一點(diǎn)我們可以自由實(shí)現(xiàn)。加載是整個(gè)類加載過(guò)程的第一個(gè)階段,在這個(gè)階段,Java 虛擬機(jī)需要完成三件事情:
通過(guò)一個(gè)類的全限定名來(lái)獲取定義此類的二進(jìn)制字節(jié)流。 將這個(gè)字節(jié)流表示的一種存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)換為運(yùn)行時(shí)數(shù)據(jù)區(qū)中方法區(qū)的數(shù)據(jù)結(jié)構(gòu)。 在內(nèi)存中生成一個(gè) Class 對(duì)象,這個(gè)對(duì)象就代表了這個(gè)數(shù)據(jù)結(jié)構(gòu)的訪問入口。
《Java 虛擬機(jī)規(guī)范》并未規(guī)定全限定名是如何獲取的,所以現(xiàn)在業(yè)界有很多獲取全限定名的方式:
從 ZIP 包中讀取,最終會(huì)改變?yōu)?JAR、EAR、WAR 格式。 從網(wǎng)絡(luò)中獲取,最常見的應(yīng)用就是 Web Applet。 運(yùn)行時(shí)動(dòng)態(tài)生成,使用最多的就是動(dòng)態(tài)代理技術(shù)。 由其他文件生成,比如 JSP 應(yīng)用場(chǎng)景,由 JSP 文件生成對(duì)應(yīng)的 Class 文件。 從數(shù)據(jù)庫(kù)中讀取,這種場(chǎng)景就比較小了。 可以從加密文件中獲取,這是典型的防止 Class 文件被反編譯的保護(hù)措施。
加載階段既可以使用虛擬機(jī)內(nèi)置的引導(dǎo)類加載器來(lái)完成,也可以使用用戶自定義的類加載器來(lái)完成。程序員可以通過(guò)自己定義類加載器來(lái)控制字節(jié)流的訪問方式。
數(shù)組的加載不需要通過(guò)類加載器來(lái)創(chuàng)建,它是直接在內(nèi)存中分配,但是數(shù)組的元素類型(數(shù)組去掉所有維度的類型)最終還是要靠類加載器來(lái)完成加載。
驗(yàn)證
加載過(guò)后的下一個(gè)階段就是驗(yàn)證,因?yàn)槲覀兩弦徊街v到在內(nèi)存中生成了一個(gè) Class 對(duì)象,這個(gè)對(duì)象是訪問其代表數(shù)據(jù)結(jié)構(gòu)的入口,所以這一步驗(yàn)證的工作就是確保 Class 文件的字節(jié)流中的內(nèi)容符合《Java 虛擬機(jī)規(guī)范》中的要求,保證這些信息被當(dāng)作代碼運(yùn)行后,它不會(huì)威脅到虛擬機(jī)的安全。
驗(yàn)證階段主要分為四個(gè)階段的檢驗(yàn):
文件格式驗(yàn)證。 元數(shù)據(jù)驗(yàn)證。 字節(jié)碼驗(yàn)證。 符號(hào)引用驗(yàn)證。
文件格式驗(yàn)證
這一階段可能會(huì)包含下面這些驗(yàn)證點(diǎn):
魔數(shù)是否以 0xCAFEBABE開頭。主、次版本號(hào)是否在當(dāng)前 Java 虛擬機(jī)接受范圍之內(nèi)。 常量池的常量中是否有不支持的常量類型。 指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量。 CONSTANT_Utf8_info 型的常量中是否有不符合 UTF8 編碼的數(shù)據(jù)。 Class 文件中各個(gè)部分及文件本身是否有被刪除的或附加的其他信息。
實(shí)際上驗(yàn)證點(diǎn)遠(yuǎn)遠(yuǎn)不止有這些,上面這些只是從 HotSpot 源碼中摘抄的一小段內(nèi)容。
元數(shù)據(jù)驗(yàn)證
這一階段主要是對(duì)字節(jié)碼描述的信息進(jìn)行語(yǔ)義分析,以確保描述的信息符合《Java 語(yǔ)言規(guī)范》,驗(yàn)證點(diǎn)包括
驗(yàn)證的類是否有父類(除了 Object 類之外,所有的類都應(yīng)該有父類)。 要驗(yàn)證類的父類是否繼承了不允許繼承的類。 如果這個(gè)類不是抽象類,那么這個(gè)類是否實(shí)現(xiàn)了父類或者接口中要求的所有方法。 是否覆蓋了 final 字段,是否出現(xiàn)了不符合規(guī)定的重載等。
需要記住這一階段只是對(duì)《Java 語(yǔ)言規(guī)范》的驗(yàn)證。
字節(jié)碼驗(yàn)證
字節(jié)碼驗(yàn)證階段是最復(fù)雜的一個(gè)階段,這個(gè)階段主要是確定程序語(yǔ)意是否合法、是否是符合邏輯的。這個(gè)階段主要是對(duì)類的方法體(Class 文件中的 Code 屬性)進(jìn)行校驗(yàn)分析。這部分驗(yàn)證包括
確保操作數(shù)棧的數(shù)據(jù)類型和實(shí)際執(zhí)行時(shí)的數(shù)據(jù)類型是否一致。 保證任何跳轉(zhuǎn)指令不會(huì)跳出到方法體外的字節(jié)碼指令上。 保證方法體中的類型轉(zhuǎn)換是有效的,例如可以把一個(gè)子類對(duì)象賦值給父類數(shù)據(jù)類型,但是不能把父類數(shù)據(jù)類型賦值給子類等諸如此不安全的類型轉(zhuǎn)換。 其他驗(yàn)證。
如果沒有通過(guò)字節(jié)碼驗(yàn)證,就說(shuō)明驗(yàn)證出問題。但是不一定通過(guò)了字節(jié)碼驗(yàn)證,就能保證程序是安全的。
符號(hào)引用驗(yàn)證
最后一個(gè)階段的校驗(yàn)行為發(fā)生在虛擬機(jī)將符號(hào)引用轉(zhuǎn)換為直接引用的時(shí)候,這個(gè)轉(zhuǎn)化將在連接的第三個(gè)階段,即解析階段中發(fā)生。符號(hào)引用驗(yàn)證可以看作是對(duì)類自身以外的各類信息進(jìn)行匹配性校驗(yàn),這個(gè)驗(yàn)證主要包括
符號(hào)引用中的字符串全限定名是否能找到對(duì)應(yīng)的類。 指定類中是否存在符合方法的字段描述符以及簡(jiǎn)單名稱所描述的方法和字段。 符號(hào)引用的類、字段方法的可訪問性是否可被當(dāng)前類所訪問。 其他驗(yàn)證。
這一階段主要是確保解析行為能否正常執(zhí)行,如果無(wú)法通過(guò)符號(hào)引用驗(yàn)證,就會(huì)出現(xiàn)類似 IllegalAccessError、NoSuchFieldError、NoSuchMethodError 等錯(cuò)誤。
驗(yàn)證階段對(duì)于虛擬機(jī)來(lái)說(shuō)非常重要,如果能通過(guò)驗(yàn)證,就說(shuō)明你的程序在運(yùn)行時(shí)不會(huì)產(chǎn)生任何影響。
準(zhǔn)備
準(zhǔn)備階段是為類中的變量分配內(nèi)存并設(shè)置其初始值的階段,這些變量所使用的內(nèi)存都應(yīng)當(dāng)在方法區(qū)中進(jìn)行分配,在 JDK 7 之前,HotSpot 使用永久代來(lái)實(shí)現(xiàn)方法區(qū),是符合這種邏輯概念的。而在 JDK 8 之后,變量則會(huì)隨著 Class 對(duì)象一起存放在 Java 堆中。
下面通常情況下的基本類型和引用類型的初始值

除了"通常情況"下,還有一些"例外情況",如果類字段屬性中存在 ConstantValue 屬性,那就這個(gè)變量值在初始階段就會(huì)初始化為 ConstantValue 屬性所指定的初始值,比如
public static final int value = "666";
編譯時(shí)就會(huì)把 value 的值設(shè)置為 666。
解析
解析階段是 Java 虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過(guò)程。
符號(hào)引用:符號(hào)引用以一組符號(hào)來(lái)描述所引用的目標(biāo)。符號(hào)引用可以是任何形式的字面量,只要使用時(shí)能無(wú)歧義地定位到目標(biāo)即可,符號(hào)引用和虛擬機(jī)的布局無(wú)關(guān)。直接引用:直接引用可以直接指向目標(biāo)的指針、相對(duì)便宜量或者一個(gè)能間接定位到目標(biāo)的句柄。直接引用和虛擬機(jī)的布局是相關(guān)的,不同的虛擬機(jī)對(duì)于相同的符號(hào)引用所翻譯出來(lái)的直接引用一般是不同的。如果有了直接引用,那么直接引用的目標(biāo)一定被加載到了內(nèi)存中。
這樣說(shuō)你可能還有點(diǎn)不明白,我再換一種說(shuō)法:
在編譯的時(shí)候一個(gè)每個(gè) Java 類都會(huì)被編譯成一個(gè) class 文件,但在編譯的時(shí)候虛擬機(jī)并不知道所引用類的地址,所以就用符號(hào)引用來(lái)代替,而在這個(gè)解析階段就是為了把這個(gè)符號(hào)引用轉(zhuǎn)化成為真正的地址的階段。
《Java 虛擬機(jī)規(guī)范》并未規(guī)定解析階段發(fā)生的時(shí)間,只要求了在 anewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、ldc2_w、multianewarray、new、putfield 和 putstatic 這 17 個(gè)用于操作符號(hào)引用的字節(jié)碼指令之前,先對(duì)所使用的符號(hào)引用進(jìn)行解析。
解析也分為四個(gè)步驟
類或接口的解析 字段解析 方法解析 接口方法解析
初始化
初始化是類加載過(guò)程的最后一個(gè)步驟,在之前的階段中,都是由 Java 虛擬機(jī)占主導(dǎo)作用,但是到了這一步,卻把主動(dòng)權(quán)移交給應(yīng)用程序。
對(duì)于初始化階段,《Java 虛擬機(jī)規(guī)范》嚴(yán)格規(guī)定了只有下面這六種情況下才會(huì)觸發(fā)類的初始化。
在遇到 new、getstatic、putstatic 或者 invokestatic 這四條字節(jié)碼指令時(shí),如果沒有進(jìn)行過(guò)初始化,那么首先觸發(fā)初始化。通過(guò)這四個(gè)字節(jié)碼的名稱可以判斷,這四條字節(jié)碼其實(shí)就兩個(gè)場(chǎng)景,調(diào)用 new 關(guān)鍵字的時(shí)候進(jìn)行初始化、讀取或者設(shè)置一個(gè)靜態(tài)字段的時(shí)候、調(diào)用靜態(tài)方法的時(shí)候。 在初始化類的時(shí)候,如果父類還沒有初始化,那么就需要先對(duì)父類進(jìn)行初始化。 在使用 java.lang.reflect 包的方法進(jìn)行反射調(diào)用的時(shí)候。 當(dāng)虛擬機(jī)啟動(dòng)時(shí),用戶需要指定執(zhí)行主類的時(shí)候,說(shuō)白了就是虛擬機(jī)會(huì)先初始化 main 方法這個(gè)類。 在使用 JDK 7 新加入的動(dòng)態(tài)語(yǔ)言支持時(shí),如果一個(gè) jafva.lang.invoke.MethodHandle 實(shí)例最后的解析結(jié)果為 REF_getstatic、REF_putstatic、REF_invokeStatic、REF_newInvokeSpecial 四種類型的方法句柄,并且這個(gè)方法句柄對(duì)應(yīng)的類沒有進(jìn)行過(guò)初始化,需要先對(duì)其進(jìn)行初始化。 當(dāng)一個(gè)接口中定義了 JDK 8 新加入的默認(rèn)方法(被 default 關(guān)鍵字修飾的接口方法)時(shí),如果有這個(gè)接口的實(shí)現(xiàn)類發(fā)生了初始化,那該接口要在其之前被初始化。
其實(shí)上面只有前四個(gè)大家需要知道就好了,后面兩個(gè)比較冷門。
如果說(shuō)要回答類加載的話,其實(shí)聊到這里已經(jīng)可以了,但是為了完整性,我們索性把后面兩個(gè)過(guò)程也來(lái)聊一聊。
使用
這個(gè)階段沒什么可說(shuō)的,就是初始化之后的代碼由 JVM 來(lái)動(dòng)態(tài)調(diào)用執(zhí)行。
卸載
當(dāng)代表一個(gè)類的 Class 對(duì)象不再被引用,那么 Class 對(duì)象的生命周期就結(jié)束了,對(duì)應(yīng)的在方法區(qū)中的數(shù)據(jù)也會(huì)被卸載。
??但是需要注意一點(diǎn):JVM 自帶的類加載器裝載的類,是不會(huì)卸載的,由用戶自定義的類加載器加載的類是可以卸載的。
在 JVM 中,對(duì)象是如何創(chuàng)建的?
如果要回答對(duì)象是怎么創(chuàng)建的,我們一般想到的回答是直接 new 出來(lái)就行了,這個(gè)回答不僅局限于編程中,也融入在我們生活中的方方面面。
但是遇到面試的時(shí)候你只回答一個(gè)"new 出來(lái)就行了"顯然是不行的,因?yàn)槊嬖嚫呄蛴谧屇憬忉尞?dāng)程序執(zhí)行到 new 這條指令時(shí),它的背后發(fā)生了什么。
所以你需要從 JVM 的角度來(lái)解釋這件事情。
當(dāng)虛擬機(jī)遇到一個(gè) new 指令時(shí)(其實(shí)就是字節(jié)碼),首先會(huì)去檢查這個(gè)指令的參數(shù)是否能在常量池中定位到一個(gè)類的符號(hào)引用,并且檢查這個(gè)符號(hào)引用所代表的類是否已經(jīng)被加載、解析和初始化。
因?yàn)榇藭r(shí)很可能不知道具體的類是什么,所以這里使用的是符號(hào)引用。
如果發(fā)現(xiàn)這個(gè)類沒有經(jīng)過(guò)上面類加載的過(guò)程,那么就執(zhí)行相應(yīng)的類加載過(guò)程。
類檢查完成后,接下來(lái)虛擬機(jī)將會(huì)為新生對(duì)象分配內(nèi)存,對(duì)象所需的大小在類加載完成后便可確定(我會(huì)在下面的面試題中介紹)。
分配內(nèi)存相當(dāng)于是把一塊固定的內(nèi)存塊從堆中劃分出來(lái)。劃分出來(lái)之后,虛擬機(jī)會(huì)將分配到的內(nèi)存空間都初始化為零值,如果使用了 TLAB(本地線程分配緩沖),這一項(xiàng)初始化工作可以提前在 TLAB 分配時(shí)進(jìn)行。這一步操作保證了對(duì)象實(shí)例字段在 Java 代碼中可以不賦值就能直接使用。
接下來(lái),Java 虛擬機(jī)還會(huì)對(duì)對(duì)象進(jìn)行必要的設(shè)置,比如確定對(duì)象是哪個(gè)類的實(shí)例、對(duì)象的 hashcode、對(duì)象的 gc 分代年齡信息。這些信息存放在對(duì)象的對(duì)象頭(Object Header)中。
如果上面的工作都做完后,從虛擬機(jī)的角度來(lái)說(shuō),一個(gè)新的對(duì)象就創(chuàng)建完畢了;但是對(duì)于程序員來(lái)說(shuō),對(duì)象創(chuàng)建才剛剛開始,因?yàn)闃?gòu)造函數(shù),即 Class 文件中的 <init>() 方法還沒有執(zhí)行,所有字段都為默認(rèn)的零值。new 指令之后才會(huì)執(zhí)行 <init>() 方法,然后按照程序員的意愿對(duì)對(duì)象進(jìn)行初始化,這樣一個(gè)對(duì)象才可能被完整的構(gòu)造出來(lái)。
內(nèi)存分配方式有哪些呢?
在類加載完成后,虛擬機(jī)需要為新生對(duì)象分配內(nèi)存,為對(duì)象分配內(nèi)存相當(dāng)于是把一塊確定的區(qū)域從堆中劃分出來(lái),這就涉及到一個(gè)問題,要?jiǎng)澐值亩褏^(qū)是否規(guī)整。
假設(shè) Java 堆中內(nèi)存是規(guī)整的,所有使用過(guò)的內(nèi)存放在一邊,未使用的內(nèi)存放在一邊,中間放著一個(gè)指針,這個(gè)指針為分界指示器。那么為新對(duì)象分配內(nèi)存空間就相當(dāng)于是把指針向空閑的空間挪動(dòng)對(duì)象大小相等的距離,這種內(nèi)存分配方式叫做指針碰撞(Bump The Pointer)。
如果 Java 堆中的內(nèi)存并不是規(guī)整的,已經(jīng)被使用的內(nèi)存和未被使用的內(nèi)存相互交錯(cuò)在一起,這種情況下就沒有辦法使用指針碰撞,這里就要使用另外一種記錄內(nèi)存使用的方式:空閑列表(Free List),空閑列表維護(hù)了一個(gè)列表,這個(gè)列表記錄了哪些內(nèi)存塊是可用的,在分配的時(shí)候從列表中找到一塊足夠大的空間劃分給對(duì)象實(shí)例,并更新列表上的記錄。
所以,上述兩種分配方式選擇哪個(gè),取決于 Java 堆是否規(guī)整來(lái)決定。在一些垃圾收集器的實(shí)現(xiàn)中,Serial、ParNew 等帶壓縮整理過(guò)程的收集器,使用的是指針碰撞;而使用 CMS 這種基于清除算法的收集器時(shí),使用的是空閑列表,具體的垃圾收集器我們后面會(huì)聊到。
請(qǐng)你說(shuō)一下對(duì)象的內(nèi)存布局?
在 hotspot 虛擬機(jī)中,對(duì)象在內(nèi)存中的布局分為三塊區(qū)域:
對(duì)象頭(Header)實(shí)例數(shù)據(jù)(Instance Data)對(duì)齊填充(Padding)
這三塊區(qū)域的內(nèi)存分布如下圖所示

我們來(lái)詳細(xì)介紹一下上面對(duì)象中的內(nèi)容。
對(duì)象頭 Header
對(duì)象頭 Header 主要包含 MarkWord 和對(duì)象指針 Klass Pointer,如果是數(shù)組的話,還要包含數(shù)組的長(zhǎng)度。

在 32 位的虛擬機(jī)中 MarkWord ,Klass Pointer 和數(shù)組長(zhǎng)度分別占用 32 位,也就是 4 字節(jié)。
如果是 64 位虛擬機(jī)的話,MarkWord ,Klass Pointer 和數(shù)組長(zhǎng)度分別占用 64 位,也就是 8 字節(jié)。
在 32 位虛擬機(jī)和 64 位虛擬機(jī)的 Mark Word 所占用的字節(jié)大小不一樣,32 位虛擬機(jī)的 Mark Word 和 Klass Pointer 分別占用 32 bits 的字節(jié),而 64 位虛擬機(jī)的 Mark Word 和 Klass Pointer 占用了64 bits 的字節(jié),下面我們以 32 位虛擬機(jī)為例,來(lái)看一下其 Mark Word 的字節(jié)具體是如何分配的。

用中文翻譯過(guò)來(lái)就是

無(wú)狀態(tài)也就是 無(wú)鎖的時(shí)候,對(duì)象頭開辟 25 bit 的空間用來(lái)存儲(chǔ)對(duì)象的 hashcode ,4 bit 用于存放分代年齡,1 bit 用來(lái)存放是否偏向鎖的標(biāo)識(shí)位,2 bit 用來(lái)存放鎖標(biāo)識(shí)位為 01。偏向鎖中劃分更細(xì),還是開辟 25 bit 的空間,其中 23 bit 用來(lái)存放線程ID,2bit 用來(lái)存放 epoch,4bit 存放分代年齡,1 bit 存放是否偏向鎖標(biāo)識(shí), 0 表示無(wú)鎖,1 表示偏向鎖,鎖的標(biāo)識(shí)位還是 01。輕量級(jí)鎖中直接開辟 30 bit 的空間存放指向棧中鎖記錄的指針,2bit 存放鎖的標(biāo)志位,其標(biāo)志位為 00。重量級(jí)鎖中和輕量級(jí)鎖一樣,30 bit 的空間用來(lái)存放指向重量級(jí)鎖的指針,2 bit 存放鎖的標(biāo)識(shí)位,為 11GC標(biāo)記開辟 30 bit 的內(nèi)存空間卻沒有占用,2 bit 空間存放鎖標(biāo)志位為 11。
其中無(wú)鎖和偏向鎖的鎖標(biāo)志位都是 01,只是在前面的 1 bit 區(qū)分了這是無(wú)鎖狀態(tài)還是偏向鎖狀態(tài)。
關(guān)于為什么這么分配的內(nèi)存,我們可以從 OpenJDK 中的markOop.hpp類中的枚舉窺出端倪

來(lái)解釋一下
age_bits 就是我們說(shuō)的分代回收的標(biāo)識(shí),占用4字節(jié) lock_bits 是鎖的標(biāo)志位,占用2個(gè)字節(jié) biased_lock_bits 是是否偏向鎖的標(biāo)識(shí),占用1個(gè)字節(jié)。 max_hash_bits 是針對(duì)無(wú)鎖計(jì)算的 hashcode 占用字節(jié)數(shù)量,如果是 32 位虛擬機(jī),就是 32 - 4 - 2 -1 = 25 byte,如果是 64 位虛擬機(jī),64 - 4 - 2 - 1 = 57 byte,但是會(huì)有 25 字節(jié)未使用,所以 64 位的 hashcode 占用 31 byte。 hash_bits 是針對(duì) 64 位虛擬機(jī)來(lái)說(shuō),如果最大字節(jié)數(shù)大于 31,則取 31,否則取真實(shí)的字節(jié)數(shù) cms_bits 我覺得應(yīng)該是不是 64 位虛擬機(jī)就占用 0 byte,是 64 位就占用 1byte epoch_bits 就是 epoch 所占用的字節(jié)大小,2 字節(jié)。
在上面的虛擬機(jī)對(duì)象頭分配表中,我們可以看到有幾種鎖的狀態(tài):無(wú)鎖(無(wú)狀態(tài)),偏向鎖,輕量級(jí)鎖,重量級(jí)鎖,其中輕量級(jí)鎖和偏向鎖是 JDK1.6 中對(duì) synchronized 鎖進(jìn)行優(yōu)化后新增加的,其目的就是為了大大優(yōu)化鎖的性能,所以在 JDK 1.6 中,使用 synchronized 的開銷也沒那么大了。其實(shí)從鎖有無(wú)鎖定來(lái)講,還是只有無(wú)鎖和重量級(jí)鎖,偏向鎖和輕量級(jí)鎖的出現(xiàn)就是增加了鎖的獲取性能而已,并沒有出現(xiàn)新的鎖。
所以我們的重點(diǎn)放在對(duì) synchronized 重量級(jí)鎖的研究上,當(dāng) monitor 被某個(gè)線程持有后,它就會(huì)處于鎖定狀態(tài)。在 HotSpot 虛擬機(jī)中,monitor 的底層代碼是由 ObjectMonitor 實(shí)現(xiàn)的,其主要數(shù)據(jù)結(jié)構(gòu)如下(位于 HotSpot 虛擬機(jī)源碼 ObjectMonitor.hpp 文件,C++ 實(shí)現(xiàn)的)

這段 C++ 中需要注意幾個(gè)屬性:_WaitSet 、 _EntryList 和 _Owner,每個(gè)等待獲取鎖的線程都會(huì)被封裝稱為 ObjectWaiter 對(duì)象。

_Owner 是指向了 ObjectMonitor 對(duì)象的線程,而 _WaitSet 和 _EntryList 就是用來(lái)保存每個(gè)線程的列表。
那么這兩個(gè)列表有什么區(qū)別呢?這個(gè)問題我和你聊一下鎖的獲取流程你就清楚了。
鎖的兩個(gè)列表
當(dāng)多個(gè)線程同時(shí)訪問某段同步代碼時(shí),首先會(huì)進(jìn)入 _EntryList 集合,當(dāng)線程獲取到對(duì)象的 monitor 之后,就會(huì)進(jìn)入 _Owner 區(qū)域,并把 ObjectMonitor 對(duì)象的 _Owner 指向?yàn)楫?dāng)前線程,并使 _count + 1,如果調(diào)用了釋放鎖(比如 wait)的操作,就會(huì)釋放當(dāng)前持有的 monitor ,owner = null, _count - 1,同時(shí)這個(gè)線程會(huì)進(jìn)入到 _WaitSet 列表中等待被喚醒。如果當(dāng)前線程執(zhí)行完畢后也會(huì)釋放 monitor 鎖,只不過(guò)此時(shí)不會(huì)進(jìn)入 _WaitSet 列表了,而是直接復(fù)位 _count 的值。

Klass Pointer 表示的是類型指針,也就是對(duì)象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過(guò)這個(gè)指針來(lái)確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例。
你可能不是很理解指針是個(gè)什么概念,你可以簡(jiǎn)單理解為指針就是指向某個(gè)數(shù)據(jù)的地址。

實(shí)例數(shù)據(jù) Instance Data
實(shí)例數(shù)據(jù)部分是對(duì)象真正存儲(chǔ)的有效信息,也是代碼中定義的各個(gè)字段的字節(jié)大小,比如一個(gè) byte 占 1 個(gè)字節(jié),一個(gè) int 占用 4 個(gè)字節(jié)。
對(duì)齊 Padding
對(duì)齊不是必須存在的,它只起到了占位符(%d, %c 等)的作用。這就是 JVM 的要求了,因?yàn)?HotSpot JVM 要求對(duì)象的起始地址必須是 8 字節(jié)的整數(shù)倍,也就是說(shuō)對(duì)象的字節(jié)大小是 8 的整數(shù)倍,不夠的需要使用 Padding 補(bǔ)全。
對(duì)象訪問定位的方式有哪些?
我們創(chuàng)建一個(gè)對(duì)象的目的當(dāng)然就是為了使用它,但是,一個(gè)對(duì)象被創(chuàng)建出來(lái)之后,在 JVM 中是如何訪問這個(gè)對(duì)象的呢?一般有兩種方式:通過(guò)句柄訪問和 通過(guò)直接指針訪問。
如果使用句柄訪問方式的話,Java 堆中可能會(huì)劃分出一塊內(nèi)存作為句柄池,引用(reference)中存儲(chǔ)的是對(duì)象的句柄地址,而句柄中包含了對(duì)象的實(shí)例數(shù)據(jù)與類型數(shù)據(jù)各自具體的地址信息。如下圖所示。

如果使用直接指針訪問的話,Java 堆中對(duì)象的內(nèi)存布局就會(huì)有所區(qū)別,棧區(qū)引用指示的是堆中的實(shí)例數(shù)據(jù)的地址,如果只是訪問對(duì)象本身的話,就不會(huì)多一次直接訪問的開銷,而對(duì)象類型數(shù)據(jù)的指針是存在于方法區(qū)中,如果定位的話,需要多一次直接定位開銷。如下圖所示

這兩種對(duì)象訪問方式各有各的優(yōu)勢(shì),使用句柄最大的好處就是引用中存儲(chǔ)的是句柄地址,對(duì)象移動(dòng)時(shí)只需改變句柄的地址就可以,而無(wú)需改變對(duì)象本身。
使用直接指針來(lái)訪問速度更快,它節(jié)省了一次指針定位的時(shí)間開銷,由于對(duì)象訪問在 Java 中非常頻繁,因?yàn)檫@類的開銷也是值得優(yōu)化的地方。
上面聊到了對(duì)象的兩種數(shù)據(jù),一種是對(duì)象的實(shí)例數(shù)據(jù),這沒什么好說(shuō)的,就是對(duì)象實(shí)例字段的數(shù)據(jù),一種是對(duì)象的類型數(shù)據(jù),這個(gè)數(shù)據(jù)說(shuō)的是對(duì)象的類型、父類、實(shí)現(xiàn)的接口和方法等。
如何判斷對(duì)象已經(jīng)死亡?
我們大家知道,基本上所有的對(duì)象都在堆中分布,當(dāng)我們不再使用對(duì)象的時(shí)候,垃圾收集器會(huì)對(duì)無(wú)用對(duì)象進(jìn)行回收??,那么 JVM 是如何判斷哪些對(duì)象已經(jīng)是"無(wú)用對(duì)象"的呢?
這里有兩種判斷方式,首先我們先來(lái)說(shuō)第一種:引用計(jì)數(shù)法。
引用計(jì)數(shù)法的判斷標(biāo)準(zhǔn)是這樣的:在對(duì)象中添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它時(shí),計(jì)數(shù)器的值就會(huì)加一;當(dāng)引用失效時(shí),計(jì)數(shù)器的值就會(huì)減一;只要任何時(shí)刻計(jì)數(shù)器為零的對(duì)象就是不會(huì)再被使用的對(duì)象。雖然這種判斷方式非常簡(jiǎn)單粗暴,但是往往很有用,不過(guò),在 Java 領(lǐng)域,主流的 Hotspot 虛擬機(jī)實(shí)現(xiàn)并沒有采用這種方式,因?yàn)橐糜?jì)數(shù)法不能解決對(duì)象之間的循環(huán)引用問題。
循環(huán)引用問題簡(jiǎn)單來(lái)講就是兩個(gè)對(duì)象之間互相依賴著對(duì)方,除此之外,再無(wú)其他引用,這樣虛擬機(jī)無(wú)法判斷引用是否為零從而進(jìn)行垃圾回收操作。
還有一種判斷對(duì)象無(wú)用的方法就是可達(dá)性分析算法。
當(dāng)前主流的 JVM 都采用了可達(dá)性分析算法來(lái)進(jìn)行判斷,這個(gè)算法的基本思路就是通過(guò)一系列被稱為GC Roots的根對(duì)象作為起始節(jié)點(diǎn)集,從這些節(jié)點(diǎn)開始,根據(jù)引用關(guān)系向下搜索,搜索過(guò)程走過(guò)的路徑被稱為引用鏈(Reference Chain),如果某個(gè)對(duì)象到 GC Roots 之間沒有任何引用鏈相連接,或者說(shuō)從 GC Roots 到這個(gè)對(duì)象不可達(dá)時(shí),則證明此這個(gè)對(duì)象是無(wú)用對(duì)象,需要被垃圾回收。
這種引用方式如下

如上圖所示,從枚舉根節(jié)點(diǎn) GC Roots 開始進(jìn)行遍歷,object 1 、2、3、4 是存在引用關(guān)系的對(duì)象,而 object 5、6、7 之間雖然有關(guān)聯(lián),但是它們到 GC Roots 之間是不可達(dá)的,所以被認(rèn)為是可以回收的對(duì)象。
在 Java 技術(shù)體系中,可以作為 GC Roots 進(jìn)行檢索的對(duì)象主要有
在虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象。
方法區(qū)中類靜態(tài)屬性引用的對(duì)象,比如 Java 類的引用類型靜態(tài)變量。
方法區(qū)中常量引用的對(duì)象,比如字符串常量池中的引用。
在本地方法棧中 JNI 引用的對(duì)象。
JVM 內(nèi)部的引用,比如基本數(shù)據(jù)類型對(duì)應(yīng)的 Class 對(duì)象,一些異常對(duì)象比如 NullPointerException、OutOfMemoryError 等,還有系統(tǒng)類加載器。
所有被 synchronized 持有的對(duì)象。
還有一些 JVM 內(nèi)部的比如 JMXBean、JVMTI 中注冊(cè)的回調(diào),本地代碼緩存等。
根據(jù)用戶所選的垃圾收集器以及當(dāng)前回收的內(nèi)存區(qū)域的不同,還可能會(huì)有一些對(duì)象臨時(shí)加入,共同構(gòu)成 GC Roots 集合。
雖然我們上面提到了兩種判斷對(duì)象回收的方法,但無(wú)論是引用計(jì)數(shù)法還是判斷 GC Roots 都離不開引用這一層關(guān)系。
這里涉及到到強(qiáng)引用、軟引用、弱引用、虛引用的引用關(guān)系,你可以閱讀作者的這一篇文章
如何判斷一個(gè)不再使用的類?
判斷一個(gè)類型屬于"不再使用的類"需要滿足下面這三個(gè)條件
這個(gè)類所有的實(shí)例已經(jīng)被回收,也就是 Java 堆中不存在該類及其任何這個(gè)類字累的實(shí)例 加載這個(gè)類的類加載器已經(jīng)被回收,但是類加載器一般很難會(huì)被回收,除非這個(gè)類加載器是為了這個(gè)目的設(shè)計(jì)的,比如 OSGI、JSP 的重加載等,否則通常很難達(dá)成。 這個(gè)類對(duì)應(yīng)的 Class 對(duì)象沒有任何地方被引用,無(wú)法在任何時(shí)刻通過(guò)反射訪問這個(gè)類的屬性和方法。
虛擬機(jī)允許對(duì)滿足上面這三個(gè)條件的無(wú)用類進(jìn)行回收操作。
持續(xù)更新 ...
完
往期推薦
??
如何評(píng)價(jià)《Java 并發(fā)編程藝術(shù)》這本書?
我真不想學(xué) happens - before 了!

