開(kāi)篇問(wèn)題:
-
一句話(huà)描述類(lèi)加載過(guò)程?
類(lèi)加載過(guò)程實(shí)際是將Java文件編譯為class文件并裝載到JVM中最終解析為01機(jī)器代碼供服務(wù)器進(jìn)行的過(guò)程,涉及到的過(guò)程包括:編譯、裝載、鏈接、初始化、使用、卸載6個(gè)過(guò)程,其中各個(gè)過(guò)程的作用分別是:
- 編譯:通過(guò)javac命令將Java文件轉(zhuǎn)換成class文件。
- 裝載:查找并加載class文件。
- 鏈接:包括:驗(yàn)證、準(zhǔn)備、解析。
- 驗(yàn)證:通過(guò)對(duì)二進(jìn)制流的內(nèi)容進(jìn)行校驗(yàn)來(lái)檢查是否符合JVM的要求規(guī)范以及是否會(huì)對(duì)程序運(yùn)行時(shí)是否會(huì)對(duì)JVM造成危害。其中包括:文件格式驗(yàn)證 --> 元數(shù)據(jù)驗(yàn)證 --> 字節(jié)碼驗(yàn)證 --> 符號(hào)引用驗(yàn)證。
- 準(zhǔn)備:在方法區(qū)中為類(lèi)變量(靜態(tài)變量)分配內(nèi)存并設(shè)置系統(tǒng)默認(rèn)初始值。
- 解析:將方法區(qū)中的符號(hào)引用轉(zhuǎn)變成直接或引用,并對(duì)解析結(jié)構(gòu)進(jìn)行緩存。
- 初始化:調(diào)用構(gòu)造方法對(duì)類(lèi)變量賦予程序中設(shè)置的值。
- 使用:
- 卸載:類(lèi)卸載的三個(gè)條件必須都滿(mǎn)足才能進(jìn)行卸載,一般情況下
詳細(xì)描述對(duì)象的內(nèi)存布局每一個(gè)部分干了什么,用到了什么技術(shù)?
對(duì)象內(nèi)存布局包括:對(duì)象頭、實(shí)例數(shù)據(jù)、對(duì)其填充三部分。其中
對(duì)象頭包含:Mark Word、Class Pointer、Length。
- Mark Word:一系列標(biāo)志位,包括:哈希碼、分代年齡、鎖狀態(tài)標(biāo)志等,其中哈希碼用到了大端存儲(chǔ)技術(shù),便于數(shù)據(jù)類(lèi)型的符號(hào)判斷)
- Class Pointer:指向?qū)ο髮?duì)應(yīng)類(lèi)的內(nèi)存地址,其中引用定位到對(duì)象的方式包括:句柄池訪問(wèn)、直接訪問(wèn)。
- 句柄池訪問(wèn):使用句柄訪問(wèn)對(duì)象,會(huì)在堆中開(kāi)辟一塊內(nèi)存作為句柄池,句柄中儲(chǔ)存了對(duì)象實(shí)例數(shù)據(jù) 的內(nèi)存地址,訪問(wèn)類(lèi)型數(shù)據(jù)的內(nèi)存地址,優(yōu)點(diǎn):reference存儲(chǔ)的是穩(wěn)定的句柄地址,在對(duì)象被移動(dòng)時(shí)只會(huì)改變句柄中的實(shí)例數(shù)據(jù)指針,而reference本身不需要改變;缺點(diǎn):增加了一次指針定位的時(shí)間開(kāi)銷(xiāo)。
- 直接訪問(wèn):指reference中直接儲(chǔ)存對(duì)象在heap中的內(nèi)存地址,但對(duì)應(yīng)的類(lèi)型數(shù)據(jù)訪問(wèn)地址需要 在實(shí)例中存儲(chǔ)。優(yōu)點(diǎn):節(jié)省了一次指針定位的開(kāi)銷(xiāo);缺點(diǎn):在對(duì)象被移動(dòng)時(shí),reference本身需要被修改。
- Length:數(shù)據(jù)對(duì)象特有,用于記錄數(shù)組長(zhǎng)度。
實(shí)例數(shù)據(jù)包含:包含了對(duì)象的所有成員變量,大小由變量本身的類(lèi)型決定,用到的技術(shù)-- 指針壓縮技術(shù)作用包括:減少GC次數(shù),提供CPU的OOP緩存。
對(duì)其填充的作用:為了保證對(duì)象的大小為8字節(jié)的整數(shù)倍,對(duì)其填充技術(shù)的作用是提高CPU訪問(wèn)數(shù)據(jù)的效率。
運(yùn)行時(shí)數(shù)據(jù)區(qū)

