Java 虛擬機(jī)屏蔽了與具體操作系統(tǒng)平臺(tái)相關(guān)的信息,使得 Java 語(yǔ)言編譯程序只需生成在 Java 虛擬機(jī)上運(yùn)行的目標(biāo)代碼(字節(jié)碼),就可以在多種平臺(tái)上不加修改地運(yùn)行甲脏。Java 虛擬機(jī)在執(zhí)行字節(jié)碼時(shí),實(shí)際上最終還是把字節(jié)碼解釋成具體平臺(tái)上的機(jī)器指令執(zhí)行。
Java 的優(yōu)點(diǎn)
- 是一門(mén)結(jié)構(gòu)嚴(yán)謹(jǐn)提茁、面向?qū)ο蟮木幊陶Z(yǔ)言饺窿。
- 擺脫了硬件平臺(tái)的束縛鸿捧,實(shí)現(xiàn)了“一次編寫(xiě)嗤军,到處運(yùn)行”的理想靶瘸。
- 提供了一種相對(duì)安全的內(nèi)存管理和訪問(wèn)機(jī)制寝优,避免了絕大部分的內(nèi)存泄漏和指針越界問(wèn)題条舔。
- 實(shí)現(xiàn)了熱點(diǎn)代碼檢測(cè)和運(yùn)行時(shí)編譯及優(yōu)化,使得 Java 應(yīng)用能隨著運(yùn)行時(shí)間的增加而獲得更高的性能乏矾。
- 有一套完善的應(yīng)用程序接口和無(wú)數(shù)的來(lái)自商業(yè)機(jī)構(gòu)和開(kāi)源社區(qū)的第三方類(lèi)庫(kù)來(lái)幫助實(shí)現(xiàn)各種各樣的功能孟抗。
Java 平臺(tái)的邏輯結(jié)構(gòu)
JVM
- JVM 是一種基于下層的操作系統(tǒng)和硬件平臺(tái)并利用軟件方法來(lái)實(shí)現(xiàn)的抽象的計(jì)算機(jī),可以在上面執(zhí)行 Java 的字節(jié)碼程序钻心。簡(jiǎn)單的說(shuō)凄硼,JVM 就是 Java 的虛擬機(jī),有了 JVM 才能運(yùn)行 Java 程序。
-
Java 編譯器只需面向 JVM捷沸,生成 JVM 能理解的代碼或字節(jié)碼文件摊沉。Java 源文件經(jīng)編譯器,編譯成字節(jié)碼程序痒给,通過(guò) JVM 將每一條指令翻譯成不同平臺(tái)機(jī)器碼说墨,通過(guò)特定平臺(tái)運(yùn)行。
JVM 自身的物理結(jié)構(gòu)
class 文件的組成
- 結(jié)構(gòu)信息苍柏。包括 class 文件格式版本號(hào)及各部分的數(shù)量與大小的信息尼斧。
- 元數(shù)據(jù)。對(duì)應(yīng)于 Java 源碼中聲明與常量的信息序仙。包含類(lèi)/繼承的超類(lèi)/實(shí)現(xiàn)的接口的聲明信息突颊、域與方法聲明信息和常量池。
- 方法信息潘悼。對(duì)應(yīng) Java 源碼中語(yǔ)句和表達(dá)式對(duì)應(yīng)的信息律秃。包含字節(jié)碼、異常處理器表治唤、求值棧與局部變量區(qū)大小棒动、求值棧的類(lèi)型記錄、調(diào)試符號(hào)信息宾添。
類(lèi)的層次關(guān)系和加載順序
類(lèi)執(zhí)行機(jī)制
- JVM 是基于棧的體系結(jié)構(gòu)來(lái)執(zhí)行 class 字節(jié)碼的船惨。線程創(chuàng)建后,都會(huì)產(chǎn)生程序計(jì)數(shù)器(PC)和棧(Stack)缕陕,程序計(jì)數(shù)器存放下一條要執(zhí)行的指令在方法內(nèi)的偏移量粱锐,棧中存放一個(gè)個(gè)棧幀,每個(gè)棧幀對(duì)應(yīng)著每個(gè)方法的每次調(diào)用扛邑,而棧幀又是有局部變量區(qū)和操作數(shù)棧兩部分組成怜浅,局部變量區(qū)用于存放方法中的局部變量和參數(shù),操作數(shù)棧中用于存放方法執(zhí)行過(guò)程中產(chǎn)生的中間結(jié)果蔬崩。
內(nèi)存區(qū)域
- Java 虛擬機(jī)在執(zhí)行 Java 程序的過(guò)程中會(huì)把他所管理的內(nèi)存劃分為若干個(gè)不同的數(shù)據(jù)區(qū)域恶座。
-
Java 虛擬機(jī)規(guī)范將 JVM 所管理的內(nèi)存分為以下幾個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū):程序計(jì)數(shù)器、Java 虛擬機(jī)棧沥阳、本地方法棧跨琳、Java 堆、方法區(qū)桐罕。
內(nèi)存區(qū)域圖
程序計(jì)數(shù)器
- 一塊較小的內(nèi)存空間脉让,它是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器,字節(jié)碼解釋器工作時(shí)通過(guò)改變?cè)撚?jì)數(shù)器的值來(lái)選擇下一條需要執(zhí)行的字節(jié)碼指令功炮,分支溅潜、跳轉(zhuǎn)、循環(huán)等基礎(chǔ)功能都要依賴(lài)它來(lái)實(shí)現(xiàn)死宣。
- 每條線程都有一個(gè)獨(dú)立的的程序計(jì)數(shù)器伟恶,各線程間的計(jì)數(shù)器互不影響,因此該區(qū)域是線程私有的毅该。
- 當(dāng)線程在執(zhí)行一個(gè) Java 方法時(shí)博秫,該計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址,當(dāng)線程在執(zhí)行的是 Native 方法(調(diào)用本地操作系統(tǒng)方法)時(shí)眶掌,該計(jì)數(shù)器的值為空挡育。
- 該內(nèi)存區(qū)域是唯一一個(gè)在 Java 虛擬機(jī)規(guī)范中么有規(guī)定任何 OOM(內(nèi)存溢出:OutOfMemoryError)情況的區(qū)域。
Java 虛擬機(jī)棧
- 該區(qū)域也是線程私有的朴爬,它的生命周期也與線程相同即寒。
- 虛擬機(jī)棧描述的是 Java 方法執(zhí)行的內(nèi)存模型:每個(gè)方法被執(zhí)行的時(shí)候都會(huì)同時(shí)創(chuàng)建一個(gè)棧幀,棧它是用于支持續(xù)虛擬機(jī)進(jìn)行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu)。
- 對(duì)于執(zhí)行引擎來(lái)講母赵,活動(dòng)線程中逸爵,只有棧頂?shù)臈怯行У模Q(chēng)為當(dāng)前棧幀凹嘲,這個(gè)棧幀所關(guān)聯(lián)的方法稱(chēng)為當(dāng)前方法师倔,執(zhí)行引擎所運(yùn)行的所有字節(jié)碼指令都只針對(duì)當(dāng)前棧幀進(jìn)行操作。
- 棧幀用于存儲(chǔ)局部變量表周蹭、操作數(shù)棧趋艘、動(dòng)態(tài)鏈接、方法返回地址和一些額外的附加信息凶朗。
- 在編譯程序代碼時(shí)瓷胧,棧幀中需要多大的局部變量表、多深的操作數(shù)棧都已經(jīng)完全確定了棚愤,并且寫(xiě)入了方法表的 Code 屬性之中搓萧。因此,一個(gè)棧幀需要分配多少內(nèi)存遇八,不會(huì)受到程序運(yùn)行期變量數(shù)據(jù)的影響矛绘,而僅僅取決于具體的虛擬機(jī)實(shí)現(xiàn)。
Java 虛擬機(jī)棧的異常
- 在 Java 虛擬機(jī)規(guī)范中刃永,對(duì)這個(gè)區(qū)域規(guī)定了兩種異常情況:如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度货矮,將拋出StackOverflowError異常。如果虛擬機(jī)在動(dòng)態(tài)擴(kuò)展棧時(shí)無(wú)法申請(qǐng)到足夠的內(nèi)存空間斯够,則拋出OutOfMemoryError異常囚玫。
- 這兩種情況存在著一些互相重疊的地方:當(dāng)棧空間無(wú)法繼續(xù)分配時(shí)读规,到底是內(nèi)存太小抓督,還是已使用的棧空間太大束亏,其本質(zhì)上只是對(duì)同一件事情的兩種描述而已铃在。在單線程的操作中,無(wú)論是由于棧幀太大碍遍,還是虛擬機(jī)椂ㄍ空間太小,當(dāng)椗戮矗空間無(wú)法分配時(shí)揣炕,虛擬機(jī)拋出的都是 StackOverflowError 異常,而不會(huì)得到 OutOfMemoryError 異常东跪。而在多線程環(huán)境下畸陡,則會(huì)拋出 OutOfMemoryError 異常鹰溜。
局部變量表
- 局部變量表是一組變量值存儲(chǔ)空間,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量丁恭,其中存放的數(shù)據(jù)的類(lèi)型是編譯期可知的各種基本數(shù)據(jù)類(lèi)型曹动、對(duì)象引用(reference)和 returnAddress 類(lèi)型(它指向了一條字節(jié)碼指令的地址)。局部變量表所需的內(nèi)存空間在編譯期間完成分配涩惑,即在 Java 程序被編譯成 Class 文件時(shí)仁期,就確定了所需分配的最大局部變量表的容量桑驱。當(dāng)進(jìn)入一個(gè)方法時(shí)竭恬,這個(gè)方法需要在棧中分配多大的局部變量空間是完全確定的,在方法運(yùn)行期間不會(huì)改變局部變量表的大小熬的。
- 局部變量表的容量以變量槽(Slot)為最小單位痊硕。在虛擬機(jī)規(guī)范中并沒(méi)有明確指明一個(gè) Slot 應(yīng)占用的內(nèi)存空間大小(允許其隨著處理器押框、操作系統(tǒng)或虛擬機(jī)的不同而發(fā)生變化)岔绸,一個(gè) Slot 可以存放一個(gè)32位以?xún)?nèi)的數(shù)據(jù)類(lèi)型:boolean、byte橡伞、char盒揉、short、int兑徘、float刚盈、reference 和 returnAddresss。reference 是對(duì)象的引用類(lèi)型挂脑,returnAddress 是為字節(jié)指令服務(wù)的藕漱,它執(zhí)行了一條字節(jié)碼指令的地址。對(duì)于 64 位的數(shù)據(jù)類(lèi)型(long和double)崭闲,虛擬機(jī)會(huì)以高位在前的方式為其分配兩個(gè)連續(xù)的 Slot 空間肋联。
- 虛擬機(jī)通過(guò)索引定位的方式使用局部變量表,索引值的范圍是從 0 開(kāi)始到局部變量表最大的 Slot 數(shù)量刁俭,對(duì)于 32 位數(shù)據(jù)類(lèi)型的變量橄仍,索引 n 代表第 n 個(gè) Slot,對(duì)于 64 位的牍戚,索引 n 代表第 n 和第 n+1 兩個(gè) Slot侮繁。
- 在方法執(zhí)行時(shí),虛擬機(jī)是使用局部變量表來(lái)完成參數(shù)值到參數(shù)變量列表的傳遞過(guò)程的翘魄,如果是實(shí)例方法(非static)鼎天,則局部變量表中的第 0 位索引的 Slot 默認(rèn)是用于傳遞方法所屬對(duì)象實(shí)例的引用,在方法中可以通過(guò)關(guān)鍵字“this”來(lái)訪問(wèn)這個(gè)隱含的參數(shù)暑竟。其余參數(shù)則按照參數(shù)表的順序來(lái)排列斋射,占用從1開(kāi)始的局部變量 Slot育勺,參數(shù)表分配完畢后,再根據(jù)方法體內(nèi)部定義的變量順序和作用域分配其余的 Slot罗岖。
- 局部變量表中的 Slot 是可重用的涧至,方法體中定義的變量,作用域并不一定會(huì)覆蓋整個(gè)方法體桑包,如果當(dāng)前字節(jié)碼PC計(jì)數(shù)器的值已經(jīng)超過(guò)了某個(gè)變量的作用域南蓬,那么這個(gè)變量對(duì)應(yīng)的 Slot 就可以交給其他變量使用。這樣的設(shè)計(jì)不僅僅是為了節(jié)省空間哑了,在某些情況下 Slot 的復(fù)用會(huì)直接影響到系統(tǒng)的而垃圾收集行為赘方。
操作數(shù)棧
- 操作數(shù)棧又常被稱(chēng)為操作棧,操作數(shù)棧的最大深度也是在編譯的時(shí)候就確定了弱左。32 位數(shù)據(jù)類(lèi)型所占的棧容量為 1,64 位數(shù)據(jù)類(lèi)型所占的棧容量為 2窄陡。當(dāng)一個(gè)方法開(kāi)始執(zhí)行時(shí),它的操作棧是空的拆火,在方法的執(zhí)行過(guò)程中跳夭,會(huì)有各種字節(jié)碼指令(比如:加操作、賦值元算等)向操作棧中寫(xiě)入和提取內(nèi)容们镜,也就是入棧和出棧操作币叹。
- Java 虛擬機(jī)的解釋執(zhí)行引擎稱(chēng)為“基于棧的執(zhí)行引擎”,其中所指的“椖O粒”就是操作數(shù)棧颈抚。因此我們也稱(chēng) Java 虛擬機(jī)是基于棧的,這點(diǎn)不同于 Android 虛擬機(jī)胞皱,Android 虛擬機(jī)是基于寄存器的邪意。
- 基于棧的指令集最主要的優(yōu)點(diǎn)是可移植性強(qiáng),主要的缺點(diǎn)是執(zhí)行速度相對(duì)會(huì)慢些反砌;而由于寄存器由硬件直接提供雾鬼,所以基于寄存器指令集最主要的優(yōu)點(diǎn)是執(zhí)行速度快,主要的缺點(diǎn)是可移植性差宴树。
動(dòng)態(tài)連接
- 每個(gè)棧幀都包含一個(gè)指向運(yùn)行時(shí)常量池(在方法區(qū)中策菜,后面介紹)中該棧幀所屬方法的引用,持有這個(gè)引用是為了支持方法調(diào)用過(guò)程中的動(dòng)態(tài)連接酒贬。Class 文件的常量池中存在有大量的符號(hào)引用又憨,字節(jié)碼中的方法調(diào)用指令就以常量池中指向方法的符號(hào)引用為參數(shù)。這些符號(hào)引用锭吨,一部分會(huì)在類(lèi)加載階段或第一次使用的時(shí)候轉(zhuǎn)化為直接引用(如 final蠢莺、static 域等),稱(chēng)為靜態(tài)解析零如,另一部分將在每一次的運(yùn)行期間轉(zhuǎn)化為直接引用躏将,這部分稱(chēng)為動(dòng)態(tài)連接锄弱。
方法返回地址
- 當(dāng)一個(gè)方法被執(zhí)行后,有兩種方式退出該方法:執(zhí)行引擎遇到了任意一個(gè)方法返回的字節(jié)碼指令或遇到了異常祸憋,并且該異常沒(méi)有在方法體內(nèi)得到處理会宪。無(wú)論采用何種退出方式,在方法退出之后蚯窥,都需要返回到方法被調(diào)用的位置掸鹅,程序才能繼續(xù)執(zhí)行。方法返回時(shí)可能需要在棧幀中保存一些信息拦赠,用來(lái)幫助恢復(fù)它的上層方法的執(zhí)行狀態(tài)巍沙。一般來(lái)說(shuō),方法正常退出時(shí)矛紫,調(diào)用者的 PC 計(jì)數(shù)器的值就可以作為返回地址赎瞎,棧幀中很可能保存了這個(gè)計(jì)數(shù)器值,而方法異常退出時(shí)颊咬,返回地址是要通過(guò)異常處理器來(lái)確定的,棧幀中一般不會(huì)保存這部分信息牡辽。
- 方法退出的過(guò)程實(shí)際上等同于把當(dāng)前棧幀出站喳篇,因此退出時(shí)可能執(zhí)行的操作有:恢復(fù)上層方法的局部變量表和操作數(shù)棧,如果有返回值态辛,則把它壓入調(diào)用者棧幀的操作數(shù)棧中麸澜,調(diào)整 PC 計(jì)數(shù)器的值以指向方法調(diào)用指令后面的一條指令。
本地方法棧
- 該區(qū)域與虛擬機(jī)棧所發(fā)揮的作用非常相似奏黑,只是虛擬機(jī)棧為虛擬機(jī)執(zhí)行 Java 方法服務(wù)炊邦,而本地方法棧則為使用到的本地操作系統(tǒng)(Native)方法服務(wù)。
Java 堆
- Java Heap 是 Java 虛擬機(jī)所管理的內(nèi)存中最大的一塊熟史,它是所有線程共享的一塊內(nèi)存區(qū)域馁害。幾乎所有的對(duì)象實(shí)例和數(shù)組都在這類(lèi)分配內(nèi)存。Java Heap 是垃圾收集器管理的主要區(qū)域蹂匹,因此很多時(shí)候也被稱(chēng)為“GC堆”碘菜。
- 根據(jù) Java 虛擬機(jī)規(guī)范的規(guī)定,Java 堆可以處在物理上不連續(xù)的內(nèi)存空間中限寞,只要邏輯上是連續(xù)的即可忍啸。如果在堆中沒(méi)有內(nèi)存可分配時(shí),并且堆也無(wú)法擴(kuò)展時(shí)履植,將會(huì)拋出 OutOfMemoryError 異常计雌。
方法區(qū)
- 方法區(qū)也是各個(gè)線程共享的內(nèi)存區(qū)域,它用于存儲(chǔ)已經(jīng)被虛擬機(jī)加載的類(lèi)信息玫霎、常量凿滤、靜態(tài)變量传泊、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。
- 方法區(qū)域又被稱(chēng)為“永久代”鸭巴,但這僅僅對(duì)于 Sun HotSpot 來(lái)講眷细,JRockit 和 IBM J9 虛擬機(jī)中并不存在永久代的概念。
- Java 虛擬機(jī)規(guī)范把方法區(qū)描述為 Java 堆的一個(gè)邏輯部分鹃祖,而且它和 Java Heap 一樣不需要連續(xù)的內(nèi)存溪椎,可以選擇固定大小或可擴(kuò)展,另外恬口,虛擬機(jī)規(guī)范允許該區(qū)域可以選擇不實(shí)現(xiàn)垃圾回收校读。相對(duì)而言,垃圾收集行為在這個(gè)區(qū)域比較少出現(xiàn)祖能。該區(qū)域的內(nèi)存回收目標(biāo)主要針是對(duì)廢棄常量的和無(wú)用類(lèi)的回收歉秫。
- 運(yùn)行時(shí)常量池是方法區(qū)的一部分,Class 文件中除了有類(lèi)的版本养铸、字段雁芙、方法、接口等描述信息外钞螟,還有一項(xiàng)信息是常量池(Class文件常量池)兔甘,用于存放編譯器生成的各種字面量和符號(hào)引用,這部分內(nèi)容將在類(lèi)加載后存放到方法區(qū)的運(yùn)行時(shí)常量池中鳞滨。
- 運(yùn)行時(shí)常量池相對(duì)于 Class 文件常量池的另一個(gè)重要特征是具備動(dòng)態(tài)性洞焙,Java 語(yǔ)言并不要求常量一定只能在編譯期產(chǎn)生,也就是并非預(yù)置入 Class 文件中的常量池的內(nèi)容才能進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池拯啦,運(yùn)行期間也可能將新的常量放入池中澡匪,這種特性被開(kāi)發(fā)人員利用比較多的是 String 類(lèi)的 intern()方法。
直接內(nèi)存
- 直接內(nèi)存并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分褒链,也不是 Java 虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域唁情,它直接從操作系統(tǒng)中分配,因此不受 Java 堆大小的限制碱蒙,但是會(huì)受到本機(jī)總內(nèi)存的大小及處理器尋址空間的限制荠瘪,因此它也可能導(dǎo)致 OutOfMemoryError 異常出現(xiàn)。
- 在 JDK1.4 中新引入了 NIO 機(jī)制赛惩,它是一種基于通道與緩沖區(qū)的新 I/O 方式哀墓,可以直接從操作系統(tǒng)中分配直接內(nèi)存,即在堆外分配內(nèi)存喷兼,這樣能在一些場(chǎng)景中提高性能篮绰,因?yàn)楸苊饬嗽?Java 堆和 Native 堆中來(lái)回復(fù)制數(shù)據(jù)。
- 根據(jù) Java 虛擬機(jī)規(guī)范的規(guī)定季惯,當(dāng)方法區(qū)無(wú)法滿足內(nèi)存分配需求時(shí)吠各,將拋出 OutOfMemoryError 異常臀突。
內(nèi)存溢出
- 在多線程情況下,給每個(gè)線程的棧分配的內(nèi)存越大贾漏,反而越容易產(chǎn)生內(nèi)存溢出異常候学。操作系統(tǒng)為每個(gè)進(jìn)程分配的內(nèi)存是有限制的,虛擬機(jī)提供了參數(shù)來(lái)控制 Java 堆和方法區(qū)這兩部分內(nèi)存的最大值纵散,忽略掉程序計(jì)數(shù)器消耗的內(nèi)存(很惺崧搿),以及進(jìn)程本身消耗的內(nèi)存伍掀,剩下的內(nèi)存便給了虛擬機(jī)棧和本地方法棧掰茶,每個(gè)線程分配到的棧容量越大,可以建立的線程數(shù)量自然就越少蜜笤。因此濒蒋,如果是建立過(guò)多的線程導(dǎo)致的內(nèi)存溢出,在不能減少線程數(shù)的情況下把兔,就只能通過(guò)減少最大堆和每個(gè)線程的棧容量來(lái)?yè)Q取更多的線程沪伙。
- 內(nèi)存泄露是指分配出去的內(nèi)存沒(méi)有被回收回來(lái),由于失去了對(duì)該內(nèi)存區(qū)域的控制垛贤,因而造成了資源的浪費(fèi)焰坪。Java 中一般不會(huì)產(chǎn)生內(nèi)存泄露,因?yàn)橛欣厥掌髯詣?dòng)回收垃圾聘惦,但這也不絕對(duì),當(dāng)我們 new 了對(duì)象儒恋,并保存了其引用善绎,但是后面一直沒(méi)用它,而垃圾回收器又不會(huì)去回收它诫尽,這邊會(huì)造成內(nèi)存泄露禀酱,
- 內(nèi)存溢出是指程序所需要的內(nèi)存超出了系統(tǒng)所能分配的內(nèi)存(包括動(dòng)態(tài)擴(kuò)展)的上限。
對(duì)象實(shí)例化分析
Object obj = new Object();
- 假設(shè)該語(yǔ)句出現(xiàn)在方法體中牧嫉,obj 會(huì)作為引用類(lèi)型(reference)的數(shù)據(jù)保存在 Java 棧的本地變量表中剂跟,而會(huì)在 Java 堆中保存該引用的實(shí)例化對(duì)象,Java 堆中還包含能查找到此對(duì)象類(lèi)型數(shù)據(jù)的地址信息(如對(duì)象類(lèi)型酣藻、父類(lèi)曹洽、實(shí)現(xiàn)的接口、方法等)辽剧,這些類(lèi)型數(shù)據(jù)則保存在方法區(qū)中送淆。
-
由于 reference 類(lèi)型在 Java 虛擬機(jī)規(guī)范里面只規(guī)定了一個(gè)指向?qū)ο蟮囊茫](méi)有定義這個(gè)引用應(yīng)該通過(guò)哪種方式去定位怕轿,以及訪問(wèn)到 Java 堆中的對(duì)象的具體位置偷崩,因此不同虛擬機(jī)實(shí)現(xiàn)的對(duì)象訪問(wèn)方式會(huì)有所不同辟拷,主流的訪問(wèn)方式有兩種:使用句柄池和直接使用指針。這兩種對(duì)象的訪問(wèn)方式各有優(yōu)勢(shì)阐斜,使用句柄訪問(wèn)方式的最大好處就是 reference 中存放的是穩(wěn)定的句柄地址衫冻,在對(duì)象被移動(dòng)(垃圾收集時(shí)移動(dòng)對(duì)象是非常普遍的行為)時(shí)只會(huì)改變句柄中的實(shí)例數(shù)據(jù)指針,而 reference 本身不需要修改谒出。使用直接指針訪問(wèn)方式的最大好處是速度快隅俘,它節(jié)省了一次指針定位的時(shí)間開(kāi)銷(xiāo)。目前 Java 默認(rèn)使用的 HotSpot 虛擬機(jī)采用的便是是第二種方式進(jìn)行對(duì)象訪問(wèn)的到推。
句柄池訪問(wèn)
直接指針訪問(wèn)
類(lèi)文件結(jié)構(gòu)
- Class 文件是一組以8位字節(jié)為基礎(chǔ)單位的二進(jìn)制流考赛,各個(gè)數(shù)據(jù)項(xiàng)目嚴(yán)格按照順序緊湊地排列在 Class 文件中,中間沒(méi)有添加任何分隔符莉测,這使得整個(gè) Class 文件中存儲(chǔ)的內(nèi)容幾乎全部都是程序運(yùn)行的必要數(shù)據(jù)颜骤。
- 根據(jù) Java 虛擬機(jī)規(guī)范的規(guī)定,Class 文件格式采用一種類(lèi)似于 C 語(yǔ)言結(jié)構(gòu)體的偽結(jié)構(gòu)來(lái)存儲(chǔ)捣卤,這種偽結(jié)構(gòu)中只有兩種數(shù)據(jù)類(lèi)型:無(wú)符號(hào)數(shù)和表忍抽。無(wú)符號(hào)數(shù)屬于基本數(shù)據(jù)類(lèi)型,以 u1董朝、u2鸠项、u4、u8 來(lái)分別代表 1子姜、2祟绊、4宋舷、8 個(gè)字節(jié)的無(wú)符號(hào)數(shù)陕靠。表是由多個(gè)無(wú)符號(hào)數(shù)或其他表作為數(shù)據(jù)項(xiàng)構(gòu)成的符合數(shù)據(jù)類(lèi)型,所有的表都習(xí)慣性地以“_info”結(jié)尾夏块。
magic 與 version
- 每個(gè) Class 文件的頭 4 個(gè)字節(jié)稱(chēng)為魔數(shù)(magic)遥赚,它的唯一作用是判斷該文件是否為一個(gè)能被虛擬機(jī)接受的 Class 文件扬舒。它的值固定為 0xCAFEBABE。緊接著 magic 的 4 個(gè)字節(jié)存儲(chǔ)的是 Class 文件的次版本號(hào)和主版本號(hào)凫佛,高版本的 JDK 能向下兼容低版本的 Class 文件讲坎,但不能運(yùn)行更高版本的 Class 文件。
類(lèi)初始化
- 遇到 new愧薛、getstatic晨炕、putstatic、invokestatic 這四條字節(jié)碼指令時(shí)厚满,如果類(lèi)還沒(méi)有進(jìn)行過(guò)初始化府瞄,則需要先觸發(fā)其初始化。生成這四條指令最常見(jiàn)的 Java 代碼場(chǎng)景是:使用 new 關(guān)鍵字實(shí)例化對(duì)象時(shí)、讀取或設(shè)置一個(gè)類(lèi)的靜態(tài)字段(static)時(shí)(被 static 修飾又被 final 修飾的遵馆,已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)鲸郊、以及調(diào)用一個(gè)類(lèi)的靜態(tài)方法時(shí)。
- 使用 Java.lang.refect 包的方法對(duì)類(lèi)進(jìn)行反射調(diào)用時(shí)货邓,如果類(lèi)還沒(méi)有進(jìn)行過(guò)初始化秆撮,則需要先觸發(fā)其初始化。
- 當(dāng)初始化一個(gè)類(lèi)的時(shí)候换况,如果發(fā)現(xiàn)其父類(lèi)還沒(méi)有進(jìn)行初始化职辨,則需要先觸發(fā)其父類(lèi)的初始化。
- 當(dāng)虛擬機(jī)啟動(dòng)時(shí)戈二,用戶(hù)需要指定一個(gè)要執(zhí)行的主類(lèi)舒裤,虛擬機(jī)會(huì)先執(zhí)行該主類(lèi)。
- 通過(guò)子類(lèi)引用父類(lèi)中的靜態(tài)字段觉吭,這時(shí)對(duì)子類(lèi)的引用為被動(dòng)引用腾供,因此不會(huì)初始化子類(lèi),只會(huì)初始化父類(lèi):
- 常量在編譯階段會(huì)存入調(diào)用它的類(lèi)的常量池中鲜滩,本質(zhì)上沒(méi)有直接引用到定義該常量的類(lèi)伴鳖,因此不會(huì)觸發(fā)定義常量的類(lèi)的初始化
- 通過(guò)數(shù)組定義來(lái)引用類(lèi),不會(huì)觸發(fā)類(lèi)的初始化但是會(huì)觸發(fā)了另一個(gè)名為“LLConst”的類(lèi)的初始化徙硅,它是一個(gè)由虛擬機(jī)自動(dòng)生成的榜聂、直接繼承于java.lang.Object 的子類(lèi),創(chuàng)建動(dòng)作由字節(jié)碼指令 newarray 觸發(fā)嗓蘑,很明顯须肆,這是一個(gè)對(duì)數(shù)組引用類(lèi)型的初初始化,而該數(shù)組中的元素僅僅包含一個(gè)對(duì) Const 類(lèi)的引用桩皿,并沒(méi)有對(duì)其進(jìn)行初始化休吠。如果我們加入對(duì) con 數(shù)組中各個(gè) Const 類(lèi)元素的實(shí)例化代碼,便會(huì)觸發(fā) Const 類(lèi)的初始化
- 接口也有初始化過(guò)程业簿,在接口中不能使用“static{}”語(yǔ)句塊,但編譯器仍然會(huì)為接口生成類(lèi)構(gòu)造器阳懂,用于初始化接口中定義的成員變量(實(shí)際上是 static final 修飾的全局常量)梅尤。二者在初始化時(shí)最主要的區(qū)別是:當(dāng)一個(gè)類(lèi)在初始化時(shí),要求其父類(lèi)全部已經(jīng)初始化過(guò)了岩调,但是一個(gè)接口在初始化時(shí)巷燥,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的時(shí)候(如引用接口中定義的常量)号枕,才會(huì)初始化該父接口缰揪。這點(diǎn)也與類(lèi)初始化的情況很不同,調(diào)用類(lèi)中的 static final 常量時(shí)并不會(huì) 觸發(fā)該類(lèi)的初始化,但是調(diào)用接口中的 static final 常量時(shí)便會(huì)觸發(fā)該接口的初始化钝腺。
類(lèi)加載機(jī)制 抛姑??艳狐?
多態(tài)性實(shí)現(xiàn)機(jī)制——靜態(tài)分派與動(dòng)態(tài)分派 定硝??毫目?
Java 語(yǔ)法糖
語(yǔ)法糖(Syntactic Sugar)蔬啡,也稱(chēng)糖衣語(yǔ)法,是由英國(guó)計(jì)算機(jī)學(xué)家 Peter.J.Landin 發(fā)明的一個(gè)術(shù)語(yǔ)镀虐,指在計(jì)算機(jī)語(yǔ)言中添加的某種語(yǔ)法箱蟆,這種語(yǔ)法對(duì)語(yǔ)言的功能并沒(méi)有影響,但是更方便程序員使用刮便。
Java 中最常用的語(yǔ)法糖主要有泛型空猜、變長(zhǎng)參數(shù)、條件編譯诺核、自動(dòng)拆裝箱抄肖、內(nèi)部類(lèi)等。虛擬機(jī)并不支持這些語(yǔ)法窖杀,它們?cè)诰幾g階段就被還原回了簡(jiǎn)單的基礎(chǔ)語(yǔ)法結(jié)構(gòu)漓摩,這個(gè)過(guò)程成為解語(yǔ)法糖。
泛型是 JDK1.5 之后引入的一項(xiàng)新特性入客,Java 語(yǔ)言在還沒(méi)有出現(xiàn)泛型時(shí)管毙,只能通過(guò) Object 是所有類(lèi)型的父類(lèi)和類(lèi)型強(qiáng)制轉(zhuǎn)換這兩個(gè)特點(diǎn)的配合來(lái)實(shí)現(xiàn)泛型的功能,這樣實(shí)現(xiàn)的泛型功能要在程序運(yùn)行期才能知道 Object 真正的對(duì)象類(lèi)型桌硫,在 javac 編譯期夭咬,編譯器無(wú)法檢查這個(gè) Object 的強(qiáng)制轉(zhuǎn)型是否成功,這便將一些風(fēng)險(xiǎn)轉(zhuǎn)接到了程序運(yùn)行期中铆隘。Java 語(yǔ)言在 JDK1.5 之后引入的泛型實(shí)際上只在程序源碼中存在卓舵,在編譯后的字節(jié)碼文件中,就已經(jīng)被替換為了原來(lái)的原生類(lèi)型膀钠,并且在相應(yīng)的地方插入了強(qiáng)制轉(zhuǎn)型代碼掏湾,所以泛型技術(shù)實(shí)際上是 Java 語(yǔ)言的一顆語(yǔ)法糖,Java 語(yǔ)言中的泛型實(shí)現(xiàn)方法稱(chēng)為類(lèi)型擦除肿嘲,基于這種方法實(shí)現(xiàn)的泛型被稱(chēng)為偽泛型融击。
javac 編譯
- javac 編譯器稱(chēng)為前端編譯器,將.java文件編譯成為.class文件雳窟。相對(duì)應(yīng)的還有后端編譯器尊浪,它在程序運(yùn)行期間將字節(jié)碼轉(zhuǎn)變成機(jī)器碼(現(xiàn)在的 Java 程序在運(yùn)行時(shí)基本都是解釋執(zhí)行加編譯執(zhí)行),如 HotSpot 虛擬機(jī)自帶的 JIT(Just In Time Compiler)編譯器(分 Client 端和 Server 端)。
詞法拇涤、語(yǔ)法分析
- 詞法分析是將源代碼的字符流轉(zhuǎn)變?yōu)闃?biāo)記(Token)集合捣作。單個(gè)字符是程序編寫(xiě)過(guò)程中的的最小元素,而標(biāo)記則是編譯過(guò)程的最小元素工育,關(guān)鍵字虾宇、變量名、字面量如绸、運(yùn)算符等都可以成為標(biāo)記嘱朽,比如整型標(biāo)志 int 由三個(gè)字符構(gòu)成,但是它只是一個(gè)標(biāo)記怔接,不可拆分搪泳。
- 語(yǔ)法分析是根據(jù)Token序列來(lái)構(gòu)造抽象語(yǔ)法樹(shù)的過(guò)程。抽象語(yǔ)法樹(shù)是一種用來(lái)描述程序代碼語(yǔ)法結(jié)構(gòu)的樹(shù)形表示方式扼脐,語(yǔ)法樹(shù)的每一個(gè)節(jié)點(diǎn)都代表著程序代碼中的一個(gè)語(yǔ)法結(jié)構(gòu)岸军,如 bao、類(lèi)型瓦侮、修飾符艰赞、運(yùn)算符等。經(jīng)過(guò)這個(gè)步驟后肚吏,編譯器就基本不會(huì)再對(duì)源碼文件進(jìn)行操作了方妖,后續(xù)的操作都建立在抽象語(yǔ)法樹(shù)之上。
填充符號(hào)表
- 完成了語(yǔ)法分析和詞法分析之后罚攀,下一步就是填充符號(hào)表的過(guò)程党觅。符號(hào)表是由一組符號(hào)地址和符號(hào)信息構(gòu)成的表格。符號(hào)表中所登記的信息在編譯的不同階段都要用到斋泄,在語(yǔ)義分析中杯瞻,符號(hào)表所登記的內(nèi)容將用于語(yǔ)義檢查和產(chǎn)生中間代碼,在目標(biāo)代碼生成階段炫掐,黨對(duì)符號(hào)名進(jìn)行地址分配時(shí)魁莉,符號(hào)表是地址分配的依據(jù)。
語(yǔ)義分析
- 語(yǔ)法樹(shù)能表示一個(gè)結(jié)構(gòu)正確的源程序的抽象募胃,但無(wú)法保證源程序是符合邏輯的沛厨。而語(yǔ)義分析的主要任務(wù)是讀結(jié)構(gòu)上正確的源程序進(jìn)行上下文有關(guān)性質(zhì)的審查。語(yǔ)義分析過(guò)程分為標(biāo)注檢查和數(shù)據(jù)及控制流分析兩個(gè)步驟:
- 標(biāo)注檢查步驟檢查的內(nèi)容包括諸如變量使用前是否已被聲明摔认、變量和賦值之間的數(shù)據(jù)類(lèi)型是否匹配等。
- 數(shù)據(jù)及控制流分析是對(duì)程序上下文邏輯更進(jìn)一步的驗(yàn)證宅粥,它可以檢查出諸如程序局部變量在使用前是否有賦值参袱、方法的每條路徑是否都有返回值、是否所有的受查異常都被正確處理了等問(wèn)題。
字節(jié)碼生成
- 字節(jié)碼生成是 javac 編譯過(guò)程的最后一個(gè)階段抹蚀。字節(jié)碼生成階段不僅僅是把前面各個(gè)步驟所生成的信息轉(zhuǎn)化成字節(jié)碼寫(xiě)到磁盤(pán)中剿牺,編譯器還進(jìn)行了少量的代碼添加和轉(zhuǎn)換工作。 實(shí)例構(gòu)造器()方法和類(lèi)構(gòu)造器()方法就是在這個(gè)階段添加到語(yǔ)法樹(shù)之中的(這里的實(shí)例構(gòu)造器并不是指默認(rèn)的構(gòu)造函數(shù)环壤,而是指我們自己重載的構(gòu)造函數(shù)晒来,如果用戶(hù)代碼中沒(méi)有提供任何構(gòu)造函數(shù),那編譯器會(huì)自動(dòng)添加一個(gè)沒(méi)有參數(shù)郑现、訪問(wèn)權(quán)限與當(dāng)前類(lèi)一致的默認(rèn)構(gòu)造函數(shù)湃崩,這個(gè)工作在填充符號(hào)表階段就已經(jīng)完成了)。
JIT 編譯
- Java 程序最初是僅僅通過(guò)解釋器解釋執(zhí)行的接箫,即對(duì)字節(jié)碼逐條解釋執(zhí)行攒读,這種方式的執(zhí)行速度相對(duì)會(huì)比較慢,尤其當(dāng)某個(gè)方法或代碼塊運(yùn)行的特別頻繁時(shí)辛友,這種方式的執(zhí)行效率就顯得很低薄扁。于是后來(lái)在虛擬機(jī)中引入了 JIT 編譯器(即時(shí)編譯器),當(dāng)虛擬機(jī)發(fā)現(xiàn)某個(gè)方法或代碼塊運(yùn)行特別頻繁時(shí)废累,就會(huì)把這些代碼認(rèn)定為“Hot Spot Code”(熱點(diǎn)代碼)邓梅,為了提高熱點(diǎn)代碼的執(zhí)行效率,在運(yùn)行時(shí)邑滨,虛擬機(jī)將會(huì)把這些代碼編譯成與本地平臺(tái)相關(guān)的機(jī)器碼日缨,并進(jìn)行各層次的優(yōu)化,完成這項(xiàng)任務(wù)的正是 JIT 編譯器驼修。
- HotSpot 虛擬機(jī)中內(nèi)置了兩個(gè)JIT編譯器:Client Complier 和 Server Complier殿遂,分別用在客戶(hù)端和服務(wù)端,目前主流的 HotSpot 虛擬機(jī)中默認(rèn)是采用解釋器與其中一個(gè)編譯器直接配合的方式工作乙各。
運(yùn)行過(guò)程中會(huì)被即時(shí)編譯器編譯的“熱點(diǎn)代碼”有兩類(lèi):
- 被多次調(diào)用的方法墨礁。
- 被多次調(diào)用的循環(huán)體。
目前主要的熱點(diǎn)判定方式
- 基于采樣的熱點(diǎn)探測(cè):采用這種方法的虛擬機(jī)會(huì)周期性地檢查各個(gè)線程的棧頂耳峦,如果發(fā)現(xiàn)某些方法經(jīng)常出現(xiàn)在棧頂恩静,那這段方法代碼就是“熱點(diǎn)代碼”。這種探測(cè)方法的好處是實(shí)現(xiàn)簡(jiǎn)單高效蹲坷,還可以很容易地獲取方法調(diào)用關(guān)系驶乾,缺點(diǎn)是很難精確地確認(rèn)一個(gè)方法的熱度,容易因?yàn)槭艿骄€程阻塞或別的外界因素的影響而擾亂熱點(diǎn)探測(cè)循签。
- 基于計(jì)數(shù)器的熱點(diǎn)探測(cè):采用這種方法的虛擬機(jī)會(huì)為每個(gè)方法级乐,甚至是代碼塊建立計(jì)數(shù)器,統(tǒng)計(jì)方法的執(zhí)行次數(shù)县匠,如果執(zhí)行次數(shù)超過(guò)一定的閥值风科,就認(rèn)為它是“熱點(diǎn)方法”撒轮。這種統(tǒng)計(jì)方法實(shí)現(xiàn)復(fù)雜一些,需要為每個(gè)方法建立并維護(hù)計(jì)數(shù)器贼穆,而且不能直接獲取到方法的調(diào)用關(guān)系题山,但是它的統(tǒng)計(jì)結(jié)果相對(duì)更加精確嚴(yán)謹(jǐn)。
在 HotSpot 虛擬機(jī)的熱點(diǎn)判定方式
- 在 HotSpot 虛擬機(jī)中使用的是基于計(jì)數(shù)器的熱點(diǎn)探測(cè)方法故痊,因此它為每個(gè)方法準(zhǔn)備了兩個(gè)計(jì)數(shù)器:方法調(diào)用計(jì)數(shù)器和回邊計(jì)數(shù)器顶瞳。
- 方法調(diào)用計(jì)數(shù)器用來(lái)統(tǒng)計(jì)方法調(diào)用的次數(shù),在默認(rèn)設(shè)置下愕秫,方法調(diào)用計(jì)數(shù)器統(tǒng)計(jì)的并不是方法被調(diào)用的絕對(duì)次數(shù)慨菱,而是一個(gè)相對(duì)的執(zhí)行頻率,即一段時(shí)間內(nèi)方法被調(diào)用的次數(shù)豫领。
- 回邊計(jì)數(shù)器用于統(tǒng)計(jì)一個(gè)方法中循環(huán)體代碼執(zhí)行的次數(shù)(準(zhǔn)確地說(shuō)抡柿,應(yīng)該是回邊的次數(shù),因?yàn)椴⒎撬械难h(huán)都是回邊)等恐,在字節(jié)碼中遇到控制流向后跳轉(zhuǎn)的指令就稱(chēng)為“回邊”洲劣。
- 在確定虛擬機(jī)運(yùn)行參數(shù)的前提下,這兩個(gè)計(jì)數(shù)器都有一個(gè)確定的閥值课蔬,當(dāng)計(jì)數(shù)器的值超過(guò)了閥值囱稽,就會(huì)觸發(fā)JIT編譯。觸發(fā)了 JIT 編譯后二跋,在默認(rèn)設(shè)置下战惊,執(zhí)行引擎并不會(huì)同步等待編譯請(qǐng)求完成,而是繼續(xù)進(jìn)入解釋器按照解釋方式執(zhí)行字節(jié)碼扎即,直到提交的請(qǐng)求被編譯器編譯完成為止(編譯工作在后臺(tái)線程中進(jìn)行)吞获。當(dāng)編譯工作完成后,下一次調(diào)用該方法或代碼時(shí)谚鄙,就會(huì)使用已編譯的版本各拷。
對(duì)象引用
Java 中的垃圾回收一般是在 Java 堆中進(jìn)行,因?yàn)槎阎袔缀醮娣帕?Java 中所有的對(duì)象實(shí)例闷营。在 JDK1.2 之前烤黍,Java 中的引用定義很很純粹:如果 reference 類(lèi)型的數(shù)據(jù)中存儲(chǔ)的數(shù)值代表的是另外一塊內(nèi)存的起始地址,就稱(chēng)這塊數(shù)據(jù)代表著一個(gè)引用傻盟。但在 JDK1.2 之后速蕊,Java 對(duì)引用的概念進(jìn)行了擴(kuò)充,將其分為強(qiáng)引用(Strong Reference)娘赴、軟引用(Soft Reference)规哲、弱引用(Weak Reference)、虛引用(Phantom Reference)四種诽表,引用強(qiáng)度依次減弱媳叨。
- 強(qiáng)引用:如“Object obj = new Object()”腥光,這類(lèi)引用是 Java 程序中最普遍的。只要強(qiáng)引用還存在糊秆,垃圾收集器就永遠(yuǎn)不會(huì)回收掉被引用的對(duì)象。
- 軟引用:它用來(lái)描述一些可能還有用议双,但并非必須的對(duì)象痘番。在系統(tǒng)內(nèi)存不夠用時(shí),這類(lèi)引用關(guān)聯(lián)的對(duì)象將被垃圾收集器回收平痰。JDK1.2 之后提供了 SoftReference 類(lèi)來(lái)實(shí)現(xiàn)軟引用汞舱。
- 弱引用:它也是用來(lái)描述非需對(duì)象的,但它的強(qiáng)度比軟引用更弱些宗雇,被弱引用關(guān)聯(lián)的對(duì)象只能生存島下一次垃圾收集發(fā)生之前昂芜。當(dāng)垃圾收集器工作時(shí),無(wú)論當(dāng)前內(nèi)存是否足夠赔蒲,都會(huì)回收掉只被弱引用關(guān)聯(lián)的對(duì)象泌神。在 JDK1.2 之后,提供了 WeakReference 類(lèi)來(lái)實(shí)現(xiàn)弱引用舞虱。
- 虛引用:最弱的一種引用關(guān)系欢际,完全不會(huì)對(duì)其生存時(shí)間構(gòu)成影響,也無(wú)法通過(guò)虛引用來(lái)取得一個(gè)對(duì)象實(shí)例矾兜。為一個(gè)對(duì)象設(shè)置虛引用關(guān)聯(lián)的唯一目的是希望能在這個(gè)對(duì)象被收集器回收時(shí)收到一個(gè)系統(tǒng)通知损趋。JDK1.2 之后提供了 PhantomReference 類(lèi)來(lái)實(shí)現(xiàn)虛引用。
垃圾對(duì)象的判定
引用計(jì)數(shù)算法
- 給對(duì)象添加一個(gè)引用計(jì)數(shù)器椅寺,每當(dāng)有一個(gè)地方引用它時(shí)浑槽,計(jì)數(shù)器值就加 1,當(dāng)引用失效時(shí)返帕,計(jì)數(shù)器值就減1桐玻,任何時(shí)刻計(jì)數(shù)器都為 0 的對(duì)象就是不可能再被使用的。
- 引用計(jì)數(shù)算法的實(shí)現(xiàn)簡(jiǎn)單溉旋,判定效率也很高畸冲,在大部分情況下它都是一個(gè)不錯(cuò)的選擇,當(dāng) Java 語(yǔ)言并沒(méi)有選擇這種算法來(lái)進(jìn)行垃圾回收观腊,主要原因是它很難解決對(duì)象之間的相互循環(huán)引用問(wèn)題邑闲。
根搜索算法
- 這種算法的基本思路是通過(guò)一系列名為“GC Roots”的對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)開(kāi)始向下搜索梧油,搜索所走過(guò)的路徑稱(chēng)為引用鏈苫耸,當(dāng)一個(gè)對(duì)象到 GC Roots 沒(méi)有任何引用鏈相連時(shí),就證明此對(duì)象是不可用的儡陨。Java 和 C# 中都是采用根搜索算法來(lái)判定對(duì)象是否存活的褪子。
- 在根搜索算法中量淌,要真正宣告一個(gè)對(duì)象死亡,至少要經(jīng)歷兩次標(biāo)記過(guò)程:如果對(duì)象在進(jìn)行根搜索后發(fā)現(xiàn)沒(méi)有與 GC Roots 相連接的引用鏈嫌褪,那它會(huì)被第一次標(biāo)記并且進(jìn)行一次篩選呀枢,篩選的條件是此對(duì)象是否有必要執(zhí)行 finalize()方法。當(dāng)對(duì)象沒(méi)有覆蓋 finalize()方法笼痛,或 finalize()方法已經(jīng)被虛擬機(jī)調(diào)用過(guò)裙秋,虛擬機(jī)將這兩種情況都視為沒(méi)有必要執(zhí)行。如果該對(duì)象被判定為有必要執(zhí)行 finalize()方法缨伊,那么這個(gè)對(duì)象將會(huì)被放置在一個(gè)名為 F-Queue 隊(duì)列中摘刑,并在稍后由一條由虛擬機(jī)自動(dòng)建立的、低優(yōu)先級(jí)的 Finalizer 線程去執(zhí)行 finalize()方法刻坊。finalize()方法是對(duì)象逃脫死亡命運(yùn)的最后一次機(jī)會(huì)(因?yàn)橐粋€(gè)對(duì)象的 finalize()方法最多只會(huì)被系統(tǒng)自動(dòng)調(diào)用一次)枷恕,稍后 GC 將對(duì) F-Queue 中的對(duì)象進(jìn)行第二次小規(guī)模的標(biāo)記,如果要在 finalize()方法中成功拯救自己谭胚,只要在 finalize()方法中讓該對(duì)象重引用鏈上的任何一個(gè)對(duì)象建立關(guān)聯(lián)即可徐块。而如果對(duì)象這時(shí)還沒(méi)有關(guān)聯(lián)到任何鏈上的引用,那它就會(huì)被回收掉漏益。
垃圾收集算法
標(biāo)記—清除算法
- 標(biāo)記—清除算法是最基礎(chǔ)的收集算法蛹锰,它分為“標(biāo)記”和“清除”兩個(gè)階段:首先標(biāo)記出所需回收的對(duì)象,在標(biāo)記完成后統(tǒng)一回收掉所有被標(biāo)記的對(duì)象绰疤,它的標(biāo)記過(guò)程其實(shí)就是前面的根搜索算法中判定垃圾對(duì)象的標(biāo)記過(guò)程铜犬。(會(huì)造成大量的內(nèi)存碎片)
標(biāo)記—整理算法
- 復(fù)制算法比較適合于新生代,在老年代中轻庆,對(duì)象存活率比較高癣猾,如果執(zhí)行較多的復(fù)制操作,效率將會(huì)變低余爆,所以老年代一般會(huì)選用其他算法纷宇,如標(biāo)記—整理算法。該算法標(biāo)記的過(guò)程與標(biāo)記—清除算法中的標(biāo)記過(guò)程一樣蛾方,但對(duì)標(biāo)記后出的垃圾對(duì)象的處理情況有所不同像捶,它不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有的對(duì)象都向一端移動(dòng)桩砰,然后直接清理掉端邊界以外的內(nèi)存拓春。(不會(huì)產(chǎn)生內(nèi)存碎片,成本相對(duì)較高)
分代收集
- 當(dāng)前商業(yè)虛擬機(jī)的垃圾收集 都采用分代收集亚隅,它根據(jù)對(duì)象的存活周期的不同將內(nèi)存劃分為幾塊硼莽,一般是把 Java 堆分為新生代和老年代。在新生代中煮纵,每次垃圾收集時(shí)都會(huì)發(fā)現(xiàn)有大量對(duì)象死去懂鸵,只有少量存活偏螺,因此可選用復(fù)制算法來(lái)完成收集,而老年代中因?yàn)閷?duì)象存活率高匆光、沒(méi)有額外空間對(duì)它進(jìn)行分配擔(dān)保套像,就必須使用標(biāo)記—清除算法或標(biāo)記—整理算法來(lái)進(jìn)行回收。
垃圾收集器
內(nèi)存的分配策
- 對(duì)象優(yōu)先在 Eden 分配终息。
- 大對(duì)象直接進(jìn)入老年代凉夯。
- 長(zhǎng)期存活的對(duì)象將進(jìn)入老年代。
垃圾回收策略
- 新生代 GC(Minor GC):發(fā)生在新生代的垃圾收集動(dòng)作采幌,因?yàn)?Java 對(duì)象大多都具有朝生夕滅的特性,因此Minor GC 非常頻繁震桶,一般回收速度也比較快休傍。
- 老年代 GC(Major GC/Full GC):發(fā)生在老年代的 GC,出現(xiàn)了 Major GC蹲姐,經(jīng)常會(huì)伴隨至少一次 Minor GC磨取。由于老年代中的對(duì)象生命周期比較長(zhǎng),因此 Major GC 并不頻繁柴墩,一般都是等待老年代滿了后才進(jìn)行 Full GC忙厌,而且其速度一般會(huì)比 Minor GC 慢 10 倍以上。另外江咳,如果分配了 Direct Memory逢净,在老年代中進(jìn)行 Full GC時(shí),會(huì)順便清理掉 Direct Memory 中的廢棄對(duì)象歼指。
性能調(diào)優(yōu)
- 我們可以通過(guò)給 Java 虛擬機(jī)分配超大堆(前提是物理機(jī)的內(nèi)存足夠大)來(lái)提升服務(wù)器的響應(yīng)速度爹土,但分配超大堆的前提是有把握把應(yīng)用程序的 Full GC 頻率控制得足夠低,因?yàn)橐淮?Full GC 的時(shí)間造成比較長(zhǎng)時(shí)間的停頓踩身≌鸵穑控制 Full GC 頻率的關(guān)鍵是保證應(yīng)用中絕大多數(shù)對(duì)象的生存周期不應(yīng)太長(zhǎng),尤其不能產(chǎn)生批量的挟阻、生命周期長(zhǎng)的大對(duì)象琼娘,這樣才能保證老年代的穩(wěn)定。
- Direct Memory 在堆內(nèi)存外分配附鸽,而且二者均受限于物理機(jī)內(nèi)存脱拼,且成負(fù)相關(guān)關(guān)系,因此分配超大堆時(shí)拒炎,如果用到了 NIO 機(jī)制分配使用了很多的 Direct Memory挪拟,則有可能導(dǎo)致 Direct Memory 的 OutOfMemoryError 異常,這時(shí)可以通過(guò) -XX:MaxDirectMemorySize 參數(shù)調(diào)整 Direct Memory 的大小击你。