柚子快報(bào)激活碼778899分享:JVM學(xué)習(xí)筆記(一)
柚子快報(bào)激活碼778899分享:JVM學(xué)習(xí)筆記(一)
1 .JVM包含哪幾部分?
JVM 主要由四大部分組成:ClassLoader(類加載器),Runtime Data Area(運(yùn)行時(shí)數(shù)據(jù)區(qū),內(nèi)存分區(qū)),Execution Engine(執(zhí)行引擎),Native Interface(本地庫(kù)接口),下圖可以大致描述 JVM 的結(jié)構(gòu)。
?
? JVM 是執(zhí)行 Java 程序的虛擬計(jì)算機(jī)系統(tǒng),那我們來(lái)看看執(zhí)行過(guò)程:首先需要準(zhǔn)備好編譯好的 Java 字節(jié)碼文件(即class文件),計(jì)算機(jī)要運(yùn)行程序需要先通過(guò)一定方式(類加載器)將 class 文件加載到內(nèi)存中(運(yùn)行時(shí)數(shù)據(jù)區(qū)),但是字節(jié)碼文件是JVM定義的一套指令集規(guī)范,并不能直接交給底層操作系統(tǒng)去執(zhí)行,因此需要特定的命令解釋器(執(zhí)行引擎)將字節(jié)碼翻譯成特定的操作系統(tǒng)指令集交給 CPU 去執(zhí)行,這個(gè)過(guò)程中會(huì)需要調(diào)用到一些不同語(yǔ)言為 Java 提供的接口(例如驅(qū)動(dòng)、地圖制作等),這就用到了本地 Native 接口(本地庫(kù)接口)。
ClassLoader:負(fù)責(zé)加載字節(jié)碼文件即 class 文件,class 文件在文件開頭有特定的文件標(biāo)示,并且 ClassLoader 只負(fù)責(zé)class 文件的加載,至于它是否可以運(yùn)行,則由 Execution Engine 決定。Runtime Data Area:是存放數(shù)據(jù)的,分為五部分:Stack(虛擬機(jī)棧),Heap(堆),Method Area(方法區(qū)),PC Register(程序計(jì)數(shù)器),Native Method Stack(本地方法棧)。幾乎所有的關(guān)于 Java 內(nèi)存方面的問(wèn)題,都是集中在這塊。Execution Engine:執(zhí)行引擎,也叫 Interpreter。Class 文件被加載后,會(huì)把指令和數(shù)據(jù)信息放入內(nèi)存中,Execution Engine 則負(fù)責(zé)把這些命令解釋給操作系統(tǒng),即將 JVM 指令集翻譯為操作系統(tǒng)指令集。Native Interface:負(fù)責(zé)調(diào)用本地接口的。他的作用是調(diào)用不同語(yǔ)言的接口給 JAVA 用,他會(huì)在 Native Method Stack 中記錄對(duì)應(yīng)的本地方法,然后調(diào)用該方法時(shí)就通過(guò) Execution Engine 加載對(duì)應(yīng)的本地 lib。原本多用于一些專業(yè)領(lǐng)域,如JAVA驅(qū)動(dòng),地圖制作引擎等,現(xiàn)在關(guān)于這種本地方法接口的調(diào)用已經(jīng)被類似于Socket通信,WebService等方式取代。
2. JVM是如何運(yùn)行的?
JVM的啟動(dòng)過(guò)程分為如下四個(gè)步驟:
JVM的裝入環(huán)境和配置 ? java.exe負(fù)責(zé)查找JRE,并且它會(huì)按照如下的順序來(lái)選擇JRE:
自己目錄下的JRE;父級(jí)目錄下的JRE;查注冊(cè)中注冊(cè)的JRE。 裝載JVM 通過(guò)第一步找到JVM的路徑后,Java.exe通過(guò)LoadJavaVM來(lái)裝入JVM文件。LoadLibrary裝載JVM動(dòng)態(tài)連接庫(kù),然后把JVM中的到處函數(shù)JNI_CreateJavaVM和JNI_GetDefaultJavaVMIntArgs 掛接到InvocationFunction 變量的CreateJavaVM和GetDafaultJavaVMInitArgs 函數(shù)指針變量上。JVM的裝載工作完成。 初始化JVM,獲得本地調(diào)用接口 調(diào)用InvocationFunction -> CreateJavaVM,也就是JVM中JNI_CreateJavaVM方法獲得JNIEnv結(jié)構(gòu)的實(shí)例。 運(yùn)行Java程序 JVM運(yùn)行Java程序的方式有兩種:jar包 與 class。 運(yùn)行jar 的時(shí)候,java.exe調(diào)用GetMainClassName函數(shù),該函數(shù)先獲得JNIEnv實(shí)例然后調(diào)用JarFileJNIEnv類中g(shù)etManifest(),從其返回的Manifest對(duì)象中取getAttrebutes(“Main-Class”)的值,即jar 包中文件:META-INF/MANIFEST.MF指定的Main-Class的主類名作為運(yùn)行的主類。之后main函數(shù)會(huì)調(diào)用Java.c中LoadClass方法裝載該主類(使用JNIEnv實(shí)例的FindClass)。 運(yùn)行Class的時(shí)候,main函數(shù)直接調(diào)用Java.c中的LoadClass方法裝載該類。
3 .Java程序是怎么運(yùn)行的?
概括來(lái)說(shuō),寫好的 Java 源代碼文件經(jīng)過(guò) Java 編譯器編譯成字節(jié)碼文件后,通過(guò)類加載器加載到內(nèi)存中,才能被實(shí)例化,然后到 Java 虛擬機(jī)中解釋執(zhí)行,最后通過(guò)操作系統(tǒng)操作 CPU 執(zhí)行獲取結(jié)果。如下圖:
?
4. 本地方法棧有什么用?
本地方法棧(Native Method Stacks)與虛擬機(jī)棧所發(fā)揮的作用是非常相似的,其區(qū)別只是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法(也就是字節(jié)碼)服務(wù),而本地方法棧則是為虛擬機(jī)使用到的本地(Native)方法服務(wù)。
《Java虛擬機(jī)規(guī)范》對(duì)本地方法棧中方法使用的語(yǔ)言、使用方式與數(shù)據(jù)結(jié)構(gòu)并沒(méi)有任何強(qiáng)制規(guī)定,因此具體的虛擬機(jī)可以根據(jù)需要自由實(shí)現(xiàn)它,甚至有的Java虛擬機(jī)(譬如Hot-Spot虛擬機(jī))直接就把本地方法棧和虛擬機(jī)棧合二為一。與虛擬機(jī)棧一樣,本地方法棧也會(huì)在棧深度溢出或者棧擴(kuò)展失敗時(shí)分別拋出StackOverflowError和OutOfMemoryError異常。
5. 沒(méi)有程序計(jì)數(shù)器會(huì)怎么樣?
沒(méi)有程序計(jì)數(shù)器,Java程序中的流程控制將無(wú)法得到正確的控制,多線程也無(wú)法正確的輪換。
擴(kuò)展閱讀
? 程序計(jì)數(shù)器(Program Counter Register)是一塊較小的內(nèi)存空間,它可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。在Java虛擬機(jī)的概念模型里,字節(jié)碼解釋器工作時(shí)就是通過(guò)改變這個(gè)計(jì)數(shù)器的值來(lái)選取下一條需要執(zhí)行的字節(jié)碼指令,它是程序控制流的指示器,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個(gè)計(jì)數(shù)器來(lái)完成。
? 由于Java虛擬機(jī)的多線程是通過(guò)線程輪流切換、分配處理器執(zhí)行時(shí)間的方式來(lái)實(shí)現(xiàn)的,在任何一個(gè)確定的時(shí)刻,一個(gè)處理器(對(duì)于多核處理器來(lái)說(shuō)是一個(gè)內(nèi)核)都只會(huì)執(zhí)行一條線程中的指令。因此,為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要有一個(gè)獨(dú)立的程序計(jì)數(shù)器,各條線程之間計(jì)數(shù)器互不影響,獨(dú)立存儲(chǔ),我們稱這類內(nèi)存區(qū)域?yàn)椤熬€程私有”的內(nèi)存。
? 如果線程正在執(zhí)行的是一個(gè)Java方法,這個(gè)計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址;如果正在執(zhí)行的是本地(Native)方法,這個(gè)計(jì)數(shù)器值則應(yīng)為空(Undefined)。此內(nèi)存區(qū)域是唯一一個(gè)在《Java虛擬機(jī)規(guī)范》中沒(méi)有規(guī)定任何OutOfMemoryError情況的區(qū)域。
6 .說(shuō)一說(shuō)Java的內(nèi)存分布情況
Java虛擬機(jī)在執(zhí)行Java程序的過(guò)程中會(huì)把它所管理的內(nèi)存劃分為若干個(gè)不同的數(shù)據(jù)區(qū)域。這些區(qū)域有各自的用途,以及創(chuàng)建和銷毀的時(shí)間,有的區(qū)域隨著虛擬機(jī)進(jìn)程的啟動(dòng)而一直存在,有些區(qū)域則是依賴用戶線程的啟動(dòng)和結(jié)束而建立和銷毀。根據(jù)《Java虛擬機(jī)規(guī)范》的規(guī)定,Java虛擬機(jī)所管理的內(nèi)存將會(huì)包括以下幾個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū)域。
?
程序計(jì)數(shù)器 程序計(jì)數(shù)器(Program Counter Register)是一塊較小的內(nèi)存空間,它可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。在Java虛擬機(jī)的概念模型里,字節(jié)碼解釋器工作時(shí)就是通過(guò)改變這個(gè)計(jì)數(shù)器的值來(lái)選取下一條需要執(zhí)行的字節(jié)碼指令,它是程序控制流的指示器,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個(gè)計(jì)數(shù)器來(lái)完成。 由于Java虛擬機(jī)的多線程是通過(guò)線程輪流切換、分配處理器執(zhí)行時(shí)間的方式來(lái)實(shí)現(xiàn)的,在任何一個(gè)確定的時(shí)刻,一個(gè)處理器(對(duì)于多核處理器來(lái)說(shuō)是一個(gè)內(nèi)核)都只會(huì)執(zhí)行一條線程中的指令。因此,為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要有一個(gè)獨(dú)立的程序計(jì)數(shù)器,各條線程之間計(jì)數(shù)器互不影響,獨(dú)立存儲(chǔ),我們稱這類內(nèi)存區(qū)域?yàn)椤熬€程私有”的內(nèi)存。 如果線程正在執(zhí)行的是一個(gè)Java方法,這個(gè)計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址;如果正在執(zhí)行的是本地(Native)方法,這個(gè)計(jì)數(shù)器值則應(yīng)為空(Undefined)。此內(nèi)存區(qū)域是唯一一個(gè)在《Java虛擬機(jī)規(guī)范》中沒(méi)有規(guī)定任何OutOfMemoryError情況的區(qū)域。 Java虛擬機(jī)棧 與程序計(jì)數(shù)器一樣,Java虛擬機(jī)棧(Java Virtual Machine Stack)也是線程私有的,它的生命周期與線程相同。虛擬機(jī)棧描述的是Java方法執(zhí)行的線程內(nèi)存模型:每個(gè)方法被執(zhí)行的時(shí)候,Java虛擬機(jī)都會(huì)同步創(chuàng)建一個(gè)棧幀[插圖](Stack Frame)用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)連接、方法出口等信息。每一個(gè)方法被調(diào)用直至執(zhí)行完畢的過(guò)程,就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中從入棧到出棧的過(guò)程。 在《Java虛擬機(jī)規(guī)范》中,對(duì)這個(gè)內(nèi)存區(qū)域規(guī)定了兩類異常狀況:如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度,將拋出StackOverflowError異常;如果Java虛擬機(jī)棧容量可以動(dòng)態(tài)擴(kuò)展,當(dāng)棧擴(kuò)展時(shí)無(wú)法申請(qǐng)到足夠的內(nèi)存會(huì)拋出OutOfMemoryError異常。 本地方法棧 本地方法棧(Native Method Stacks)與虛擬機(jī)棧所發(fā)揮的作用是非常相似的,其區(qū)別只是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法(也就是字節(jié)碼)服務(wù),而本地方法棧則是為虛擬機(jī)使用到的本地(Native)方法服務(wù)。 《Java虛擬機(jī)規(guī)范》對(duì)本地方法棧中方法使用的語(yǔ)言、使用方式與數(shù)據(jù)結(jié)構(gòu)并沒(méi)有任何強(qiáng)制規(guī)定,因此具體的虛擬機(jī)可以根據(jù)需要自由實(shí)現(xiàn)它,甚至有的Java虛擬機(jī)(譬如Hot-Spot虛擬機(jī))直接就把本地方法棧和虛擬機(jī)棧合二為一。與虛擬機(jī)棧一樣,本地方法棧也會(huì)在棧深度溢出或者棧擴(kuò)展失敗時(shí)分別拋出StackOverflowError和OutOfMemoryError異常。 Java堆 對(duì)于Java應(yīng)用程序來(lái)說(shuō),Java堆(Java Heap)是虛擬機(jī)所管理的內(nèi)存中最大的一塊。Java堆是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對(duì)象實(shí)例,Java世界里“幾乎”所有的對(duì)象實(shí)例都在這里分配內(nèi)存。在《Java虛擬機(jī)規(guī)范》中對(duì)Java堆的描述是:“所有的對(duì)象實(shí)例以及數(shù)組都應(yīng)當(dāng)在堆上分配”,而這里筆者寫的“幾乎”是指從實(shí)現(xiàn)角度來(lái)看,隨著Java語(yǔ)言的發(fā)展,現(xiàn)在已經(jīng)能看到些許跡象表明日后可能出現(xiàn)值類型的支持,即使只考慮現(xiàn)在,由于即時(shí)編譯技術(shù)的進(jìn)步,尤其是逃逸分析技術(shù)的日漸強(qiáng)大,棧上分配、標(biāo)量替換優(yōu)化手段已經(jīng)導(dǎo)致一些微妙的變化悄然發(fā)生,所以說(shuō)Java對(duì)象實(shí)例都分配在堆上也漸漸變得不是那么絕對(duì)了。 根據(jù)《Java虛擬機(jī)規(guī)范》的規(guī)定,Java堆可以處于物理上不連續(xù)的內(nèi)存空間中,但在邏輯上它應(yīng)該被視為連續(xù)的,這點(diǎn)就像我們用磁盤空間去存儲(chǔ)文件一樣,并不要求每個(gè)文件都連續(xù)存放。但對(duì)于大對(duì)象(典型的如數(shù)組對(duì)象),多數(shù)虛擬機(jī)實(shí)現(xiàn)出于實(shí)現(xiàn)簡(jiǎn)單、存儲(chǔ)高效的考慮,很可能會(huì)要求連續(xù)的內(nèi)存空間。 Java堆既可以被實(shí)現(xiàn)成固定大小的,也可以是可擴(kuò)展的,不過(guò)當(dāng)前主流的Java虛擬機(jī)都是按照可擴(kuò)展來(lái)實(shí)現(xiàn)的(通過(guò)參數(shù)-Xmx和-Xms設(shè)定)。如果在Java堆中沒(méi)有內(nèi)存完成實(shí)例分配,并且堆也無(wú)法再擴(kuò)展時(shí),Java虛擬機(jī)將會(huì)拋出OutOfMemoryError異常。 方法區(qū) 方法區(qū)(Method Area)與Java堆一樣,是各個(gè)線程共享的內(nèi)存區(qū)域,它用于存儲(chǔ)已被虛擬機(jī)加載的類型信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼緩存等數(shù)據(jù)。雖然《Java虛擬機(jī)規(guī)范》中把方法區(qū)描述為堆的一個(gè)邏輯部分,但是它卻有一個(gè)別名叫作“非堆”(Non-Heap),目的是與Java堆區(qū)分開來(lái)。 根據(jù)《Java虛擬機(jī)規(guī)范》的規(guī)定,如果方法區(qū)無(wú)法滿足新的內(nèi)存分配需求時(shí),將拋出OutOfMemoryError異常。 運(yùn)行時(shí)常量池 運(yùn)行時(shí)常量池(Runtime Constant Pool)是方法區(qū)的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項(xiàng)信息是常量池表(Constant Pool Table),用于存放編譯期生成的各種字面量與符號(hào)引用,這部分內(nèi)容將在類加載后存放到方法區(qū)的運(yùn)行時(shí)常量池中。 既然運(yùn)行時(shí)常量池是方法區(qū)的一部分,自然受到方法區(qū)內(nèi)存的限制,當(dāng)常量池?zé)o法再申請(qǐng)到內(nèi)存時(shí)會(huì)拋出OutOfMemoryError異常。 直接內(nèi)存 直接內(nèi)存(Direct Memory)并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是《Java虛擬機(jī)規(guī)范》中定義的內(nèi)存區(qū)域。但是這部分內(nèi)存也被頻繁地使用,而且也可能導(dǎo)致OutOfMemoryError異常出現(xiàn)。 顯然,本機(jī)直接內(nèi)存的分配不會(huì)受到Java堆大小的限制,但是,既然是內(nèi)存,則肯定還是會(huì)受到本機(jī)總內(nèi)存(包括物理內(nèi)存、SWAP分區(qū)或者分頁(yè)文件)大小以及處理器尋址空間的限制,一般服務(wù)器管理員配置虛擬機(jī)參數(shù)時(shí),會(huì)根據(jù)實(shí)際內(nèi)存去設(shè)置-Xmx等參數(shù)信息,但經(jīng)常忽略掉直接內(nèi)存,使得各個(gè)內(nèi)存區(qū)域總和大于物理內(nèi)存限制(包括物理的和操作系統(tǒng)級(jí)的限制),從而導(dǎo)致動(dòng)態(tài)擴(kuò)展時(shí)出現(xiàn)OutOfMemoryError異常。
7 .類存放在哪里?
方法區(qū)(Method Area)與Java堆一樣,是各個(gè)線程共享的內(nèi)存區(qū)域,它用于存儲(chǔ)已被虛擬機(jī)加載的類型信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼緩存等數(shù)據(jù)。雖然《Java虛擬機(jī)規(guī)范》中把方法區(qū)描述為堆的一個(gè)邏輯部分,但是它卻有一個(gè)別名叫作“非堆”(Non-Heap),目的是與Java堆區(qū)分開來(lái)。
8. 局部變量存放在哪里?
與程序計(jì)數(shù)器一樣,Java虛擬機(jī)棧(Java Virtual Machine Stack)也是線程私有的,它的生命周期與線程相同。虛擬機(jī)棧描述的是Java方法執(zhí)行的線程內(nèi)存模型:每個(gè)方法被執(zhí)行的時(shí)候,Java虛擬機(jī)都會(huì)同步創(chuàng)建一個(gè)棧幀(Stack Frame)用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)連接、方法出口等信息。每一個(gè)方法被調(diào)用直至執(zhí)行完畢的過(guò)程,就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中從入棧到出棧的過(guò)程。
? 局部變量表存放了編譯期可知的各種Java虛擬機(jī)基本數(shù)據(jù)類型(boolean、byte、char、short、int、float、long、double)、對(duì)象引用(reference類型,它并不等同于對(duì)象本身,可能是一個(gè)指向?qū)ο笃鹗嫉刂返囊弥羔?,也可能是指向一個(gè)代表對(duì)象的句柄或者其他與此對(duì)象相關(guān)的位置)和returnAddress類型(指向了一條字節(jié)碼指令的地址)。
9 .介紹一下Java代碼的編譯過(guò)程
從Javac代碼的總體結(jié)構(gòu)來(lái)看,編譯過(guò)程大致可以分為1個(gè)準(zhǔn)備過(guò)程和3個(gè)處理過(guò)程,它們分別如下所示。
準(zhǔn)備過(guò)程:初始化插入式注解處理器。 解析與填充符號(hào)表過(guò)程,包括:
詞法、語(yǔ)法分析,將源代碼的字符流轉(zhuǎn)變?yōu)闃?biāo)記集合,構(gòu)造出抽象語(yǔ)法樹。填充符號(hào)表,產(chǎn)生符號(hào)地址和符號(hào)信息。 插入式注解處理器的注解處理過(guò)程: 在Javac源碼中,插入式注解處理器的初始化過(guò)程是在initPorcessAnnotations()方法中完成的,而它的執(zhí)行過(guò)程則是在processAnnotations()方法中完成。這個(gè)方法會(huì)判斷是否還有新的注解處理器需要執(zhí)行,如果有的話,通過(guò)JavacProcessing-Environment類的doProcessing()方法來(lái)生成一個(gè)新的JavaCompiler對(duì)象,對(duì)編譯的后續(xù)步驟進(jìn)行處理。 分析與字節(jié)碼生成過(guò)程,包括:
標(biāo)注檢查,對(duì)語(yǔ)法的靜態(tài)信息進(jìn)行檢查。數(shù)據(jù)流及控制流分析,對(duì)程序動(dòng)態(tài)運(yùn)行過(guò)程進(jìn)行檢查。解語(yǔ)法糖,將簡(jiǎn)化代碼編寫的語(yǔ)法糖還原為原有的形式。字節(jié)碼生成,將前面各個(gè)步驟所生成的信息轉(zhuǎn)化成字節(jié)碼。
上述3個(gè)處理過(guò)程里,執(zhí)行插入式注解時(shí)又可能會(huì)產(chǎn)生新的符號(hào),如果有新的符號(hào)產(chǎn)生,就必須轉(zhuǎn)回到之前的解析、填充符號(hào)表的過(guò)程中重新處理這些新符號(hào),從總體來(lái)看,三者之間的關(guān)系與交互順序如圖所示。
?
10 .介紹一下類加載的過(guò)程
一個(gè)類型從被加載到虛擬機(jī)內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個(gè)生命周期將會(huì)經(jīng)歷加載(Loading)、驗(yàn)證(Verification)、準(zhǔn)備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)七個(gè)階段,其中驗(yàn)證、準(zhǔn)備、解析三個(gè)部分統(tǒng)稱為連接(Linking)。這七個(gè)階段的發(fā)生順序如下圖所示。
?
? 在上述七個(gè)階段中,包括了類加載的全過(guò)程,即加載、驗(yàn)證、準(zhǔn)備、解析和初始化這五個(gè)階段。
? 一、加載
? “加載”(Loading)階段是整個(gè)“類加載”(Class Loading)過(guò)程中的一個(gè)階段,在加載階段,Java虛擬機(jī)需要完成以下三件事情:
通過(guò)一個(gè)類的全限定名來(lái)獲取定義此類的二進(jìn)制字節(jié)流。將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。在內(nèi)存中生成一個(gè)代表這個(gè)類的java.lang.Class對(duì)象,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問(wèn)入口。
? 加載階段結(jié)束后,Java虛擬機(jī)外部的二進(jìn)制字節(jié)流就按照虛擬機(jī)所設(shè)定的格式存儲(chǔ)在方法區(qū)之中了,方法區(qū)中的數(shù)據(jù)存儲(chǔ)格式完全由虛擬機(jī)實(shí)現(xiàn)自行定義,《Java虛擬機(jī)規(guī)范》未規(guī)定此區(qū)域的具體數(shù)據(jù)結(jié)構(gòu)。類型數(shù)據(jù)妥善安置在方法區(qū)之后,會(huì)在Java堆內(nèi)存中實(shí)例化一個(gè)java.lang.Class類的對(duì)象,這個(gè)對(duì)象將作為程序訪問(wèn)方法區(qū)中的類型數(shù)據(jù)的外部接口。
? 二、驗(yàn)證
? 驗(yàn)證是連接階段的第一步,這一階段的目的是確保Class文件的字節(jié)流中包含的信息符合《Java虛擬機(jī)規(guī)范》的全部約束要求,保證這些信息被當(dāng)作代碼運(yùn)行后不會(huì)危害虛擬機(jī)自身的安全。驗(yàn)證階段大致上會(huì)完成下面四個(gè)階段的檢驗(yàn)動(dòng)作:文件格式驗(yàn)證、元數(shù)據(jù)驗(yàn)證、字節(jié)碼驗(yàn)證和符號(hào)引用驗(yàn)證。
文件格式驗(yàn)證: 第一階段要驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范,并且能被當(dāng)前版本的虛擬機(jī)處理。 元數(shù)據(jù)驗(yàn)證: 第二階段是對(duì)字節(jié)碼描述的信息進(jìn)行語(yǔ)義分析,以保證其描述的信息符合《Java語(yǔ)言規(guī)范》的要求。 字節(jié)碼驗(yàn)證: 第三階段是通過(guò)數(shù)據(jù)流分析和控制流分析,確定程序語(yǔ)義是合法的、符合邏輯的。 符號(hào)引用驗(yàn)證: 符號(hào)引用驗(yàn)證可以看作是對(duì)類自身以外(常量池中的各種符號(hào)引用)的各類信息進(jìn)行匹配性校驗(yàn),通俗來(lái)說(shuō)就是,該類是否缺少或者被禁止訪問(wèn)它依賴的某些外部類、方法、字段等資源。
? 三、準(zhǔn)備
? 準(zhǔn)備階段是正式為類中定義的變量(即靜態(tài)變量,被static修飾的變量)分配內(nèi)存并設(shè)置類變量初始值的階段。從概念上講,這些變量所使用的內(nèi)存都應(yīng)當(dāng)在方法區(qū)中進(jìn)行分配,但必須注意到方法區(qū)本身是一個(gè)邏輯上的區(qū)域,在JDK7及之前,HotSpot使用永久代來(lái)實(shí)現(xiàn)方法區(qū)時(shí),實(shí)現(xiàn)是完全符合這種邏輯概念的。而在JDK 8及之后,類變量則會(huì)隨著Class對(duì)象一起存放在Java堆中,這時(shí)候“類變量在方法區(qū)”就完全是一種對(duì)邏輯概念的表述了。
? 四、解析
? 解析階段是Java虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過(guò)程,符號(hào)引用在Class文件中以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等類型的常量出現(xiàn),那解析階段中所說(shuō)的直接引用與符號(hào)引用又有什么關(guān)聯(lián)呢?
? 符號(hào)引用(Symbolic References):符號(hào)引用以一組符號(hào)來(lái)描述所引用的目標(biāo),符號(hào)可以是任何形式的字面量,只要使用時(shí)能無(wú)歧義地定位到目標(biāo)即可。符號(hào)引用與虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局無(wú)關(guān),引用的目標(biāo)并不一定是已經(jīng)加載到虛擬機(jī)內(nèi)存當(dāng)中的內(nèi)容。各種虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局可以各不相同,但是它們能接受的符號(hào)引用必須都是一致的,因?yàn)榉?hào)引用的字面量形式明確定義在《Java虛擬機(jī)規(guī)范》的Class文件格式中。
? 直接引用(Direct References):直接引用是可以直接指向目標(biāo)的指針、相對(duì)偏移量或者是一個(gè)能間接定位到目標(biāo)的句柄。直接引用是和虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局直接相關(guān)的,同一個(gè)符號(hào)引用在不同虛擬機(jī)實(shí)例上翻譯出來(lái)的直接引用一般不會(huì)相同。如果有了直接引用,那引用的目標(biāo)必定已經(jīng)在虛擬機(jī)的內(nèi)存中存在。
? 五、初始化
? 類的初始化階段是類加載過(guò)程的最后一個(gè)步驟,之前介紹的幾個(gè)類加載的動(dòng)作里,除了在加載階段用戶應(yīng)用程序可以通過(guò)自定義類加載器的方式局部參與外,其余動(dòng)作都完全由Java虛擬機(jī)來(lái)主導(dǎo)控制。直到初始化階段,Java虛擬機(jī)才真正開始執(zhí)行類中編寫的Java程序代碼,將主導(dǎo)權(quán)移交給應(yīng)用程序。
? 進(jìn)行準(zhǔn)備階段時(shí),變量已經(jīng)賦過(guò)一次系統(tǒng)要求的初始零值,而在初始化階段,則會(huì)根據(jù)程序員通過(guò)程序編碼制定的主觀計(jì)劃去初始化類變量和其他資源。我們也可以從另外一種更直接的形式來(lái)表達(dá):初始化階段就是執(zhí)行類構(gòu)造器()方法的過(guò)程。()并不是程序員在Java代碼中直接編寫的方法,它是Javac編譯器的自動(dòng)生成物。
11. 介紹一下對(duì)象的實(shí)例化過(guò)程
對(duì)象實(shí)例化過(guò)程,就是執(zhí)行類構(gòu)造函數(shù)對(duì)應(yīng)在字節(jié)碼文件中的()方法(實(shí)例構(gòu)造器),()方法由非靜態(tài)變量、非靜態(tài)代碼塊以及對(duì)應(yīng)的構(gòu)造器組成。
()方法可以重載多個(gè),類有幾個(gè)構(gòu)造器就有幾個(gè)()方法;()方法中的代碼執(zhí)行順序?yàn)椋焊割愖兞砍跏蓟?、父類代碼塊、父類構(gòu)造器、子類變量初始化、子類代碼塊、子類構(gòu)造器。
? 靜態(tài)變量、靜態(tài)代碼塊、普通變量、普通代碼塊、構(gòu)造器的執(zhí)行順序如下圖:
?
? 具有父類的子類的實(shí)例化順序如下:
?
? 擴(kuò)展閱讀
? Java是一門面向?qū)ο蟮木幊陶Z(yǔ)言,Java程序運(yùn)行過(guò)程中無(wú)時(shí)無(wú)刻都有對(duì)象被創(chuàng)建出來(lái)。在語(yǔ)言層面上,創(chuàng)建對(duì)象通常(例外:復(fù)制、反序列化)僅僅是一個(gè)new關(guān)鍵字而已,而在虛擬機(jī)中,對(duì)象(文中討論的對(duì)象限于普通Java對(duì)象,不包括數(shù)組和Class對(duì)象等)的創(chuàng)建又是怎樣一個(gè)過(guò)程呢?
? 當(dāng)Java虛擬機(jī)遇到一條字節(jié)碼new指令時(shí),首先將去檢查這個(gè)指令的參數(shù)是否能在常量池中定位到一個(gè)類的符號(hào)引用,并且檢查這個(gè)符號(hào)引用代表的類是否已被加載、解析和初始化過(guò)。如果沒(méi)有,那必須先執(zhí)行相應(yīng)的類加載過(guò)程。
? 在類加載檢查通過(guò)后,接下來(lái)虛擬機(jī)將為新生對(duì)象分配內(nèi)存。對(duì)象所需內(nèi)存的大小在類加載完成后便可完全確定,為對(duì)象分配空間的任務(wù)實(shí)際上便等同于把一塊確定大小的內(nèi)存塊從Java堆中劃分出來(lái)。假設(shè)Java堆中內(nèi)存是絕對(duì)規(guī)整的,所有被使用過(guò)的內(nèi)存都被放在一邊,空閑的內(nèi)存被放在另一邊,中間放著一個(gè)指針作為分界點(diǎn)的指示器,那所分配內(nèi)存就僅僅是把那個(gè)指針向空閑空間方向挪動(dòng)一段與對(duì)象大小相等的距離,這種分配方式稱為“指針碰撞”(Bump The Pointer)。但如果Java堆中的內(nèi)存并不是規(guī)整的,已被使用的內(nèi)存和空閑的內(nèi)存相互交錯(cuò)在一起,那就沒(méi)有辦法簡(jiǎn)單地進(jìn)行指針碰撞了,虛擬機(jī)就必須維護(hù)一個(gè)列表,記錄上哪些內(nèi)存塊是可用的,在分配的時(shí)候從列表中找到一塊足夠大的空間劃分給對(duì)象實(shí)例,并更新列表上的記錄,這種分配方式稱為“空閑列表”(Free List)。選擇哪種分配方式由Java堆是否規(guī)整決定,而Java堆是否規(guī)整又由所采用的垃圾收集器是否帶有空間壓縮整理(Compact)的能力決定。因此,當(dāng)使用Serial、ParNew等帶壓縮整理過(guò)程的收集器時(shí),系統(tǒng)采用的分配算法是指針碰撞,既簡(jiǎn)單又高效;而當(dāng)使用CMS這種基于清除(Sweep)算法的收集器時(shí),理論上就只能采用較為復(fù)雜的空閑列表來(lái)分配內(nèi)存。
? 除如何劃分可用空間之外,還有另外一個(gè)需要考慮的問(wèn)題:對(duì)象創(chuàng)建在虛擬機(jī)中是非常頻繁的行為,即使僅僅修改一個(gè)指針?biāo)赶虻奈恢?,在并發(fā)情況下也并不是線程安全的,可能出現(xiàn)正在給對(duì)象A分配內(nèi)存,指針還沒(méi)來(lái)得及修改,對(duì)象B又同時(shí)使用了原來(lái)的指針來(lái)分配內(nèi)存的情況。解決這個(gè)問(wèn)題有兩種可選方案:一種是對(duì)分配內(nèi)存空間的動(dòng)作進(jìn)行同步處理——實(shí)際上虛擬機(jī)是采用CAS配上失敗重試的方式保證更新操作的原子性;另外一種是把內(nèi)存分配的動(dòng)作按照線程劃分在不同的空間之中進(jìn)行,即每個(gè)線程在Java堆中預(yù)先分配一小塊內(nèi)存,稱為本地線程分配緩沖(Thread Local Allocation Buffer,TLAB),哪個(gè)線程要分配內(nèi)存,就在哪個(gè)線程的本地緩沖區(qū)中分配,只有本地緩沖區(qū)用完了,分配新的緩存區(qū)時(shí)才需要同步鎖定。虛擬機(jī)是否使用TLAB,可以通過(guò)-XX:+/-UseTLAB參數(shù)來(lái)設(shè)定。
? 內(nèi)存分配完成之后,虛擬機(jī)必須將分配到的內(nèi)存空間(但不包括對(duì)象頭)都初始化為零值,如果使用了TLAB的話,這一項(xiàng)工作也可以提前至TLAB分配時(shí)順便進(jìn)行。這步操作保證了對(duì)象的實(shí)例字段在Java代碼中可以不賦初始值就直接使用,使程序能訪問(wèn)到這些字段的數(shù)據(jù)類型所對(duì)應(yīng)的零值。
? 接下來(lái),Java虛擬機(jī)還要對(duì)對(duì)象進(jìn)行必要的設(shè)置,例如這個(gè)對(duì)象是哪個(gè)類的實(shí)例、如何才能找到類的元數(shù)據(jù)信息、對(duì)象的哈希碼(實(shí)際上對(duì)象的哈希碼會(huì)延后到真正調(diào)用Object::hashCode()方法時(shí)才計(jì)算)、對(duì)象的GC分代年齡等信息。這些信息存放在對(duì)象的對(duì)象頭(Object Header)之中。根據(jù)虛擬機(jī)當(dāng)前運(yùn)行狀態(tài)的不同,如是否啟用偏向鎖等,對(duì)象頭會(huì)有不同的設(shè)置方式。
? 在上面工作都完成之后,從虛擬機(jī)的視角來(lái)看,一個(gè)新的對(duì)象已經(jīng)產(chǎn)生了。但是從Java程序的視角看來(lái),對(duì)象創(chuàng)建才剛剛開始——構(gòu)造函數(shù),即Class文件中的()方法還沒(méi)有執(zhí)行,所有的字段都為默認(rèn)的零值,對(duì)象需要的其他資源和狀態(tài)信息也還沒(méi)有按照預(yù)定的意圖構(gòu)造好。一般來(lái)說(shuō)(由字節(jié)碼流中new指令后面是否跟隨invokespecial指令所決定,Java編譯器會(huì)在遇到new關(guān)鍵字的地方同時(shí)生成這兩條字節(jié)碼指令,但如果直接通過(guò)其他方式產(chǎn)生的則不一定如此),new指令之后會(huì)接著執(zhí)行()方法,按照程序員的意愿對(duì)對(duì)象進(jìn)行初始化,這樣一個(gè)真正可用的對(duì)象才算完全被構(gòu)造出來(lái)。
12. 元空間在棧內(nèi)還是棧外?
? 在棧外,元空間占用的是本地內(nèi)存。
? 擴(kuò)展閱讀
? 許多Java程序員都習(xí)慣在HotSpot虛擬機(jī)上開發(fā)、部署程序,很多人都更愿意把方法區(qū)稱呼為“永久代“,或?qū)烧呋鞛橐徽?。本質(zhì)上這兩者并不是等價(jià)的,因?yàn)閮H僅是當(dāng)時(shí)的HotSpot虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)選擇把收集器的分代設(shè)計(jì)擴(kuò)展至方法區(qū),或者說(shuō)使用永久代來(lái)實(shí)現(xiàn)方法區(qū)而已,這樣使得HotSpot的垃圾收集器能夠像管理Java堆一樣管理這部分內(nèi)存,省去專門為方法區(qū)編寫內(nèi)存管理代碼的工作。但是對(duì)于其他虛擬機(jī)實(shí)現(xiàn),譬如BEAJRockit、IBM J9等來(lái)說(shuō),是不存在永久代的概念的。原則上如何實(shí)現(xiàn)方法區(qū)屬于虛擬機(jī)實(shí)現(xiàn)細(xì)節(jié),不受《Java虛擬機(jī)規(guī)范》管束,并不要求統(tǒng)一。
? 現(xiàn)在回頭來(lái)看,當(dāng)年使用永久代來(lái)實(shí)現(xiàn)方法區(qū)的決定并不是一個(gè)好主意,這種設(shè)計(jì)導(dǎo)致了Java應(yīng)用更容易遇到內(nèi)存溢出的問(wèn)題(永久代有-XX:MaxPermSize的上限,即使不設(shè)置也有默認(rèn)大小,而J9和JRockit只要沒(méi)有觸碰到進(jìn)程可用內(nèi)存的上限,例如32位系統(tǒng)中的4GB限制,就不會(huì)出問(wèn)題),而且有極少數(shù)方法(例如String::intern())會(huì)因永久代的原因而導(dǎo)致不同虛擬機(jī)下有不同的表現(xiàn)。
? 當(dāng)Oracle收購(gòu)BEA獲得了JRockit的所有權(quán)后,準(zhǔn)備把JRockit中的優(yōu)秀功能,譬如Java Mission Control管理工具,移植到HotSpot虛擬機(jī)時(shí),但因?yàn)閮烧邔?duì)方法區(qū)實(shí)現(xiàn)的差異而面臨諸多困難??紤]到HotSpot未來(lái)的發(fā)展,在JDK 6的時(shí)候HotSpot開發(fā)團(tuán)隊(duì)就有放棄永久代,逐步改為采用本地內(nèi)存(Native Memory)來(lái)實(shí)現(xiàn)方法區(qū)的計(jì)劃了,到了JDK 7的HotSpot,已經(jīng)把原本放在永久代的字符串常量池、靜態(tài)變量等移出,而到了JDK 8,終于完全廢棄了永久代的概念,改用與JRockit、J9一樣在本地內(nèi)存中實(shí)現(xiàn)的元空間(Meta-space)來(lái)代替,把JDK 7中永久代還剩余的內(nèi)容(主要是類型信息)全部移到元空間中。
13. 談?wù)凧VM的類加載器,以及雙親委派模型
一、類加載器
? Java虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)有意把類加載階段中的“通過(guò)一個(gè)類的全限定名來(lái)獲取描述該類的二進(jìn)制字節(jié)流”這個(gè)動(dòng)作放到Java虛擬機(jī)外部去實(shí)現(xiàn),以便讓應(yīng)用程序自己決定如何去獲取所需的類。實(shí)現(xiàn)這個(gè)動(dòng)作的代碼被稱為“類加載器”(Class Loader)。
? 類加載器雖然只用于實(shí)現(xiàn)類的加載動(dòng)作,但它在Java程序中起到的作用卻遠(yuǎn)超類加載階段。對(duì)于任意一個(gè)類,都必須由加載它的類加載器和這個(gè)類本身一起共同確立其在Java虛擬機(jī)中的唯一性,每一個(gè)類加載器,都擁有一個(gè)獨(dú)立的類名稱空間。這句話可以表達(dá)得更通俗一些:比較兩個(gè)類是否“相等”,只有在這兩個(gè)類是由同一個(gè)類加載器加載的前提下才有意義,否則,即使這兩個(gè)類來(lái)源于同一個(gè)Class文件,被同一個(gè)Java虛擬機(jī)加載,只要加載它們的類加載器不同,那這兩個(gè)類就必定不相等。
? 二、雙親委派模型
? 自JDK1.2以來(lái),Java一直保持著三層類加載器、雙親委派的類加載架構(gòu)。對(duì)于這個(gè)時(shí)期的Java應(yīng)用,絕大多數(shù)Java程序都會(huì)使用到以下3個(gè)系統(tǒng)提供的類加載器來(lái)進(jìn)行加載。
? 啟動(dòng)類加載器(Bootstrap Class Loader):這個(gè)類加載器負(fù)責(zé)加載存放在
? 這些類加載器之間的協(xié)作關(guān)系“通?!睍?huì)如下圖所示,圖中展示的各種類加載器之間的層次關(guān)系被稱為類加載器的“雙親委派模型(Parents Delegation Model)”。雙親委派模型要求除了頂層的啟動(dòng)類加載器外,其余的類加載器都應(yīng)有自己的父類加載器。不過(guò)這里類加載器之間的父子關(guān)系一般不是以繼承(Inheritance)的關(guān)系來(lái)實(shí)現(xiàn)的,而是通常使用組合(Composition)關(guān)系來(lái)復(fù)用父加載器的代碼。
?
? 雙親委派模型的工作過(guò)程是:如果一個(gè)類加載器收到了類加載的請(qǐng)求,它首先不會(huì)自己去嘗試加載這個(gè)類,而是把這個(gè)請(qǐng)求委派給父類加載器去完成,每一個(gè)層次的類加載器都是如此,因此所有的加載請(qǐng)求最終都應(yīng)該傳送到最頂層的啟動(dòng)類加載器中,只有當(dāng)父加載器反饋?zhàn)约簾o(wú)法完成這個(gè)加載請(qǐng)求(它的搜索范圍中沒(méi)有找到所需的類)時(shí),子加載器才會(huì)嘗試自己去完成加載。
? 使用雙親委派模型來(lái)組織類加載器之間的關(guān)系,一個(gè)顯而易見的好處就是Java中的類隨著它的類加載器一起具備了一種帶有優(yōu)先級(jí)的層次關(guān)系。例如類java.lang.Object,它存放在rt.jar之中,無(wú)論哪一個(gè)類加載器要加載這個(gè)類,最終都是委派給處于模型最頂端的啟動(dòng)類加載器進(jìn)行加載,因此Object類在程序的各種類加載器環(huán)境中都能夠保證是同一個(gè)類。反之,如果沒(méi)有使用雙親委派模型,都由各個(gè)類加載器自行去加載的話,如果用戶自己也編寫了一個(gè)名為java.lang.Object的類,并放在程序的ClassPath中,那系統(tǒng)中就會(huì)出現(xiàn)多個(gè)不同的Object類,Java類型體系中最基礎(chǔ)的行為也就無(wú)從保證,應(yīng)用程序?qū)?huì)變得一片混亂。
? 擴(kuò)展閱讀
? 雙親委派模型對(duì)于保證Java程序的穩(wěn)定運(yùn)作極為重要,但它的實(shí)現(xiàn)卻異常簡(jiǎn)單,用以實(shí)現(xiàn)雙親委派的代碼只有短短十余行,全部集中在java.lang.ClassLoader的loadClass()方法之中。
protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats PerfCounter.getParentDelegationTime().addTime(t1 - t0); PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
? 這段代碼的邏輯清晰易懂:先檢查請(qǐng)求加載的類型是否已經(jīng)被加載過(guò),若沒(méi)有則調(diào)用父加載器的loadClass()方法,若父加載器為空則默認(rèn)使用啟動(dòng)類加載器作為父加載器。假如父類加載器加載失敗,拋出ClassNotFoundException異常的話,才調(diào)用自己的findClass()方法嘗試進(jìn)行加載。
14. 雙親委派機(jī)制會(huì)被破壞嗎?
? 雙親委派模型并不是一個(gè)具有強(qiáng)制性約束的模型,而是Java設(shè)計(jì)者推薦給開發(fā)者們的類加載器實(shí)現(xiàn)方式。在Java的世界中大部分的類加載器都遵循這個(gè)模型,但也有例外的情況,直到Java模塊化出現(xiàn)為止,雙親委派模型主要出現(xiàn)過(guò)3次較大規(guī)?!氨黄茐摹钡那闆r。
? 雙親委派模型的第一次“被破壞”其實(shí)發(fā)生在雙親委派模型出現(xiàn)之前——即JDK 1.2面世以前的“遠(yuǎn)古”時(shí)代。由于雙親委派模型在JDK 1.2之后才被引入,但是類加載器的概念和抽象類java.lang.ClassLoader則在Java的第一個(gè)版本中就已經(jīng)存在,面對(duì)已經(jīng)存在的用戶自定義類加載器的代碼,Java設(shè)計(jì)者們引入雙親委派模型時(shí)不得不做出一些妥協(xié),為了兼容這些已有代碼,無(wú)法再以技術(shù)手段避免loadClass()被子類覆蓋的可能性,只能在JDK 1.2之后的java.lang.ClassLoader中添加一個(gè)新的protected方法findClass(),并引導(dǎo)用戶編寫的類加載邏輯時(shí)盡可能去重寫這個(gè)方法,而不是在loadClass()中編寫代碼。雙親委派的具體邏輯就實(shí)現(xiàn)在這里面,按照l(shuí)oadClass()方法的邏輯,如果父類加載失敗,會(huì)自動(dòng)調(diào)用自己的findClass()方法來(lái)完成加載,這樣既不影響用戶按照自己的意愿去加載類,又可以保證新寫出來(lái)的類加載器是符合雙親委派規(guī)則的。
? 雙親委派模型的第二次“被破壞”是由這個(gè)模型自身的缺陷導(dǎo)致的,雙親委派很好地解決了各個(gè)類加載器協(xié)作時(shí)基礎(chǔ)類型的一致性問(wèn)題(越基礎(chǔ)的類由越上層的加載器進(jìn)行加載),基礎(chǔ)類型之所以被稱為“基礎(chǔ)”,是因?yàn)樗鼈兛偸亲鳛楸挥脩舸a繼承、調(diào)用的API存在,但程序設(shè)計(jì)往往沒(méi)有絕對(duì)不變的完美規(guī)則,如果有基礎(chǔ)類型又要調(diào)用回用戶的代碼,那該怎么辦呢?
? 這并非是不可能出現(xiàn)的事情,一個(gè)典型的例子便是JNDI服務(wù),JNDI現(xiàn)在已經(jīng)是Java的標(biāo)準(zhǔn)服務(wù),它的代碼由啟動(dòng)類加載器來(lái)完成加載(在JDK 1.3時(shí)加入到rt.jar的),肯定屬于Java中很基礎(chǔ)的類型了。但JNDI存在的目的就是對(duì)資源進(jìn)行查找和集中管理,它需要調(diào)用由其他廠商實(shí)現(xiàn)并部署在應(yīng)用程序的ClassPath下的JNDI服務(wù)提供者接口(Service Provider Interface,SPI)的代碼,現(xiàn)在問(wèn)題來(lái)了,啟動(dòng)類加載器是絕不可能認(rèn)識(shí)、加載這些代碼的,那該怎么辦?
? 為了解決這個(gè)困境,Java的設(shè)計(jì)團(tuán)隊(duì)只好引入了一個(gè)不太優(yōu)雅的設(shè)計(jì):線程上下文類加載器(Thread Context ClassLoader)。這個(gè)類加載器可以通過(guò)java.lang.Thread類的setContext-ClassLoader()方法進(jìn)行設(shè)置,如果創(chuàng)建線程時(shí)還未設(shè)置,它將會(huì)從父線程中繼承一個(gè),如果在應(yīng)用程序的全局范圍內(nèi)都沒(méi)有設(shè)置過(guò)的話,那這個(gè)類加載器默認(rèn)就是應(yīng)用程序類加載器。
? 有了線程上下文類加載器,程序就可以做一些“舞弊”的事情了。JNDI服務(wù)使用這個(gè)線程上下文類加載器去加載所需的SPI服務(wù)代碼,這是一種父類加載器去請(qǐng)求子類加載器完成類加載的行為,這種行為實(shí)際上是打通了雙親委派模型的層次結(jié)構(gòu)來(lái)逆向使用類加載器,已經(jīng)違背了雙親委派模型的一般性原則,但也是無(wú)可奈何的事情。Java中涉及SPI的加載基本上都采用這種方式來(lái)完成,例如JNDI、JDBC、JCE、JAXB和JBI等。不過(guò),當(dāng)SPI的服務(wù)提供者多于一個(gè)的時(shí)候,代碼就只能根據(jù)具體提供者的類型來(lái)硬編碼判斷,為了消除這種極不優(yōu)雅的實(shí)現(xiàn)方式,在JDK 6時(shí),JDK提供了java.util.ServiceLoader類,以META-INF/services中的配置信息,輔以責(zé)任鏈模式,這才算是給SPI的加載提供了一種相對(duì)合理的解決方案。
? 雙親委派模型的第三次“被破壞”是由于用戶對(duì)程序動(dòng)態(tài)性的追求而導(dǎo)致的,這里所說(shuō)的“動(dòng)態(tài)性”指的是一些非?!盁帷遍T的名詞:代碼熱替換(HotSwap)、模塊熱部署(Hot Deployment)等。說(shuō)白了就是希望Java應(yīng)用程序能像我們的電腦外設(shè)那樣,接上鼠標(biāo)、U盤,不用重啟機(jī)器就能立即使用,鼠標(biāo)有問(wèn)題或要升級(jí)就換個(gè)鼠標(biāo),不用關(guān)機(jī)也不用重啟。對(duì)于個(gè)人電腦來(lái)說(shuō),重啟一次其實(shí)沒(méi)有什么大不了的,但對(duì)于一些生產(chǎn)系統(tǒng)來(lái)說(shuō),關(guān)機(jī)重啟一次可能就要被列為生產(chǎn)事故,這種情況下熱部署就對(duì)軟件開發(fā)者,尤其是大型系統(tǒng)或企業(yè)級(jí)軟件開發(fā)者具有很大的吸引力。
? 早在2008年,在Java社區(qū)關(guān)于模塊化規(guī)范的第一場(chǎng)戰(zhàn)役里,由Sun/Oracle公司所提出的JSR-294、JSR-277規(guī)范提案就曾敗給以IBM公司主導(dǎo)的JSR-291(即OSGi R4.2)提案。盡管Sun/Oracle并不甘心就此失去Java模塊化的主導(dǎo)權(quán),隨即又再拿出Jigsaw項(xiàng)目迎戰(zhàn),但此時(shí)OSGi已經(jīng)站穩(wěn)腳跟,成為業(yè)界“事實(shí)上”的Java模塊化標(biāo)準(zhǔn)。曾經(jīng)在很長(zhǎng)一段時(shí)間內(nèi),IBM憑借著OSGi廣泛應(yīng)用基礎(chǔ)讓Jigsaw吃盡苦頭,其影響一直持續(xù)到Jigsaw隨JDK 9面世才算告一段落。而且即使Jigsaw現(xiàn)在已經(jīng)是Java的標(biāo)準(zhǔn)功能了,它仍需小心翼翼地避開OSGi運(yùn)行期動(dòng)態(tài)熱部署上的優(yōu)勢(shì),僅局限于靜態(tài)地解決模塊間封裝隔離和訪問(wèn)控制的問(wèn)題,現(xiàn)在我們先來(lái)簡(jiǎn)單看一看OSGi是如何通過(guò)類加載器實(shí)現(xiàn)熱部署的。
? OSGi實(shí)現(xiàn)模塊化熱部署的關(guān)鍵是它自定義的類加載器機(jī)制的實(shí)現(xiàn),每一個(gè)程序模塊(OSGi中稱為Bundle)都有一個(gè)自己的類加載器,當(dāng)需要更換一個(gè)Bundle時(shí),就把Bundle連同類加載器一起換掉以實(shí)現(xiàn)代碼的熱替換。在OSGi環(huán)境下,類加載器不再雙親委派模型推薦的樹狀結(jié)構(gòu),而是進(jìn)一步發(fā)展為更加復(fù)雜的網(wǎng)狀結(jié)構(gòu),當(dāng)收到類加載請(qǐng)求時(shí),OSGi將按照下面的順序進(jìn)行類搜索:
? 將以java.*開頭的類,委派給父類加載器加載。? 否則,將委派列表名單內(nèi)的類,委派給父類加載器加載。? 否則,將Import列表中的類,委派給Export這個(gè)類的Bundle的類加載器加載。? 否則,查找當(dāng)前Bundle的ClassPath,使用自己的類加載器加載。? 否則,查找類是否在自己的Fragment Bundle中,如果在,則委派給Fragment Bundle的類加載器加載。? 否則,查找Dynamic Import列表的Bundle,委派給對(duì)應(yīng)Bundle的類加載器加載。? 否則,類查找失敗。
? 上面的查找順序中只有開頭兩點(diǎn)仍然符合雙親委派模型的原則,其余的類查找都是在平級(jí)的類加載器中進(jìn)行的。
15 .介紹一下Java的垃圾回收機(jī)制
一、哪些內(nèi)存需要回收
? 在Java內(nèi)存運(yùn)行時(shí)區(qū)域的各個(gè)部分中,堆和方法區(qū)這兩個(gè)區(qū)域則有著很顯著的不確定性:一個(gè)接口的多個(gè)實(shí)現(xiàn)類需要的內(nèi)存可能會(huì)不一樣,一個(gè)方法所執(zhí)行的不同條件分支所需要的內(nèi)存也可能不一樣,只有處于運(yùn)行期間,我們才能知道程序究竟會(huì)創(chuàng)建哪些對(duì)象,創(chuàng)建多少個(gè)對(duì)象,這部分內(nèi)存的分配和回收是動(dòng)態(tài)的。垃圾收集器所關(guān)注的正是這部分內(nèi)存該如何管理,我們平時(shí)所說(shuō)的內(nèi)存分配與回收也僅僅特指這一部分內(nèi)存。
? 二、怎么定義垃圾
? 引用計(jì)數(shù)算法:
? 在對(duì)象中添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它時(shí),計(jì)數(shù)器值就加一;當(dāng)引用失效時(shí),計(jì)數(shù)器值就減一;任何時(shí)刻計(jì)數(shù)器為零的對(duì)象就是不可能再被使用的。
? 但是,在Java領(lǐng)域,至少主流的Java虛擬機(jī)里面都沒(méi)有選用引用計(jì)數(shù)算法來(lái)管理內(nèi)存,主要原因是,這個(gè)看似簡(jiǎn)單的算法有很多例外情況要考慮,必須要配合大量額外處理才能保證正確地工作,譬如單純的引用計(jì)數(shù)就很難解決對(duì)象之間相互循環(huán)引用的問(wèn)題。
? 舉個(gè)簡(jiǎn)單的例子:對(duì)象objA和objB都有字段instance,賦值令objA.instance=objB及objB.instance=objA,除此之外,這兩個(gè)對(duì)象再無(wú)任何引用,實(shí)際上這兩個(gè)對(duì)象已經(jīng)不可能再被訪問(wèn),但是它們因?yàn)榛ハ嘁弥鴮?duì)方,導(dǎo)致它們的引用計(jì)數(shù)都不為零,引用計(jì)數(shù)算法也就無(wú)法回收它們。
? 可達(dá)性分析算法:
? 當(dāng)前主流的商用程序語(yǔ)言的內(nèi)存管理子系統(tǒng),都是通過(guò)可達(dá)性分析(Reachability Analysis)算法來(lái)判定對(duì)象是否存活的。這個(gè)算法的基本思路就是通過(guò)一系列稱為“GC Roots”的根對(duì)象作為起始節(jié)點(diǎn)集,從這些節(jié)點(diǎn)開始,根據(jù)引用關(guān)系向下搜索,搜索過(guò)程所走過(guò)的路徑稱為“引用鏈”(Reference Chain),如果某個(gè)對(duì)象到GC Roots間沒(méi)有任何引用鏈相連,或者用圖論的話來(lái)說(shuō)就是從GC Roots到這個(gè)對(duì)象不可達(dá)時(shí),則證明此對(duì)象是不可能再被使用的。
? 如下圖所示,對(duì)象object 5、object 6、object 7雖然互有關(guān)聯(lián),但是它們到GC Roots是不可達(dá)的,因此它們將會(huì)被判定為可回收的對(duì)象。
?
? 在Java技術(shù)體系里面,固定可作為GC Roots的對(duì)象包括以下幾種:
? 在虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象,譬如各個(gè)線程被調(diào)用的方法堆棧中使用到的參數(shù)、局部變量、臨時(shí)變量等。? 在方法區(qū)中類靜態(tài)屬性引用的對(duì)象,譬如Java類的引用類型靜態(tài)變量。? 在方法區(qū)中常量引用的對(duì)象,譬如字符串常量池(String Table)里的引用。? 在本地方法棧中JNI(即通常所說(shuō)的Native方法)引用的對(duì)象。? Java虛擬機(jī)內(nèi)部的引用,如基本數(shù)據(jù)類型對(duì)應(yīng)的Class對(duì)象,一些常駐的異常對(duì)象(比如NullPointExcepiton、OutOfMemoryError)等,還有系統(tǒng)類加載器。? 所有被同步鎖(synchronized關(guān)鍵字)持有的對(duì)象。? 反映Java虛擬機(jī)內(nèi)部情況的JMXBean、JVMTI中注冊(cè)的回調(diào)、本地代碼緩存等。
? 回收方法區(qū):
? 方法區(qū)的垃圾收集主要回收兩部分內(nèi)容:廢棄的常量和不再使用的類型。回收廢棄常量與回收J(rèn)ava堆中的對(duì)象非常類似。舉個(gè)常量池中字面量回收的例子,假如一個(gè)字符串“java”曾經(jīng)進(jìn)入常量池中,但是當(dāng)前系統(tǒng)又沒(méi)有任何一個(gè)字符串對(duì)象的值是“java”,換句話說(shuō),已經(jīng)沒(méi)有任何字符串對(duì)象引用常量池中的“java”常量,且虛擬機(jī)中也沒(méi)有其他地方引用這個(gè)字面量。如果在這時(shí)發(fā)生內(nèi)存回收,而且垃圾收集器判斷確有必要的話,這個(gè)“java”常量就將會(huì)被系統(tǒng)清理出常量池。常量池中其他類(接口)、方法、字段的符號(hào)引用也與此類似。
? 判定一個(gè)常量是否“廢棄”還是相對(duì)簡(jiǎn)單,而要判定一個(gè)類型是否屬于“不再被使用的類”的條件就比較苛刻了。需要同時(shí)滿足下面三個(gè)條件:
? 該類所有的實(shí)例都已經(jīng)被回收,也就是Java堆中不存在該類及其任何派生子類的實(shí)例。? 加載該類的類加載器已經(jīng)被回收,這個(gè)條件除非是經(jīng)過(guò)精心設(shè)計(jì)的可替換類加載器的場(chǎng)景,如OSGi、JSP的重加載等,否則通常是很難達(dá)成的。? 該類對(duì)應(yīng)的java.lang.Class對(duì)象沒(méi)有在任何地方被引用,無(wú)法在任何地方通過(guò)反射訪問(wèn)該類的方法。
? 三、怎么回收垃圾
? 分代收集理論:
? 當(dāng)前商業(yè)虛擬機(jī)的垃圾收集器,大多數(shù)都遵循了“分代收集”(GenerationalCollection)的理論進(jìn)行設(shè)計(jì),分代收集名為理論,實(shí)質(zhì)是一套符合大多數(shù)程序運(yùn)行實(shí)際情況的經(jīng)驗(yàn)法則,它建立在兩個(gè)分代假說(shuō)之上:
? 弱分代假說(shuō)(Weak Generational Hypothesis):絕大多數(shù)對(duì)象都是朝生夕滅的。? 強(qiáng)分代假說(shuō)(Strong Generational Hypothesis):熬過(guò)越多次垃圾收集過(guò)程的對(duì)象就越難以消亡。
? 這兩個(gè)分代假說(shuō)共同奠定了多款常用的垃圾收集器的一致的設(shè)計(jì)原則:收集器應(yīng)該將Java堆劃分出不同的區(qū)域,然后將回收對(duì)象依據(jù)其年齡(年齡即對(duì)象熬過(guò)垃圾收集過(guò)程的次數(shù))分配到不同的區(qū)域之中存儲(chǔ)。顯而易見,如果一個(gè)區(qū)域中大多數(shù)對(duì)象都是朝生夕滅,難以熬過(guò)垃圾收集過(guò)程的話,那么把它們集中放在一起,每次回收時(shí)只關(guān)注如何保留少量存活而不是去標(biāo)記那些大量將要被回收的對(duì)象,就能以較低代價(jià)回收到大量的空間;如果剩下的都是難以消亡的對(duì)象,那把它們集中放在一塊,虛擬機(jī)便可以使用較低的頻率來(lái)回收這個(gè)區(qū)域,這就同時(shí)兼顧了垃圾收集的時(shí)間開銷和內(nèi)存的空間有效利用。
? 標(biāo)記-清除算法:
? 最早出現(xiàn)也是最基礎(chǔ)的垃圾收集算法是“標(biāo)記-清除”(Mark-Sweep)算法,如它的名字一樣,算法分為“標(biāo)記”和“清除”兩個(gè)階段:首先標(biāo)記出所有需要回收的對(duì)象,在標(biāo)記完成后,統(tǒng)一回收掉所有被標(biāo)記的對(duì)象,也可以反過(guò)來(lái),標(biāo)記存活的對(duì)象,統(tǒng)一回收所有未被標(biāo)記的對(duì)象。
? 它的主要缺點(diǎn)有兩個(gè):第一個(gè)是執(zhí)行效率不穩(wěn)定,如果Java堆中包含大量對(duì)象,而且其中大部分是需要被回收的,這時(shí)必須進(jìn)行大量標(biāo)記和清除的動(dòng)作,導(dǎo)致標(biāo)記和清除兩個(gè)過(guò)程的執(zhí)行效率都隨對(duì)象數(shù)量增長(zhǎng)而降低;第二個(gè)是內(nèi)存空間的碎片化問(wèn)題,標(biāo)記、清除之后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會(huì)導(dǎo)致當(dāng)以后在程序運(yùn)行過(guò)程中需要分配較大對(duì)象時(shí)無(wú)法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動(dòng)作。標(biāo)記-清除算法的執(zhí)行過(guò)程如下圖所示。
?
? 標(biāo)記-復(fù)制算法:
? 為了解決標(biāo)記-清除算法面對(duì)大量可回收對(duì)象時(shí)執(zhí)行效率低的問(wèn)題,1969年Fenichel提出了一種稱為“半?yún)^(qū)復(fù)制”(Semispace Copying)的垃圾收集算法,它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對(duì)象復(fù)制到另外一塊上面,然后再把已使用過(guò)的內(nèi)存空間一次清理掉。如果內(nèi)存中多數(shù)對(duì)象都是存活的,這種算法將會(huì)產(chǎn)生大量的內(nèi)存間復(fù)制的開銷,但對(duì)于多數(shù)對(duì)象都是可回收的情況,算法需要復(fù)制的就是占少數(shù)的存活對(duì)象,而且每次都是針對(duì)整個(gè)半?yún)^(qū)進(jìn)行內(nèi)存回收,分配內(nèi)存時(shí)也就不用考慮有空間碎片的復(fù)雜情況,只要移動(dòng)堆頂指針,按順序分配即可。這樣實(shí)現(xiàn)簡(jiǎn)單,運(yùn)行高效,不過(guò)其缺陷也顯而易見,這種復(fù)制回收算法的代價(jià)是將可用內(nèi)存縮小為了原來(lái)的一半,空間浪費(fèi)未免太多了一點(diǎn)。標(biāo)記-復(fù)制算法的執(zhí)行過(guò)程如下圖所示。
?
? 在1989年,Andrew Appel針對(duì)具備“朝生夕滅”特點(diǎn)的對(duì)象,提出了一種更優(yōu)化的半?yún)^(qū)復(fù)制分代策略,現(xiàn)在稱為“Appel式回收”。Appel式回收的具體做法是把新生代分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次分配內(nèi)存只使用Eden和其中一塊Survivor。發(fā)生垃圾搜集時(shí),將Eden和Survivor中仍然存活的對(duì)象一次性復(fù)制到另外一塊Survivor空間上,然后直接清理掉Eden和已用過(guò)的那塊Survivor空間。HotSpot虛擬機(jī)默認(rèn)Eden和Survivor的大小比例是8∶1,也即每次新生代中可用內(nèi)存空間為整個(gè)新生代容量的90%(Eden的80%加上一個(gè)Survivor的10%),只有一個(gè)Survivor空間,即10%的新生代是會(huì)被“浪費(fèi)”的。當(dāng)然,98%的對(duì)象可被回收僅僅是“普通場(chǎng)景”下測(cè)得的數(shù)據(jù),任何人都沒(méi)有辦法百分百保證每次回收都只有不多于10%的對(duì)象存活,因此Appel式回收還有一個(gè)充當(dāng)罕見情況的“逃生門”的安全設(shè)計(jì),當(dāng)Survivor空間不足以容納一次Minor GC之后存活的對(duì)象時(shí),就需要依賴其他內(nèi)存區(qū)域(實(shí)際上大多就是老年代)進(jìn)行分配擔(dān)保(Handle Promotion)。
? 標(biāo)記-整理算法:
? 標(biāo)記-復(fù)制算法在對(duì)象存活率較高時(shí)就要進(jìn)行較多的復(fù)制操作,效率將會(huì)降低。更關(guān)鍵的是,如果不想浪費(fèi)50%的空間,就需要有額外的空間進(jìn)行分配擔(dān)保,以應(yīng)對(duì)被使用的內(nèi)存中所有對(duì)象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。
? 針對(duì)老年代對(duì)象的存亡特征,1974年Edward Lueders提出了另外一種有針對(duì)性的“標(biāo)記-整理”(Mark-Compact)算法,其中的標(biāo)記過(guò)程仍然與“標(biāo)記-清除”算法一樣,但后續(xù)步驟不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有存活的對(duì)象都向內(nèi)存空間一端移動(dòng),然后直接清理掉邊界以外的內(nèi)存,“標(biāo)記-整理”算法的示意圖如下圖所示。
?
16 .請(qǐng)介紹一下分代回收機(jī)制
當(dāng)前商業(yè)虛擬機(jī)的垃圾收集器,大多數(shù)都遵循了“分代收集”(GenerationalCollection)[插圖]的理論進(jìn)行設(shè)計(jì),分代收集名為理論,實(shí)質(zhì)是一套符合大多數(shù)程序運(yùn)行實(shí)際情況的經(jīng)驗(yàn)法則,它建立在兩個(gè)分代假說(shuō)之上:
? 弱分代假說(shuō)(Weak Generational Hypothesis):絕大多數(shù)對(duì)象都是朝生夕滅的。? 強(qiáng)分代假說(shuō)(Strong Generational Hypothesis):熬過(guò)越多次垃圾收集過(guò)程的對(duì)象就越難以消亡。
? 這兩個(gè)分代假說(shuō)共同奠定了多款常用的垃圾收集器的一致的設(shè)計(jì)原則:收集器應(yīng)該將Java堆劃分出不同的區(qū)域,然后將回收對(duì)象依據(jù)其年齡(年齡即對(duì)象熬過(guò)垃圾收集過(guò)程的次數(shù))分配到不同的區(qū)域之中存儲(chǔ)。把分代收集理論具體放到現(xiàn)在的商用Java虛擬機(jī)里,設(shè)計(jì)者一般至少會(huì)把Java堆劃分為新生代(Young Generation)和老年代(Old Generation)兩個(gè)區(qū)域。顧名思義,在新生代中,每次垃圾收集時(shí)都發(fā)現(xiàn)有大批對(duì)象死去,而每次回收后存活的少量對(duì)象,將會(huì)逐步晉升到老年代中存放。
? 分代收集并非只是簡(jiǎn)單劃分一下內(nèi)存區(qū)域那么容易,它至少存在一個(gè)明顯的困難:對(duì)象不是孤立的,對(duì)象之間會(huì)存在跨代引用。假如要現(xiàn)在進(jìn)行一次只局限于新生代區(qū)域內(nèi)的收集,但新生代中的對(duì)象是完全有可能被老年代所引用的,為了找出該區(qū)域中的存活對(duì)象,不得不在固定的GC Roots之外,再額外遍歷整個(gè)老年代中所有對(duì)象來(lái)確??蛇_(dá)性分析結(jié)果的正確性,反過(guò)來(lái)也是一樣。遍歷整個(gè)老年代所有對(duì)象的方案雖然理論上可行,但無(wú)疑會(huì)為內(nèi)存回收帶來(lái)很大的性能負(fù)擔(dān)。為了解決這個(gè)問(wèn)題,就需要對(duì)分代收集理論添加第三條經(jīng)驗(yàn)法則:
? 跨代引用假說(shuō)(Intergenerational Reference Hypothesis):跨代引用相對(duì)于同代引用來(lái)說(shuō)僅占極少數(shù)。
? 依據(jù)這條假說(shuō),我們就不應(yīng)再為了少量的跨代引用去掃描整個(gè)老年代,也不必浪費(fèi)空間專門記錄每一個(gè)對(duì)象是否存在及存在哪些跨代引用,只需在新生代上建立一個(gè)全局的數(shù)據(jù)結(jié)構(gòu)(稱為“記憶集”,RememberedSet),這個(gè)結(jié)構(gòu)把老年代劃分成若干小塊,標(biāo)識(shí)出老年代的哪一塊內(nèi)存會(huì)存在跨代引用。此后當(dāng)發(fā)生Minor GC時(shí),只有包含了跨代引用的小塊內(nèi)存里的對(duì)象才會(huì)被加入到GC Roots進(jìn)行掃描。雖然這種方法需要在對(duì)象改變引用關(guān)系(如將自己或者某個(gè)屬性賦值)時(shí)維護(hù)記錄數(shù)據(jù)的正確性,會(huì)增加一些運(yùn)行時(shí)的開銷,但比起收集時(shí)掃描整個(gè)老年代來(lái)說(shuō)仍然是劃算的。
17 .JVM中一次完整的GC流程是怎樣的?
新創(chuàng)建的對(duì)象一般會(huì)被分配在新生代中,常用的新生代的垃圾回收器是 ParNew 垃圾回收器,它按照 8:1:1 將新生代分成 Eden 區(qū),以及兩個(gè) Survivor 區(qū)。某一時(shí)刻,我們創(chuàng)建的對(duì)象將 Eden 區(qū)全部擠滿,這個(gè)對(duì)象就是擠滿新生代的最后一個(gè)對(duì)象。此時(shí),Minor GC 就觸發(fā)了。
? 在正式 Minor GC 前,JVM 會(huì)先檢查新生代中對(duì)象,是比老年代中剩余空間大還是小。為什么要做這樣的檢查呢?原因很簡(jiǎn)單,假如 Minor GC 之后 Survivor 區(qū)放不下剩余對(duì)象,這些對(duì)象就要進(jìn)入到老年代,所以要提前檢查老年代是不是夠用。這樣就有兩種情況:
? 老年代剩余空間大于新生代中的對(duì)象大小,那就直接Minor GC,GC完survivor不夠放,老年代也絕對(duì)夠放; ? 老年代剩余空間小于新生代中的對(duì)象大小,這個(gè)時(shí)候就要查看是否啟用了“老年代空間分配擔(dān)保規(guī)則”,具體來(lái)說(shuō)就是看 -XX:-HandlePromotionFailure 參數(shù)是否設(shè)置了。 ? 老年代空間分配擔(dān)保規(guī)則是這樣的,如果老年代中剩余空間大小,大于歷次 Minor GC 之后剩余對(duì)象的大小,那就允許進(jìn)行 Minor GC。因?yàn)閺母怕噬蟻?lái)說(shuō),以前的放的下,這次的也應(yīng)該放的下。那就有兩種情況: ? 老年代中剩余空間大小,大于歷次Minor GC之后剩余對(duì)象的大小,進(jìn)行 Minor GC; ? 老年代中剩余空間大小,小于歷次Minor GC之后剩余對(duì)象的大小,進(jìn)行Full GC,把老年代空出來(lái)再檢查。
? 開啟老年代空間分配擔(dān)保規(guī)則只能說(shuō)是大概率上來(lái)說(shuō),Minor GC 剩余后的對(duì)象夠放到老年代,所以當(dāng)然也會(huì)有萬(wàn)一,Minor GC 后會(huì)有這樣三種情況:
? Minor GC 之后的對(duì)象足夠放到 Survivor 區(qū),皆大歡喜,GC 結(jié)束;? Minor GC 之后的對(duì)象不夠放到 Survivor 區(qū),接著進(jìn)入到老年代,老年代能放下,那也可以,GC 結(jié)束;? Minor GC 之后的對(duì)象不夠放到 Survivor 區(qū),老年代也放不下,那就只能 Full GC。
? 前面都是成功 GC 的例子,還有 3 中情況,會(huì)導(dǎo)致 GC 失敗,報(bào) OOM:
? 緊接上一節(jié) Full GC 之后,老年代任然放不下剩余對(duì)象,就只能 OOM;? 未開啟老年代分配擔(dān)保機(jī)制,且一次 Full GC 后,老年代任然放不下剩余對(duì)象,也只能 OOM;? 開啟老年代分配擔(dān)保機(jī)制,但是擔(dān)保不通過(guò),一次 Full GC 后,老年代任然放不下剩余對(duì)象,也是能 OOM。
18 .Full GC會(huì)導(dǎo)致什么?
Full GC會(huì)“Stop The World”,即在GC期間全程暫停用戶的應(yīng)用程序。
柚子快報(bào)激活碼778899分享:JVM學(xué)習(xí)筆記(一)
精彩鏈接
本文內(nèi)容根據(jù)網(wǎng)絡(luò)資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點(diǎn)和立場(chǎng)。
轉(zhuǎn)載請(qǐng)注明,如有侵權(quán),聯(lián)系刪除。