通過(guò)CPU與主存的關(guān)系可以推斷出JVM是如何跟服務(wù)器的CPU和內(nèi)存進(jìn)行交互的 – java是多線程機(jī)制,當(dāng)多個(gè)任務(wù)執(zhí)行(類(lèi)比我們的CPU運(yùn)算核心)對(duì)同一塊內(nèi)存(類(lèi)比:主存)數(shù)據(jù)進(jìn)行操作時(shí)(PS:線程共享)必然會(huì)發(fā)生數(shù)據(jù)不一致的情況,這個(gè)時(shí)候需要有一塊區(qū)域或者一種操作(類(lèi)比:協(xié)議)保證數(shù)據(jù)的一致性。而每個(gè)線程又有自己?jiǎn)为?dú)的工作內(nèi)存(類(lèi)比高速緩沖區(qū)),當(dāng)我們線程進(jìn)行運(yùn)作時(shí),數(shù)據(jù)肯定會(huì)從JVM主存拷貝到線程自己的工作內(nèi)存,然后再進(jìn)行操作。

方法區(qū)
方法區(qū)是被線程共享的Non-Heap(非堆) 內(nèi)存區(qū)域,用于存儲(chǔ)已被虛擬機(jī)加載的類(lèi)信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù),在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。
注意:
JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)是一種規(guī)范,真正的實(shí)現(xiàn)
方法區(qū)在JDK 8中就是Metaspace,在JDK6或7中就是Perm Space
堆
堆是Java虛擬機(jī)所管理內(nèi)存中最大的一塊,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建,被所有線程共享。其中 Java對(duì)象實(shí)例以及數(shù)組都在堆上分配。
虛擬機(jī)棧
問(wèn)題:那一個(gè)線程執(zhí)行的狀態(tài)如何維護(hù)?一個(gè)線程可以執(zhí)行多 少個(gè)方法?這樣的關(guān)系怎么維護(hù)呢?
- ?虛擬機(jī)棧是一個(gè)線程執(zhí)行的區(qū)域,保存著一個(gè)線程中方法的調(diào)用狀態(tài)。換句話(huà)說(shuō),一個(gè)Java線程的運(yùn)行狀態(tài),由一個(gè)虛擬機(jī)棧來(lái)保存,所以虛擬機(jī)??隙ㄊ蔷€程私有的,獨(dú)有的,隨著線程的創(chuàng)建而創(chuàng)建。
- 每一個(gè)被線程執(zhí)行的方法,為該棧中的棧幀,即每個(gè)方法對(duì)應(yīng)一個(gè)棧幀。 調(diào)用一個(gè)方法,就會(huì)向棧中壓入一個(gè)棧幀;一個(gè)方法調(diào)用完成,就會(huì)把該棧幀從棧中彈出。
棧幀:
棧幀:每個(gè)棧幀對(duì)應(yīng)一個(gè)被調(diào)用的方法,可以理解為一個(gè)方法的運(yùn)行空間。
棧幀中包括局部變量 表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法返回地址和即時(shí)信息。
?局部變量 表:方法中定義的局部變量以及方法的參數(shù)存放在這張表中, 局部變量表中的變量不可直接使用,如需要使用的話(huà),必須通過(guò)相關(guān)指令將其加載至操作數(shù)棧中作為操作數(shù)使用。
操作數(shù) 棧:以壓棧和出棧的方式存儲(chǔ)操作數(shù)的。如 1+1 兩個(gè)1存儲(chǔ)在局部變量 表中, 1+1這個(gè)操作在操作數(shù)棧中完成。
動(dòng)態(tài)鏈接:每個(gè)棧幀都包含一個(gè)指向運(yùn)行時(shí)常量池中該棧幀所屬方法的引用,持有這個(gè)引用是為了支持方法調(diào)用過(guò)程中的動(dòng)態(tài)連接(符號(hào)引用編程直接引用)。因?yàn)?/strong>類(lèi)加載機(jī)制 僅僅是將本文件的符號(hào)引用變成直接引用 ,當(dāng)遇到多態(tài)的調(diào)用時(shí),只能通過(guò)運(yùn)行來(lái)確定子類(lèi)對(duì)接的引用是哪一個(gè)。
方法返回地址:當(dāng)一個(gè)方法開(kāi)始執(zhí)行后,只有兩種方式可以退出,一種是遇到方法返回的字節(jié)碼指令;一種是遇 見(jiàn)異常,并且這個(gè)異常沒(méi)有在方法體內(nèi)得到處理。
本地方法棧
當(dāng)前線程執(zhí)行的方法是Native類(lèi)型的,這些方法就會(huì)在本地方法棧中執(zhí)行
思考:在Java方法執(zhí)行的時(shí)候如何調(diào)用native的方法呢?
通過(guò)動(dòng)態(tài)鏈接來(lái)進(jìn)行調(diào)用。
動(dòng)態(tài)鏈接.png
程序計(jì)數(shù)器
作用:是記錄當(dāng)前線程的執(zhí)行位置,線程私有。
如果線程正在執(zhí)行Java方法,則計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址;
如果正在執(zhí)行的是Native方法,則這個(gè)計(jì)數(shù)器為空。
除了上面五塊內(nèi)存之外,其實(shí)我們的JVM還會(huì)使用到其他兩塊內(nèi)存
直接內(nèi)存(Direct Memory)
并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是JVM規(guī)范中定義的內(nèi)存區(qū)域,但是這部分內(nèi)存也被頻 繁地使用,而且也可能導(dǎo)致OutOfMemoryError 異常出現(xiàn),所以我們放到這里一起講解。在JDK 1.4 中新加入了NIO(New Input/Output)類(lèi),引入了一種基于通道(Channel)與緩沖區(qū) (Buffer)的I/O 方式,它可以使用Native 函數(shù)庫(kù)直接分配堆外內(nèi)存,然后通過(guò)一個(gè)存儲(chǔ)在Java 堆 里面的DirectByteBuffer 對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作。這樣能在一些場(chǎng)景中顯著提高性能, 因?yàn)楸苊饬嗽贘ava 堆和Native 堆中來(lái)回復(fù)制數(shù)據(jù)。
本機(jī)直接內(nèi)存的分配不會(huì)受到Java 堆大小的限制,但是,既然是內(nèi)存,則肯定還是會(huì)受到本機(jī)總 內(nèi)存的大小及處理器尋址空間的限制。因此在分配JVM空間的時(shí)候應(yīng)該考慮直接內(nèi)存所帶來(lái)的影 響,特別是應(yīng)用到NIO的場(chǎng)景。
其他內(nèi)存:
Code Cache:**JVM本身是個(gè)本地程序,還需要其他的內(nèi)存去完成各種基本任務(wù),比如,JIT 編譯器在運(yùn)行時(shí)對(duì)熱點(diǎn)方法進(jìn)行編譯,就會(huì)將編譯后的方法儲(chǔ)存在Code Cache里面;GC等 功能。需要運(yùn)行在本地線程之中,類(lèi)似部分都需要占用內(nèi)存空間。這些是實(shí)現(xiàn)JVM JIT等功能 的需要,但規(guī)范中并不涉及
其中??梢灾赶蚨?case: Object obj=new Object()
方法區(qū)指向堆 case: private static Object obj=new Object();
堆指向方法區(qū) case: Class類(lèi)對(duì)象指向它的元數(shù)據(jù)。 new Object().getClass();
Java對(duì)象內(nèi)存模型
一個(gè)Java對(duì)象在內(nèi)存中包括3個(gè)部分:對(duì)象頭、實(shí)例數(shù)據(jù)和對(duì)齊填充。
Java對(duì)象內(nèi)存布局.png
內(nèi)存模型設(shè)計(jì)之–大小端存儲(chǔ)
小端存儲(chǔ):便于數(shù)據(jù)之間的類(lèi)型轉(zhuǎn)換,例如:long類(lèi)型轉(zhuǎn)換為int類(lèi)型時(shí),高地址部分的數(shù)據(jù)可以 直接截掉。
大端存儲(chǔ):便于數(shù)據(jù)類(lèi)型的符號(hào)判斷,因?yàn)樽畹偷刂肺粩?shù)據(jù)即為符號(hào)位,可以直接判斷數(shù)據(jù)的正 負(fù)號(hào)。
內(nèi)存模型設(shè)計(jì)之–Class Pointer
引用定位到對(duì)象的方式有兩種,一種叫句柄池訪問(wèn),一種叫直接訪問(wèn)
句柄池訪問(wèn)對(duì)象.png

