JAVA JVM面試題總結(jié)
JVM 的主要作用是什么?
JVM 就是 Java Virtual Machine(Java虛擬機(jī))的縮寫(xiě),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ī)加載的 類(lèi)信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。
- 堆:堆是線程共享的數(shù)據(jù)區(qū),堆是 JVM 中最大的一塊存儲(chǔ)區(qū)域,所有的對(duì)象實(shí)例都會(huì)分配在堆上。JDK 1.7后,字符串常量池從永久代中剝離出來(lái),存放在堆中。
堆空間的內(nèi)存分配(默認(rèn)情況下):
老年代 : 三分之二的堆空間
年輕代 : 三分之一的堆空間
eden 區(qū): 8/10 的年輕代空間
survivor 0 : 1/10 的年輕代空間
survivor 1 : 1/10 的年輕代空間
命令行上執(zhí)行如下命令,會(huì)查看默認(rèn)的 JVM 參數(shù)。
java -XX:+PrintFlagsFinal -version
輸出的內(nèi)容非常多,但是只有兩行能夠反映出上面的內(nèi)存分配結(jié)果
- 運(yùn)行時(shí)常量池:運(yùn)行時(shí)常量池又被稱(chēng)為 Runtime Constant Pool,這塊區(qū)域是方法區(qū)的一部分,它的名字非常有意思,通常被稱(chēng)為 非堆。它并不要求常量一定只有在編譯期才能產(chǎn)生,也就是并非編譯期間將常量放在常量池中,運(yùn)行期間也可以將新的常量放入常量池中,String 的 intern 方法就是一個(gè)典型的例子。
請(qǐng)你描述一下 Java 中的類(lèi)加載機(jī)制?
Java 虛擬機(jī)負(fù)責(zé)把描述類(lèi)的數(shù)據(jù)從 Class 文件加載到系統(tǒng)內(nèi)存中,并對(duì)類(lèi)的數(shù)據(jù)進(jìn)行校驗(yàn)、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機(jī)直接使用的 Java 類(lèi)型,這個(gè)過(guò)程被稱(chēng)之為 Java 的類(lèi)加載機(jī)制。
一個(gè)類(lèi)從被加載到虛擬機(jī)內(nèi)存開(kāi)始,到卸載出內(nèi)存為止,一共會(huì)經(jīng)歷下面這些過(guò)程。
類(lèi)加載機(jī)制一共有五個(gè)步驟,分別是加載、鏈接、初始化、使用和卸載階段,這五個(gè)階段的順序是確定的。
其中鏈接階段會(huì)細(xì)分成三個(gè)階段,分別是驗(yàn)證、準(zhǔn)備、解析階段,這三個(gè)階段的順序是不確定的,這三個(gè)階段通常交互進(jìn)行。解析階段通常會(huì)在初始化之后再開(kāi)始,這是為了支持 Java 語(yǔ)言的運(yùn)行時(shí)綁定特性(也被稱(chēng)為動(dòng)態(tài)綁定
)。
下面我們就來(lái)聊一下這幾個(gè)過(guò)程。
加載
關(guān)于什么時(shí)候開(kāi)始加載這個(gè)過(guò)程,《Java 虛擬機(jī)規(guī)范》并沒(méi)有強(qiáng)制約束,所以這一點(diǎn)我們可以自由實(shí)現(xiàn)。加載是整個(gè)類(lèi)加載過(guò)程的第一個(gè)階段,在這個(gè)階段,Java 虛擬機(jī)需要完成三件事情:
- 通過(guò)一個(gè)類(lèi)的全限定名來(lái)獲取定義此類(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)的訪問(wèn)入口。
《Java 虛擬機(jī)規(guī)范》并未規(guī)定全限定名是如何獲取的,所以現(xiàn)在業(yè)界有很多獲取全限定名的方式:
- 從 ZIP 包中讀取,最終會(huì)改變?yōu)?JAR、EAR、WAR 格式。
- 從網(wǎng)絡(luò)中獲取,最常見(jiàn)的應(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)完成,也可以使用用戶(hù)自定義的類(lèi)加載器來(lái)完成。程序員可以通過(guò)自己定義類(lèi)加載器來(lái)控制字節(jié)流的訪問(wèn)方式。
數(shù)組的加載不需要通過(guò)類(lèi)加載器來(lái)創(chuàng)建,它是直接在內(nèi)存中分配,但是數(shù)組的元素類(lèi)型(數(shù)組去掉所有維度的類(lèi)型)最終還是要靠類(lèi)加載器來(lái)完成加載。
驗(yàn)證
加載過(guò)后的下一個(gè)階段就是驗(yàn)證,因?yàn)槲覀兩弦徊街v到在內(nèi)存中生成了一個(gè) Class 對(duì)象,這個(gè)對(duì)象是訪問(wèn)其代表數(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
開(kāi)頭。 - 主、次版本號(hào)是否在當(dāng)前 Java 虛擬機(jī)接受范圍之內(nèi)。
- 常亮池的常量中是否有不支持的常量類(lèi)型。
- 指向常量的各種索引值中是否有指向不存在的常量或不符合類(lè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)證的類(lèi)是否有父類(lèi)(除了 Object 類(lèi)之外,所有的類(lèi)都應(yīng)該有父類(lèi))。
- 要驗(yàn)證類(lèi)的父類(lèi)是否繼承了不允許繼承的類(lèi)。
- 如果這個(gè)類(lèi)不是抽象類(lèi),那么這個(gè)類(lèi)是否實(shí)現(xiàn)了父類(lèi)或者接口中要求的所有方法。
- 是否覆蓋了 final 字段,是否出現(xiàn)了不符合規(guī)定的重載等。
需要記住這一階段只是對(duì)《Java 語(yǔ)言規(guī)范》的驗(yàn)證。
字節(jié)碼驗(yàn)證
字節(jié)碼驗(yàn)證階段是最復(fù)雜的一個(gè)階段,這個(gè)階段主要是確定程序語(yǔ)意是否合法、是否是符合邏輯的。這個(gè)階段主要是對(duì)類(lèi)的方法體(Class 文件中的 Code 屬性)進(jìn)行校驗(yàn)分析。這部分驗(yàn)證包括
- 確保操作數(shù)棧的數(shù)據(jù)類(lèi)型和實(shí)際執(zhí)行時(shí)的數(shù)據(jù)類(lèi)型是否一致。
- 保證任何跳轉(zhuǎn)指令不會(huì)跳出到方法體外的字節(jié)碼指令上。
- 保證方法體中的類(lèi)型轉(zhuǎn)換是有效的,例如可以把一個(gè)子類(lèi)對(duì)象賦值給父類(lèi)數(shù)據(jù)類(lèi)型,但是不能把父類(lèi)數(shù)據(jù)類(lèi)型賦值給子類(lèi)等諸如此不安全的類(lèi)型轉(zhuǎn)換。
- 其他驗(yàn)證。
如果沒(méi)有通過(guò)字節(jié)碼驗(yàn)證,就說(shuō)明驗(yàn)證出問(wè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ì)類(lèi)自身以外的各類(lèi)信息進(jìn)行匹配性校驗(yàn),這個(gè)驗(yàn)證主要包括
- 符號(hào)引用中的字符串全限定名是否能找到對(duì)應(yīng)的類(lèi)。
- 指定類(lèi)中是否存在符合方法的字段描述符以及簡(jiǎn)單名稱(chēng)所描述的方法和字段。
- 符號(hào)引用的類(lèi)、字段方法的可訪問(wèn)性是否可被當(dāng)前類(lèi)所訪問(wèn)。
- 其他驗(yàn)證。
這一階段主要是確保解析行為能否正常執(zhí)行,如果無(wú)法通過(guò)符號(hào)引用驗(yàn)證,就會(huì)出現(xiàn)類(lèi)似 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)備階段是為類(lèi)中的變量分配內(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 堆中。
下面通常情況下的基本類(lèi)型和引用類(lèi)型的初始值
除了"通常情況"下,還有一些"例外情況",如果類(lèi)字段屬性中存在 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 類(lèi)都會(huì)被編譯成一個(gè) class 文件,但在編譯的時(shí)候虛擬機(jī)并不知道所引用類(lèi)的地址,所以就用符號(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è)步驟
類(lèi)或接口的解析
字段解析
方法解析
接口方法解析
初始化
初始化是類(lèi)加載過(guò)程的最后一個(gè)步驟,在之前的階段中,都是由 Java 虛擬機(jī)占主導(dǎo)作用,但是到了這一步,卻把主動(dòng)權(quán)移交給應(yīng)用程序。
對(duì)于初始化階段,《Java 虛擬機(jī)規(guī)范》嚴(yán)格規(guī)定了只有下面這六種情況下才會(huì)觸發(fā)類(lèi)的初始化。
- 在遇到 new、getstatic、putstatic 或者 invokestatic 這四條字節(jié)碼指令時(shí),如果沒(méi)有進(jìn)行過(guò)初始化,那么首先觸發(fā)初始化。通過(guò)這四個(gè)字節(jié)碼的名稱(chēng)可以判斷,這四條字節(jié)碼其實(shí)就兩個(gè)場(chǎng)景,調(diào)用 new 關(guān)鍵字的時(shí)候進(jìn)行初始化、讀取或者設(shè)置一個(gè)靜態(tài)字段的時(shí)候、調(diào)用靜態(tài)方法的時(shí)候。
- 在初始化類(lèi)的時(shí)候,如果父類(lèi)還沒(méi)有初始化,那么就需要先對(duì)父類(lèi)進(jìn)行初始化。
- 在使用 java.lang.reflect 包的方法進(jìn)行反射調(diào)用的時(shí)候。
- 當(dāng)虛擬機(jī)啟動(dòng)時(shí),用戶(hù)需要指定執(zhí)行主類(lèi)的時(shí)候,說(shuō)白了就是虛擬機(jī)會(huì)先初始化 main 方法這個(gè)類(lèi)。
- 在使用 JDK 7 新加入的動(dòng)態(tài)語(yǔ)言支持時(shí),如果一個(gè) jafva.lang.invoke.MethodHandle 實(shí)例最后的解析結(jié)果為 REF_getstatic、REF_putstatic、REF_invokeStatic、REF_newInvokeSpecial 四種類(lèi)型的方法句柄,并且這個(gè)方法句柄對(duì)應(yīng)的類(lèi)沒(méi)有進(jìn)行過(guò)初始化,需要先對(duì)其進(jìn)行初始化。
- 當(dāng)一個(gè)接口中定義了 JDK 8 新加入的默認(rèn)方法(被 default 關(guān)鍵字修飾的接口方法)時(shí),如果有這個(gè)借口的實(shí)現(xiàn)類(lèi)發(fā)生了初始化,那該接口要在其之前被初始化。
其實(shí)上面只有前四個(gè)大家需要知道就好了,后面兩個(gè)比較冷門(mén)。
如果說(shuō)要答類(lèi)加載的話,其實(shí)聊到這里已經(jīng)可以了,但是為了完整性,我們索性把后面兩個(gè)過(guò)程也來(lái)聊一聊。
使用
這個(gè)階段沒(méi)什么可說(shuō)的,就是初始化之后的代碼由 JVM 來(lái)動(dòng)態(tài)調(diào)用執(zhí)行。
卸載
當(dāng)代表一個(gè)類(lèi)的 Class 對(duì)象不再被引用,那么 Class 對(duì)象的生命周期就結(jié)束了,對(duì)應(yīng)的在方法區(qū)中的數(shù)據(jù)也會(huì)被卸載。
⚠️但是需要注意一點(diǎn):JVM 自帶的類(lèi)加載器裝載的類(lèi),是不會(huì)卸載的,由用戶(hù)自定義的類(lèi)加載器加載的類(lèi)是可以卸載的。
在 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è)類(lèi)的符號(hào)引用,并且檢查這個(gè)符號(hào)引用所代表的類(lèi)是否已經(jīng)被加載、解析和初始化。
因?yàn)榇藭r(shí)很可能不知道具體的類(lèi)是什么,所以這里使用的是符號(hào)引用。
如果發(fā)現(xiàn)這個(gè)類(lèi)沒(méi)有經(jīng)過(guò)上面類(lèi)加載的過(guò)程,那么就執(zhí)行相應(yīng)的類(lèi)加載過(guò)程。
類(lèi)檢查完成后,接下來(lái)虛擬機(jī)將會(huì)為新生對(duì)象分配內(nèi)存,對(duì)象所需的大小在類(lèi)加載完成后便可確定(我會(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è)類(lèi)的實(shí)例、對(duì)象的 hashcode、對(duì)象的 gc 分代年齡信息。這些信息存放在對(duì)象的對(duì)象頭(Object Header)中。
如果上面的工作都做完后,從虛擬機(jī)的角度來(lái)說(shuō),一個(gè)新的對(duì)象就創(chuàng)建完畢了;但是對(duì)于程序員來(lái)說(shuō),對(duì)象創(chuàng)建才剛剛開(kāi)始,因?yàn)闃?gòu)造函數(shù),即 Class 文件中的 <init>()
方法還沒(méi)有執(zhí)行,所有字段都為默認(rèn)的零值。new 指令之后才會(huì)執(zhí)行 <init>()
方法,然后按照程序員的意愿對(duì)對(duì)象進(jìn)行初始化,這樣一個(gè)對(duì)象才可能被完整的構(gòu)造出來(lái)。
內(nèi)存分配方式有哪些呢?
在類(lèi)加載完成后,虛擬機(jī)需要為新生對(duì)象分配內(nèi)存,為對(duì)象分配內(nèi)存相當(dāng)于是把一塊確定的區(qū)域從堆中劃分出來(lái),這就涉及到一個(gè)問(wèn)題,要?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ò)在一起,這種情況下就沒(méi)有辦法使用指針碰撞,這里就要使用另外一種記錄內(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ì)象頭開(kāi)辟 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ì),還是開(kāi)辟 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í)鎖中直接開(kāi)辟 30 bit 的空間存放指向棧中鎖記錄的指針,2bit 存放鎖的標(biāo)志位,其標(biāo)志位為 00。
- 重量級(jí)鎖中和輕量級(jí)鎖一樣,30 bit 的空間用來(lái)存放指向重量級(jí)鎖的指針,2 bit 存放鎖的標(biāo)識(shí)位,為 11。
- GC標(biāo)記開(kāi)辟 30 bit 的內(nèi)存空間卻沒(mé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)中的枚舉窺出端倪
來(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 我覺(jué)得應(yīng)該是不是 64 位虛擬機(jī)就占用 0 byte,是 64 位就占用 1byteepoch_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 的開(kāi)銷(xiāo)也沒(méi)那么大了。其實(shí)從鎖有無(wú)鎖定來(lái)講,還是只有無(wú)鎖和重量級(jí)鎖,偏向鎖和輕量級(jí)鎖的出現(xiàn)就是增加了鎖的獲取性能而已,并沒(méi)有出現(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ì)被封裝稱(chēng)為 ObjectWaiter
對(duì)象。
_Owner 是指向了 ObjectMonitor 對(duì)象的線程,而 _WaitSet 和 _EntryList 就是用來(lái)保存每個(gè)線程的列表。
那么這兩個(gè)列表有什么區(qū)別呢?這個(gè)問(wèn)題我和你聊一下鎖的獲取流程你就清楚了。
鎖的兩個(gè)列表
當(dāng)多個(gè)線程同時(shí)訪問(wèn)某段同步代碼時(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 表示的是類(lèi)型指針,也就是對(duì)象指向它的類(lèi)元數(shù)據(jù)的指針,虛擬機(jī)通過(guò)這個(gè)指針來(lái)確定這個(gè)對(duì)象是哪個(gè)類(lèi)的實(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ì)象訪問(wèn)定位的方式有哪些?
我們創(chuàng)建一個(gè)對(duì)象的目的當(dāng)然就是為了使用它,但是,一個(gè)對(duì)象被創(chuàng)建出來(lái)之后,在 JVM 中是如何訪問(wèn)這個(gè)對(duì)象的呢?一般有兩種方式:通過(guò)句柄訪問(wèn)和 通過(guò)直接指針訪問(wèn)。
- 如果使用句柄訪問(wèn)方式的話,Java 堆中可能會(huì)劃分出一塊內(nèi)存作為句柄池,引用(reference)中存儲(chǔ)的是對(duì)象的句柄地址,而句柄中包含了對(duì)象的實(shí)例數(shù)據(jù)與類(lèi)型數(shù)據(jù)各自具體的地址信息。如下圖所示。
- 如果使用直接指針訪問(wèn)的話,Java 堆中對(duì)象的內(nèi)存布局就會(huì)有所區(qū)別,棧區(qū)引用指示的是堆中的實(shí)例數(shù)據(jù)的地址,如果只是訪問(wèn)對(duì)象本身的話,就不會(huì)多一次直接訪問(wèn)的開(kāi)銷(xiāo),而對(duì)象類(lèi)型數(shù)據(jù)的指針是存在于方法區(qū)中,如果定位的話,需要多一次直接定位開(kāi)銷(xiāo)。如下圖所示
這兩種對(duì)象訪問(wèn)方式各有各的優(yōu)勢(shì),使用句柄最大的好處就是引用中存儲(chǔ)的是句柄地址,對(duì)象移動(dòng)時(shí)只需改變句柄的地址就可以,而無(wú)需改變對(duì)象本身。
使用直接指針來(lái)訪問(wèn)速度更快,它節(jié)省了一次指針定位的時(shí)間開(kāi)銷(xiāo),由于對(duì)象訪問(wèn)在 Java 中非常頻繁,因?yàn)檫@類(lèi)的開(kāi)銷(xiāo)也是值得優(yōu)化的地方。
上面聊到了對(duì)象的兩種數(shù)據(jù),一種是對(duì)象的實(shí)例數(shù)據(jù),這沒(méi)什么好說(shuō)的,就是對(duì)象實(shí)例字段的數(shù)據(jù),一種是對(duì)象的類(lèi)型數(shù)據(jù),這個(gè)數(shù)據(jù)說(shuō)的是對(duì)象的類(lèi)型、父類(lèi)、實(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)并沒(méi)有采用這種方式,因?yàn)橐糜?jì)數(shù)法不能解決對(duì)象之間的循環(huán)引用問(wèn)題。
循環(huán)引用問(wèn)題簡(jiǎn)單來(lái)講就是兩個(gè)對(duì)象之間互相依賴(lài)著對(duì)方,除此之外,再無(wú)其他引用,這樣虛擬機(jī)無(wú)法判斷引用是否為零從而進(jìn)行垃圾回收操作。
還有一種判斷對(duì)象無(wú)用的方法就是可達(dá)性分析算法。
當(dāng)前主流的 JVM 都采用了可達(dá)性分析算法來(lái)進(jìn)行判斷,這個(gè)算法的基本思路就是通過(guò)一系列被稱(chēng)為GC Roots
的根對(duì)象作為起始節(jié)點(diǎn)集,從這些節(jié)點(diǎn)開(kāi)始,根據(jù)引用關(guān)系向下搜索,搜索過(guò)程走過(guò)的路徑被稱(chēng)為引用鏈
(Reference Chain),如果某個(gè)對(duì)象到 GC Roots 之間沒(méi)有任何引用鏈相連接,或者說(shuō)從 GC Roots 到這個(gè)對(duì)象不可達(dá)時(shí),則證明此這個(gè)對(duì)象是無(wú)用對(duì)象,需要被垃圾回收。
這種引用方式如下
如上圖所示,從枚舉根節(jié)點(diǎn) GC Roots 開(kāi)始進(jìn)行遍歷,object 1 、2、3、4 是存在引用關(guān)系的對(duì)象,而 object 5、6、7 之間雖然有關(guān)聯(lián),但是它們到 GC Roots 之間是不可大的,所以被認(rèn)為是可以回收的對(duì)象。
- 在 Java 技術(shù)體系中,可以作為 GC Roots 進(jìn)行檢索的對(duì)象主要有
- 在虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象。
- 方法區(qū)中類(lèi)靜態(tài)屬性引用的對(duì)象,比如 Java 類(lèi)的引用類(lèi)型靜態(tài)變量。
- 方法區(qū)中常量引用的對(duì)象,比如字符串常量池中的引用。
- 在本地方法棧中 JNI 引用的對(duì)象。
- JVM 內(nèi)部的引用,比如基本數(shù)據(jù)類(lèi)型對(duì)應(yīng)的 Class 對(duì)象,一些異常對(duì)象比如 NullPointerException、OutOfMemoryError 等,還有系統(tǒng)類(lèi)加載器。
- 所有被 synchronized 持有的對(duì)象。
- 還有一些 JVM 內(nèi)部的比如 JMXBean、JVMTI 中注冊(cè)的回調(diào),本地代碼緩存等。
- 根據(jù)用戶(hù)所選的垃圾收集器以及當(dāng)前回收的內(nèi)存區(qū)域的不同,還可能會(huì)有一些對(duì)象臨時(shí)加入,共同構(gòu)成 GC Roots 集合。
雖然我們上面提到了兩種判斷對(duì)象回收的方法,但無(wú)論是引用計(jì)數(shù)法還是判斷 GC Roots 都離不開(kāi)引用
這一層關(guān)系。
這里涉及到到強(qiáng)引用、軟引用、弱引用、虛引用的引用關(guān)系,你可以閱讀作者的這一篇文章
如何判斷一個(gè)不再使用的類(lèi)?
判斷一個(gè)類(lèi)型屬于"不再使用的類(lèi)"需要滿(mǎn)足下面這三個(gè)條件
- 這個(gè)類(lèi)所有的實(shí)例已經(jīng)被回收,也就是 Java 堆中不存在該類(lèi)及其任何這個(gè)類(lèi)字累的實(shí)例
- 加載這個(gè)類(lèi)的類(lèi)加載器已經(jīng)被回收,但是類(lèi)加載器一般很難會(huì)被回收,除非這個(gè)類(lèi)加載器是為了這個(gè)目的設(shè)計(jì)的,比如 OSGI、JSP 的重加載等,否則通常很難達(dá)成。
- 這個(gè)類(lèi)對(duì)應(yīng)的 Class 對(duì)象沒(méi)有任何地方被引用,無(wú)法在任何時(shí)刻通過(guò)反射訪問(wèn)這個(gè)類(lèi)的屬性和方法。
虛擬機(jī)允許對(duì)滿(mǎn)足上面這三個(gè)條件的無(wú)用類(lèi)進(jìn)行回收操作。
到此這篇關(guān)于JAVA JVM面試題總結(jié)的文章就介紹到這了,更多相關(guān)JAVA JVM面試題內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot+Thymeleaf+ECharts實(shí)現(xiàn)大數(shù)據(jù)可視化(基礎(chǔ)篇)
本文主要介紹了SpringBoot+Thymeleaf+ECharts實(shí)現(xiàn)大數(shù)據(jù)可視化,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧<BR>2022-06-06詳解Java如何實(shí)現(xiàn)多線程步調(diào)一致
本章節(jié)主要講解另外兩個(gè)線程同步器:CountDownLatch和CyclicBarrier的用法,使用場(chǎng)景以及實(shí)現(xiàn)原理,感興趣的小伙伴可以了解一下2023-07-07三步輕松實(shí)現(xiàn)Java的SM2前端加密后端解密
SM2算法和RSA算法都是公鑰密碼算法,SM2算法是一種更先進(jìn)安全的算法,在我們國(guó)家商用密碼體系中被用來(lái)替換RSA算法,這篇文章主要給大家介紹了關(guān)于如何通過(guò)三步輕松實(shí)現(xiàn)Java的SM2前端加密后端解密的相關(guān)資料,需要的朋友可以參考下2024-01-01Java實(shí)現(xiàn)文件檢索系統(tǒng)的示例代碼
這篇文章主要為大家詳細(xì)介紹了如何劉Java語(yǔ)言實(shí)現(xiàn)簡(jiǎn)易的文件檢索系統(tǒng),文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)Java開(kāi)發(fā)有一定的幫助,需要的可以參考一下2022-07-07從源碼角度簡(jiǎn)單看StringBuilder和StringBuffer的異同(全面解析)
下面小編就為大家分享一篇從源碼角度簡(jiǎn)單看StringBuilder和StringBuffer的異同(全面解析),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2017-12-12