區(qū)別:
句柄池:
使用句柄訪問(wèn)對(duì)象,會(huì)在堆中開(kāi)辟一塊內(nèi)存作為句柄池,句柄中儲(chǔ)存了對(duì)象實(shí)例數(shù)據(jù)(屬性值結(jié)構(gòu)體) 的內(nèi)存地址,訪問(wèn)類(lèi)型數(shù)據(jù)的內(nèi)存地址(類(lèi)信息,方法類(lèi)型信息),對(duì)象實(shí)例數(shù)據(jù)一般也在heap中開(kāi) 辟,類(lèi)型數(shù)據(jù)一般儲(chǔ)存在方法區(qū)中。
優(yōu)點(diǎn):reference存儲(chǔ)的是穩(wěn)定的句柄地址,在對(duì)象被移動(dòng)(垃圾收集時(shí)移動(dòng)對(duì)象是非常普遍的行為) 時(shí)只會(huì)改變句柄中的實(shí)例數(shù)據(jù)指針,而reference本身不需要改變。
缺點(diǎn):增加了一次指針定位的時(shí)間開(kāi)銷(xiāo)。 直接訪問(wèn):
直接指針訪問(wèn)方式指reference中直接儲(chǔ)存對(duì)象在heap中的內(nèi)存地址,但對(duì)應(yīng)的類(lèi)型數(shù)據(jù)訪問(wèn)地址需要 在實(shí)例中存儲(chǔ)。
優(yōu)點(diǎn):節(jié)省了一次指針定位的開(kāi)銷(xiāo)。 缺點(diǎn):在對(duì)象被移動(dòng)時(shí)(如進(jìn)行GC后的內(nèi)存重新排列),reference本身需要被修改
內(nèi)存模型設(shè)計(jì)之–指針壓縮
指針壓縮的目的:
- 為了保證CPU普通對(duì)象指針(oop)緩存
- 為了減少GC的發(fā)生,因?yàn)橹羔槻粔嚎s是8字節(jié),這樣在64位操作系統(tǒng)的堆上其他資源空間就少了。
64位操作系統(tǒng)中 內(nèi)存 > 4G 默認(rèn)開(kāi)啟指針壓縮技術(shù),內(nèi)存< 4G,默認(rèn)是32位系統(tǒng)默認(rèn)不開(kāi)啟。內(nèi)存 > 32G 指針壓縮失效。所以我們通常在部署服務(wù)時(shí),JVM內(nèi)存不要超過(guò)32G,因?yàn)槌^(guò)32G就無(wú)法開(kāi)啟 指針壓縮了。
內(nèi)存 > 32G指針壓縮失效的原因是:4G*8 = 32G
32位系統(tǒng)的CPU 最大支持2^32 = 4G ,如果是64位系統(tǒng),最大支持 2^64, 但是對(duì)其填充是按照8字節(jié)進(jìn)行填充,指針壓縮可以理解為在32位系統(tǒng)在64位上面使用,因?yàn)?2位系統(tǒng)的CPU尋址空間最大支持4G,對(duì)其填充*8 = 32G,這就是內(nèi)存>32G指針壓縮失效的原因。
關(guān)閉指針壓縮 : -XX:+UseCompressedOops
內(nèi)存模型設(shè)計(jì)之–對(duì)齊填充
對(duì)齊填充的意義是提高CPU訪問(wèn)數(shù)據(jù)的效率,主要針對(duì)會(huì)存在該實(shí)例對(duì)象數(shù)據(jù)跨內(nèi)存地址區(qū)域存儲(chǔ)的情況。
例如:在沒(méi)有對(duì)齊填充的情況下,內(nèi)存地址存放情況如下:

因?yàn)樘幚砥髦荒?x00-0x07,0x08-0x0F這樣讀取數(shù)據(jù),所以當(dāng)我們想獲取這個(gè)long型的數(shù)據(jù)時(shí),處理 器必須要讀兩次內(nèi)存,第一次(0x00-0x07),第二次(0x08-0x0F),然后將兩次的結(jié)果才能獲得真正的數(shù)值。
那么在有對(duì)齊填充的情況下,內(nèi)存地址存放情況是這樣的:

現(xiàn)在處理器只需要直接一次讀取(0x08-0x0F)的內(nèi)存地址就可以獲得我們想要的數(shù)據(jù)了。


