<<深入理解JVM>>筆記與一些體悟


這段時(shí)間這本書(shū)的第二遍已經(jīng)看完了,但是很多地方模糊不清楚,總的來(lái)說(shuō),這邊書(shū)收獲很大,作為Java開(kāi)發(fā),這本書(shū)很有幫助,本來(lái)準(zhǔn)備進(jìn)軍多線程的,因此而耽擱一段時(shí)間,總之感覺(jué)還是把這本書(shū)一些章節(jié)吃透,再去多線程

第一章 走進(jìn)Java(歷史與展望)

展望Java技術(shù)的未來(lái):從目前國(guó)內(nèi)形勢(shì)來(lái)看,go-lang在逐漸走上臺(tái)面,php也同時(shí)占有一席之地
PYPL排行榜也是一個(gè)關(guān)于編程語(yǔ)言流行度的參考指標(biāo),其榜單數(shù)據(jù)的排名均是根據(jù)榜單對(duì)象在 Google 上相關(guān)的搜索頻率進(jìn)行統(tǒng)計(jì)排名,原始數(shù)據(jù)來(lái)自 Google Trends,也就是說(shuō)某項(xiàng)語(yǔ)言或者某款 IDE 在 Google 上搜索頻率越高,表示它越受歡迎。上面這份排行是基于google搜索次數(shù)決定的



TIOBE編程社區(qū)索引是編程語(yǔ)言流行程度的一個(gè)指標(biāo)。索引每月更新一次。評(píng)級(jí)是基于全球熟練工程師、課程和第三方供應(yīng)商的數(shù)量。流行的搜索引擎,如谷歌,必應(yīng),雅虎!,維基百科,亞馬遜,YouTube和百度被用來(lái)計(jì)算收視率。需要注意的是,TIOBE索引并不是關(guān)于最好的編程語(yǔ)言,也不是大多數(shù)代碼都是用哪種語(yǔ)言編寫的。索引可用于檢查您的編程技能是否仍然是最新的,或者在開(kāi)始構(gòu)建新的軟件系統(tǒng)時(shí),對(duì)應(yīng)采用何種編程語(yǔ)言作出戰(zhàn)略決策

Java從當(dāng)初的一次編寫到處運(yùn)行 ,到未來(lái)的期望無(wú)語(yǔ)言傾向,感覺(jué)面臨著巨大的挑戰(zhàn),畢竟現(xiàn)在還是天下第一
Java的優(yōu)勢(shì):
1.龐大的用戶群體。
2.穩(wěn)定的語(yǔ)言,使得項(xiàng)目更正規(guī),更容易形成大型體系的工程。
缺點(diǎn):
1.泛型那里很不好用,Java采用了類型擦除方式。使得使用泛型會(huì)造成大量的自動(dòng)拆箱、裝箱,使得泛型速度變慢。
2.啟動(dòng)Java虛擬機(jī)的時(shí)候,還是太長(zhǎng)了,不如一些動(dòng)態(tài)類型語(yǔ)言來(lái)的開(kāi)發(fā)效率高,對(duì)編程人員,用戶也友好。

正式進(jìn)入本書(shū):
一、無(wú)語(yǔ)言傾向
2018年4月,Oracle Labs新公開(kāi)了一項(xiàng)黑科技:Graal VM,這是一個(gè)在Hotspot虛擬機(jī)基礎(chǔ)上增強(qiáng)而成的跨語(yǔ)言全棧虛擬機(jī),可以作為“任何語(yǔ)言”的運(yùn)行平臺(tái),既包括Java、Scala、Groovy等基于Java虛擬機(jī)的語(yǔ)言,還包括C、C++、Rust等基于LLVM的語(yǔ)言,同時(shí)也支持,Javascript、Python和R語(yǔ)言等。Graal VM 可以無(wú)額外開(kāi)銷地混合使用這些編程語(yǔ)言,支持不同語(yǔ)言中混用對(duì)方的接口和對(duì)象,也能夠支持這些語(yǔ)言使用已經(jīng)編寫好地本地庫(kù)文件。它的基本工作原理:將這些語(yǔ)言的源代碼或者源代碼編譯后的中間格式(例如LLVM字節(jié)碼)通過(guò)解釋器轉(zhuǎn)換為能被Graal VM接受的中間表示,例如設(shè)計(jì)一個(gè)解釋器專門對(duì)LLVM輸出的字節(jié)碼進(jìn)行轉(zhuǎn)換來(lái)支持C語(yǔ)言。(Truffle快速構(gòu)建面向一種新語(yǔ)言的解釋器。)Graal VM才是真正意義上的與物理計(jì)算機(jī)相對(duì)應(yīng)的高級(jí)語(yǔ)言虛擬機(jī),理由是它與物理硬件的指令集一樣,做到了只與機(jī)器特性相關(guān)而不與某種高級(jí)語(yǔ)言特性相關(guān)。
Graal VM相比于Hotspot 主要差異在于即時(shí)編譯器,相比較起來(lái)互有勝負(fù),但是Oracle Labs和美國(guó)大學(xué)里所做的最新即時(shí)編譯技術(shù)的研究全部都遷移到基于Graal VM之上進(jìn)行了,令人期待。

二、新一代即時(shí)編譯器
Hotspot 虛擬機(jī)中含有兩個(gè)即時(shí)編譯器,分別是編譯耗時(shí)短但是輸出代碼優(yōu)化程度較低的客戶端編譯器(C1)以及編譯耗時(shí)長(zhǎng)但輸出代碼優(yōu)化質(zhì)量也更高的服務(wù)端編譯器(C2),通常他們會(huì)在分層編譯機(jī)制下與解釋器互相配合來(lái)共同構(gòu)成Hotspot虛擬機(jī)的執(zhí)行子系統(tǒng)。
Graal 編譯器(作為C2編譯器替代者),C2時(shí)間太長(zhǎng)了,其作者都因?yàn)樘珡?fù)雜而不愿意維護(hù),且用C++編寫而成。Graal編譯器本身就是Java語(yǔ)言寫成,且C2編譯器代碼可以輕松移植到Graal編譯器上,不成熟,需要通過(guò)-XX:+UnlockExperimentalVMOptions -XX:UseJVMCICompiler參數(shù)來(lái)開(kāi)啟。

三、向native邁進(jìn)
Java自身存在缺點(diǎn),主要是近幾年在從大型單體應(yīng)用架構(gòu)向小型微服務(wù)架構(gòu)發(fā)展的技術(shù)潮流之下,Java表現(xiàn)的不適應(yīng)。(沒(méi)看過(guò)微服務(wù)所以詳細(xì)寫一下)在微服務(wù)架構(gòu)視角下,應(yīng)用拆分后,單個(gè)微服務(wù)不在需要面隊(duì)數(shù)十、數(shù)百GB乃至TB的內(nèi)存,有了高可用的服務(wù)集群,也無(wú)需追求單個(gè)服務(wù)7*24小時(shí)運(yùn)行,隨時(shí)中斷和更新;但是Java的啟動(dòng)時(shí)間較長(zhǎng),需要時(shí)間才能到達(dá)最高性能,就和這些場(chǎng)景有點(diǎn)矛盾。在無(wú)服務(wù)架構(gòu)下,矛盾會(huì)更大。
AppCDS 允許把加載解析的類型信息緩存起來(lái),從而提升下次啟動(dòng)速度。提前編譯能帶來(lái)的最大好處是Java虛擬機(jī)加載這些預(yù)編譯成二進(jìn)制庫(kù)之后能直接調(diào)用,無(wú)需等待即時(shí)編譯器在運(yùn)行是將其編譯成二進(jìn)制機(jī)器碼,理論上,提前編譯可以減少即時(shí)編譯帶來(lái)的預(yù)熱時(shí)間,減少Java長(zhǎng)期給人帶來(lái)的第一次運(yùn)行慢的不良體驗(yàn)。但是壞處也很明顯,必須為不同的硬件、操作系統(tǒng)去編譯對(duì)應(yīng)的發(fā)行包;降低Java連接過(guò)程的動(dòng)態(tài)性,必須要求加載的代碼在編譯期全部已知,而不能在運(yùn)行期才確定否則只能舍棄以及編譯好的,退回即時(shí)編譯狀態(tài)。
SubStrate VM出現(xiàn),一個(gè)極小的運(yùn)行時(shí)環(huán)境,包括了獨(dú)立的異常處理、同步調(diào)度、線程管理、內(nèi)存管理和JNI訪問(wèn),目標(biāo)是代替Hotspot用來(lái)支持提前編譯后的程序運(yùn)行,無(wú)需重復(fù)開(kāi)啟Java虛擬機(jī)初始化過(guò)程,不能動(dòng)態(tài)加載其他編譯器不可知的代碼和類庫(kù)。好處就是顯著降低內(nèi)存占用和啟動(dòng)時(shí)間,運(yùn)行在Substrate VM上的小規(guī)模應(yīng)用,其內(nèi)存占用和啟動(dòng)時(shí)間比Hotspot下降5-50倍。

四、Java語(yǔ)法糖持續(xù)變多,給編程人員提供良好的體驗(yàn)
結(jié)束~
第一部分 自動(dòng)內(nèi)存管理 援引作者一句話Java與C++之間由內(nèi)存動(dòng)態(tài)分配和垃圾收集技術(shù)所圍成的高墻,墻外面的人想進(jìn)去,墻里面的人卻想出來(lái)

第二章 Java內(nèi)存區(qū)域與內(nèi)存溢出異常

1. 運(yùn)行時(shí)數(shù)據(jù)區(qū)域
共包含:方法區(qū)(線程公有)、Java堆(線程公有)、Java虛擬機(jī)棧(線程私有)、本地方法棧(線程私有)、程序計(jì)數(shù)器(線程私有)、運(yùn)行時(shí)常量池(方法區(qū)一部分)、直接內(nèi)存(不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是《Java虛擬機(jī)規(guī)范》中定義的區(qū)域。
1.1 程序計(jì)數(shù)器
1.字節(jié)碼解釋器工作時(shí)就是通過(guò)改變這個(gè)計(jì)數(shù)器的值來(lái)選取下一條執(zhí)行的字節(jié)碼指令,是程序控制流的指示器,線程恢復(fù)也需要依賴程序計(jì)數(shù)器。
2.Java虛擬機(jī)多線程通過(guò)線程切換、分配處理器執(zhí)行時(shí)間的方式實(shí)現(xiàn),一個(gè)確定的時(shí)刻一個(gè)處理器只會(huì)執(zhí)行一條線程中的指令,因此每個(gè)線程各自擁有自己的程序計(jì)數(shù)器
3.如果執(zhí)行Java方法則計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址,如果執(zhí)行的是native方法,則計(jì)數(shù)器值為空
1.2 虛擬機(jī)棧
1.其線程私有、它的生命周期與線程相同;每個(gè)方法被執(zhí)行時(shí),Java虛擬機(jī)都會(huì)創(chuàng)建一個(gè)棧幀用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等信息。每一個(gè)方法被調(diào)用直至執(zhí)行完畢,就對(duì)應(yīng)一個(gè)棧幀在虛擬機(jī)棧中從入棧到出棧的過(guò)程。
2.Java內(nèi)存區(qū)域,程序員最關(guān)心兩個(gè)區(qū)域堆(heap)和棧(stack),棧指的是虛擬機(jī)棧或者虛擬機(jī)棧中的局部變量表部分。
3.局部變量表存放了基本數(shù)據(jù)類型(boolean、byte、char、short、int、float、long、double)、引用類型(reference)和returnAddress類型。
4.局部變量表的存儲(chǔ)空間以局部變量槽slot表示,long 和 double都占用2個(gè)slot,其余占用一個(gè),局部變量表所需的內(nèi)存空間在編譯期就完成,在方法的運(yùn)行時(shí)期不會(huì)改變局部變量表大小
5.如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度,拋出StackOverFlow異常;如果Java虛擬機(jī)棧容量可以動(dòng)態(tài)擴(kuò)展、當(dāng)棧擴(kuò)展時(shí)無(wú)法申請(qǐng)到足夠的內(nèi)存會(huì)拋出OutOfMemory異常。
1.3 本地方法棧
1.和虛擬機(jī)棧發(fā)揮的作用類似,區(qū)別就是虛擬機(jī)棧為執(zhí)行Java方法服務(wù),本地方法棧則為虛擬機(jī)棧用到的Native方法服務(wù)。
2.異常類同虛擬機(jī)棧
1.4 Java堆
1.堆中只存儲(chǔ)對(duì)象實(shí)例。
2.Java是垃圾收集器管理的內(nèi)存區(qū)域,從分配內(nèi)存的角度看,所有線程共享的Java堆可以劃分出多個(gè)線程私有的分配緩沖區(qū),用來(lái)提升對(duì)象分配效率。Java堆的劃分只有一個(gè)目的就是,更好的分配內(nèi)存,更好的回收內(nèi)存 。
3.堆里面不要求物理上連續(xù)存儲(chǔ),但邏輯上是連續(xù)的,對(duì)于大對(duì)象(典型的數(shù)組對(duì)象)很可能要求連續(xù)的內(nèi)存空間。
4.如果在Java堆中沒(méi)有內(nèi)存完成實(shí)例分配,并且堆無(wú)法再擴(kuò)展時(shí),拋出OutOfMemoryError(OOM)。
1.5 方法區(qū)
1.用于存儲(chǔ)已經(jīng)被虛擬機(jī)加載類型信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼緩存等數(shù)據(jù)。
2.JDK8完全廢除永久代,采用本地內(nèi)存來(lái)實(shí)現(xiàn)方法區(qū),使用元空間
3.這個(gè)區(qū)域內(nèi)存回收的目標(biāo)是:常量池的回收和對(duì)類型的卸載。(比較難實(shí)現(xiàn))
4.如果方法區(qū)無(wú)法滿足新的內(nèi)存分配需求時(shí),拋出OOM。
1.5 運(yùn)行時(shí)常量池
1.作為方法區(qū)的一部分,常量池表,用于存放編譯期生成的各種字面量與符號(hào)引用,這部分內(nèi)容將在類加載后放在運(yùn)行時(shí)常量池中??赡芤矔?huì)存儲(chǔ)直接引用
2.具備動(dòng)態(tài)性,Java并不要求常量一定要編譯期才可以產(chǎn)生例如String類的intern()方法。
3.當(dāng)常量池?zé)o法申請(qǐng)到內(nèi)存時(shí)拋出OOM
1.6 直接內(nèi)存
1.不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是《Java虛擬機(jī)規(guī)范》中定義的區(qū)域。
2.JDK1.4 新加入的NIO,引入了一種基于通道channel與緩沖區(qū)BUffer的I/O方式,使用Native函數(shù)直接分配堆外內(nèi)存,通過(guò)存儲(chǔ)在Java堆里面的DirectByteBUffer對(duì)象作為這塊區(qū)域的引用進(jìn)行操作,避免了Java堆和Native堆中來(lái)回復(fù)制數(shù)據(jù)。
3.本機(jī)直接內(nèi)存分配不受到Java堆大小的限制,但收到總內(nèi)存限制,一般服務(wù)器管理員配置虛擬機(jī)參數(shù)時(shí),忽略直接內(nèi)存,使得各內(nèi)存區(qū)域總和大于物理內(nèi)存限制,導(dǎo)致OOM異常。
2. 對(duì)象的一生
Hotspot虛擬機(jī)在Java堆中對(duì)象分配、布局和訪問(wèn)的全過(guò)程。
2.1 對(duì)象的創(chuàng)建
1.當(dāng)虛擬機(jī)遇到一條字節(jié)碼new指令時(shí),首先去檢查這個(gè)指令的參數(shù)是否能在常量池中定位到一個(gè)類的符號(hào)引用,并且檢查到這個(gè)符號(hào)引用代表的類是否已經(jīng)被加載、解析和初始化過(guò),如果沒(méi)有先執(zhí)行相應(yīng)的類加載。
2.類加載檢查通過(guò)后,給對(duì)象分配內(nèi)存:
(1)指針碰撞:假設(shè)Java堆中內(nèi)存是絕對(duì)規(guī)整的,所有被用過(guò)的內(nèi)存放一邊,空閑的內(nèi)存放一邊,中間放一個(gè)指針作為分界點(diǎn)的指示器,那所分配內(nèi)存就是僅僅把那個(gè)指針向空閑空間方向挪動(dòng)一段與對(duì)象大小相等的距離。
(2)空閑列表:如果不規(guī)整,就必須維護(hù)一個(gè)空閑列表,記錄哪塊內(nèi)存可以用,分配的時(shí)候從列表中找到足夠大的一塊空間分給對(duì)象實(shí)例,并更新記錄。
選擇哪種方式由Java堆是否規(guī)整來(lái)決定,而Java堆是否規(guī)整,又由所采用的垃圾收集器是否帶有空間壓縮整理決定。因此,使用Serial、Parnew等帶壓縮整理的收集器,采用指針碰撞;采用CMS基于清楚算法的收集器時(shí),理論上采用空閑列表實(shí)現(xiàn)。
還需要考慮一個(gè)問(wèn)題,對(duì)象創(chuàng)建是很頻繁的行為,僅僅修改一個(gè)指針?biāo)赶虻奈恢?,并發(fā)情況下并不是線程安全的,可能出現(xiàn)給A分配內(nèi)存、指針沒(méi)修改,對(duì)象B又使用原來(lái)指針?lè)峙鋬?nèi)存,解決方法:
(1)對(duì)分配內(nèi)存空間進(jìn)行同步處理——實(shí)際上虛擬機(jī)采用CAS配上失敗重試的方式保證操作的原子性
CAS看這里CAS
(2)另外一種是把內(nèi)存分配的動(dòng)作按照線程劃分在不同空間中進(jìn)行,即每個(gè)線程在Java堆中預(yù)先分配一小塊內(nèi)存,稱為本地線程分配緩沖(TLAB),哪個(gè)線程要分配內(nèi)存,就在哪個(gè)線程的本地緩沖區(qū)分配,只有本地緩沖區(qū)用完了,分配新的緩沖區(qū)才需要同步鎖定。
3.內(nèi)存分配完成后,虛擬機(jī)將分配到的內(nèi)存空間(但不包括對(duì)象頭)都初始化為0,這步操作保證了對(duì)象的實(shí)例字段在Java代碼中不賦初值就可以使用,使程序能訪問(wèn)到這些字段的數(shù)據(jù)類型所對(duì)應(yīng)的零值。
4.接下來(lái),對(duì)對(duì)象進(jìn)行必要的設(shè)置,例如這個(gè)對(duì)象是哪個(gè)類的實(shí)例,、如何才能找到類的元數(shù)據(jù)信息、對(duì)象的哈希碼、對(duì)象的GC年齡分帶,存儲(chǔ)在對(duì)象頭之中
5.構(gòu)造函數(shù),即CLass中的<init>()方法,按照程序員自己的意愿進(jìn)行初始化,這樣一個(gè)對(duì)象才被完整構(gòu)建出來(lái)
2.2 對(duì)象的內(nèi)存布局
1.對(duì)象在堆內(nèi)存中可以劃分為三個(gè)部分:對(duì)象頭、實(shí)例數(shù)據(jù)、對(duì)齊填充
2.對(duì)象頭:MarkWord 如:哈希碼、GC年齡分帶、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID等,另外一部分就是類型指針,即對(duì)象指向它的類型數(shù)據(jù)的指針,虛擬機(jī)需要通過(guò)這個(gè)指針來(lái)確定該對(duì)象是哪個(gè)類的實(shí)例。如果對(duì)象是Java數(shù)組,還要存儲(chǔ)一塊這個(gè)對(duì)象多大,記錄長(zhǎng)度。數(shù)組大小不確定,虛擬機(jī)無(wú)法通過(guò)元數(shù)據(jù)確定數(shù)組大小。
3.實(shí)例數(shù)據(jù):是對(duì)象真正存儲(chǔ)的有效信息
4.對(duì)齊填充:因?yàn)镠otspot要求對(duì)象起始地址必須是8字節(jié)的整數(shù)倍,換句話說(shuō)就是任何對(duì)象大小必須是8字節(jié)的整數(shù)倍,所以需要對(duì)齊填充。
2.3 對(duì)象的訪問(wèn)定位
1.主流方式使用句柄和直接指針兩種
(1)使用句柄的化,Java堆中可能劃分一塊內(nèi)存作為句柄池,reference中存儲(chǔ)的就是對(duì)象的句柄地址,而句柄中包含了對(duì)象實(shí)例數(shù)據(jù)與類型實(shí)例數(shù)據(jù)各自具體的地址信息。
優(yōu)勢(shì):reference中存儲(chǔ)的是穩(wěn)定的句柄地址,在對(duì)象移動(dòng)(垃圾收集)的時(shí)候,只會(huì)改變句柄中實(shí)例數(shù)據(jù)指針,而reference本身不要修改。
(2)使用直接指針的化,Java堆中對(duì)象的內(nèi)存布局就必須要考慮如何放置訪問(wèn)類型數(shù)據(jù)的相關(guān)信息,reference中存儲(chǔ)的直接就是對(duì)象地址,如果訪問(wèn)對(duì)象本身的化,就不需多一次間接訪問(wèn)的開(kāi)銷。
優(yōu)勢(shì):速度快,節(jié)省一次指針定位的時(shí)間開(kāi)銷,(Hotspot)中就用直接指針。

3. 實(shí)戰(zhàn):OutOfMemoryError異常

第三章 垃圾收集器與內(nèi)存分配策略

1)垃圾回收出現(xiàn)的原因:前面一章,分析了,JVM哪些地方會(huì)出現(xiàn)OOM異常,這章介紹Java垃圾收集器為了避免內(nèi)存溢出異常都做了哪些努力。
2)為什么學(xué)習(xí)垃圾回收?當(dāng)排查各種內(nèi)存溢出、內(nèi)存泄漏問(wèn)題時(shí),當(dāng)垃圾收集成為系統(tǒng)達(dá)到更高并發(fā)量的瓶頸時(shí),我們就必須對(duì)這些“自動(dòng)化技術(shù)”的技術(shù)實(shí)施必要的監(jiān)控和調(diào)節(jié)。
3)哪些內(nèi)存需要回收?程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法棧隨線程而生,隨線程消滅,棧中的棧幀隨著方法的進(jìn)入和退出有條不紊的執(zhí)行著入棧和出棧操作。并且每一個(gè)棧幀分配多少內(nèi)存是確定下來(lái)的,大體上是編譯器已知的,因此這幾個(gè)區(qū)域回收都具有確定性,當(dāng)方法結(jié)束或者線程結(jié)束就不需要考慮太多問(wèn)題。所以回收主要面向Java堆方法區(qū)。

  1. 對(duì)象已死?
    1)引用計(jì)數(shù)法:對(duì)象中添加一個(gè)引用計(jì)數(shù)器,如果引用+1,引用失效-1,任何時(shí)刻引用為零的對(duì)象就是不可能再被使用的。但是有些問(wèn)題無(wú)法解決,例如循環(huán)引用問(wèn)題。
    2)可達(dá)性分析算法:用GC Roots作為根對(duì)象,根據(jù)引用關(guān)系向下搜索,對(duì)象不可達(dá),則對(duì)象不在使用。固定作為GC Roots的對(duì)象包括以下幾種,
    1.在虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象,比如各個(gè)線程被調(diào)用的方法堆棧中用到的參數(shù)、局部變量、臨時(shí)變量。
    2.在方法區(qū)中類靜態(tài)屬性引用的對(duì)象,比如Java類的引用類型靜態(tài)變量。
    3.方法區(qū)中常量引用的對(duì)象。
    4.本地方法棧中JNI引用的對(duì)象。
    5.基本數(shù)據(jù)類型對(duì)應(yīng)的Class對(duì)象,系統(tǒng)類加載器。
    6.所有被同步鎖(synchronized關(guān)鍵字)持有的對(duì)象。
    7.反應(yīng)Java虛擬機(jī)內(nèi)部情況地JMXBean、JVMTI中注冊(cè)的回調(diào)、本地代碼緩存等。
    備注:可能會(huì)有臨時(shí)性加入,做局部回收的時(shí)候,某個(gè)區(qū)域內(nèi)的對(duì)象完全有可能被位于堆中的其他區(qū)域引用,這時(shí)就需要將這些關(guān)聯(lián)區(qū)域?qū)ο笠徊⒓尤隚C Roots集合中去,才能保證可達(dá)性分析的正確。
    3)四種引用關(guān)系的出現(xiàn)
    出現(xiàn)原因:當(dāng)內(nèi)存空間還足夠時(shí),能保留在內(nèi)存中,如果內(nèi)存空間再進(jìn)行垃圾收集后仍然非常緊張,那就可以拋棄這些對(duì)象——系統(tǒng)緩存
    1.強(qiáng)引用:Object obj = new Object() 這種引用關(guān)系。只要強(qiáng)引用在,垃圾收集器就永遠(yuǎn)不會(huì)回收掉被引用對(duì)象。
    2.軟引用:軟引用用來(lái)描述一些還有用、非必須的對(duì)象,只要被軟引用關(guān)聯(lián)著的對(duì)象,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常前,會(huì)把這些對(duì)象列進(jìn)回收范圍進(jìn)行第二次回收,SoftReference
    3.弱引用:用來(lái)描述那些非必須的對(duì)象,但是它強(qiáng)度比軟引用更弱一點(diǎn),被弱引用關(guān)聯(lián)的對(duì)象只能存活到下一次垃圾收集之前,垃圾收集器開(kāi)始工作,都會(huì)回收,WeakReference
    4.虛引用:一個(gè)對(duì)象是否有虛引用,完全不會(huì)對(duì)其生存時(shí)間構(gòu)成影響,也無(wú)法通過(guò)虛引用來(lái)取得一個(gè)對(duì)象實(shí)例,唯一目的就是:為了能在這個(gè)對(duì)象被收集器回收時(shí)收到一個(gè)系統(tǒng)通知,PhantomReference。
    4)回收方法區(qū)
    回收內(nèi)容:廢棄的常量和不再使用的類型
    判讀類型是否不再被使用(同時(shí)滿足以下條件):
    1.該類中所有實(shí)例都被回收
    2.加載該類的類加載器已經(jīng)被回收,除非是精心設(shè)計(jì)過(guò)的可替換的類加載器,如OSGI,JSP等,否則很難達(dá)成
    3.該類對(duì)應(yīng)的對(duì)象沒(méi)有在任何地方被引用過(guò),無(wú)法在任何地方通過(guò)反射訪問(wèn)該類
    備注:在大量使用反射、動(dòng)態(tài)代理、CGLib等字節(jié)碼框架,動(dòng)態(tài)生成JSP以及OSGI這類頻繁自定義類加載器的場(chǎng)景中,通常都需要具備類型卸載的能力,以保證不會(huì)造成過(guò)大的內(nèi)存壓力。
  2. 垃圾收集算法
    首先分為:引用計(jì)數(shù)式垃圾收集和追蹤式垃圾收集也被稱為直接垃圾收集和簡(jiǎn)接垃圾收集,Java主要用追蹤式垃圾收集。
    1)分代收集理論
    弱分代假說(shuō):絕大多數(shù)對(duì)象都是朝生夕滅的。
    強(qiáng)分代假說(shuō):熬過(guò)越多次垃圾收集過(guò)程的對(duì)象就越難以消亡。
    跨代引用假說(shuō):跨代引用相對(duì)于同代引用來(lái)說(shuō)僅占極少數(shù)。
    設(shè)計(jì)原則:收集器應(yīng)該將Java堆劃分出不同的區(qū)域,然后回收對(duì)象依據(jù)其年齡(年齡即對(duì)象熬過(guò)垃圾收集過(guò)程的次數(shù))分配到不同的區(qū)域之中存儲(chǔ)。顯而易見(jiàn),如果一個(gè)區(qū)域中大多數(shù)對(duì)象都是朝生夕滅,難以熬過(guò)垃圾收集過(guò)程那么把它們集中放在儀器,每次回收只關(guān)注如何保留少量存活等。Java堆分區(qū)之后,才有了Minor GC、Major GC、Full GC這樣的回收類型的劃分才會(huì)有按照存亡特征相匹配的算法,“標(biāo)記-復(fù)制、標(biāo)記-清除、標(biāo)記-整理算法。但是有很大的困難,例如跨代引用。針對(duì)跨代引用,完全掃描不合適,在新生代建立一個(gè)記憶集,Remembered Set,這個(gè)結(jié)構(gòu)把老年代劃分成若干小塊,引用的小塊里的對(duì)象才會(huì)被加入到GC Roots進(jìn)行掃描,雖然賦值時(shí)會(huì)增加開(kāi)銷,但是劃算的。
    2)標(biāo)記清除算法(三種算法比較了解,不做過(guò)多贅述)
    3)標(biāo)記復(fù)制算法
    4)標(biāo)記整理算法(老年代)
    備注:是否移動(dòng)回收后的存活對(duì)象是一項(xiàng)優(yōu)缺點(diǎn)并存的風(fēng)險(xiǎn):如果移動(dòng)對(duì)象,老年代這種大量對(duì)象存活,移動(dòng)就是一種比較復(fù)雜的過(guò)程,移動(dòng)對(duì)象時(shí)必須暫停用戶線程,stop the world,但是如果按照標(biāo)記清除那樣子考慮,就會(huì)產(chǎn)生空間碎片問(wèn)題,所以移動(dòng)對(duì)象與否都會(huì)有問(wèn)題,移動(dòng)內(nèi)存回收會(huì)更復(fù)雜,不移動(dòng)則內(nèi)存分配時(shí)更復(fù)雜,從停頓時(shí)間來(lái)看,不移動(dòng)停頓時(shí)間更短,但從吞吐量上來(lái)看,移動(dòng)會(huì)劃算,因?yàn)閮?nèi)存分配和訪問(wèn)垃圾收集頻率比要高的多,這部分耗時(shí)增加,總吞吐量下降。如果關(guān)注吞吐量,Parallel Scavenge收集器基于整理算法,關(guān)注延遲的則基于清除算法CMS。
  3. HotSpot的算法細(xì)節(jié)實(shí)現(xiàn)
    1)根節(jié)點(diǎn)枚舉
    1.查找能作為GC Roots的引用,迄今為止所有收集器在根節(jié)點(diǎn)枚舉這一步都必須暫停用戶線程,根結(jié)點(diǎn)枚舉始終必須在一個(gè)能保障一致性的快照中才得以進(jìn)行,一致性就是在某個(gè)時(shí)間點(diǎn)停下來(lái),原因是不暫停程序,根節(jié)點(diǎn)集合的對(duì)象引用關(guān)系還在不斷變化。
    2.算法產(chǎn)生原因:當(dāng)用戶線程暫停下來(lái)之后,其實(shí)并不需要一個(gè)不漏的檢查上下文和全局引用的位置,虛擬機(jī)通過(guò)OopMap的數(shù)據(jù)結(jié)構(gòu)來(lái)知道直接哪些地方存著對(duì)象引用。
    3.當(dāng)虛擬機(jī)加載完成時(shí),即時(shí)編譯過(guò)程中,也會(huì)在特定的位置記錄下棧里和寄存器哪些位置是引用,所以直接掃描。
    2)安全點(diǎn)
    1.原因:導(dǎo)致OopMap內(nèi)容變化的指令非常多,如果為每一條指令都生成對(duì)應(yīng)的OopMap,那將需要大量的額外存儲(chǔ)空間。
    2.在特定位置記錄了這些信息,這些位置被稱為安全點(diǎn),也就是決定了用戶程序執(zhí)行時(shí)并非在代碼指令流的任意位置都能停頓下來(lái)開(kāi)始垃圾收集,而強(qiáng)制要求到達(dá)安全點(diǎn)開(kāi)始收集。
    3.選定安全點(diǎn)的標(biāo)準(zhǔn)是,是否具有讓程序長(zhǎng)時(shí)間執(zhí)行的特征。最明顯的特征是指令序列的復(fù)用,例如方法調(diào)用、循環(huán)跳轉(zhuǎn)、異常跳轉(zhuǎn)等。所以只有這些功能的指令才會(huì)產(chǎn)生安全點(diǎn)。
    問(wèn)題:如何讓垃圾收集器發(fā)生時(shí)讓所有線程(不包括JNI——native)都跑到最近的安全點(diǎn),然后停頓下來(lái)。
    方案:搶先式中斷和主動(dòng)式中斷
    搶先式中斷:不需要線程的執(zhí)行代碼主動(dòng)去配合,在垃圾收集發(fā)生時(shí),系統(tǒng)首先把所有用戶線程全部中斷,如果發(fā)現(xiàn)有用戶線程中斷的地方不在安全點(diǎn)上,就恢復(fù)這條線程執(zhí)行,讓它跑到安全點(diǎn)上再中斷。
    主動(dòng)式中斷:當(dāng)垃圾收集器需要中斷線程時(shí),不直接對(duì)線程操作,僅僅簡(jiǎn)單在未來(lái)設(shè)置一個(gè)標(biāo)志位,各個(gè)線程執(zhí)行過(guò)程中會(huì)不停主動(dòng)去輪詢(輪詢(Polling)是一種CPU決策如何提供周邊設(shè)備服務(wù)的方式。輪詢法的概念是:由CPU定時(shí)發(fā)出詢問(wèn),依序詢問(wèn)每一個(gè)周邊設(shè)備是否需要其服務(wù),有即給予服務(wù),服務(wù)結(jié)束后再問(wèn)下一個(gè)周邊,接著不斷周而復(fù)始。)這個(gè)標(biāo)志,一旦發(fā)現(xiàn)中斷標(biāo)志為真就自己在最近的安全點(diǎn)上主動(dòng)中斷掛起。(Hotspot實(shí)現(xiàn))
    3)安全區(qū)域
    1.原因:當(dāng)程序不執(zhí)行的時(shí)候,不執(zhí)行就是沒(méi)有分配處理器時(shí)間,典型的場(chǎng)景便是用戶線程處于Sleep和Blocked狀態(tài),這時(shí)用戶線程就無(wú)法響應(yīng)虛擬機(jī)的中斷請(qǐng)求,所以引入“安全區(qū)域”。
    2.安全區(qū)域是指能夠確保在某一段代碼片段中,引用關(guān)系不會(huì)發(fā)生變化,因此,在這個(gè)區(qū)域中任意地方開(kāi)始垃圾收集都是安全的。當(dāng)用戶線程執(zhí)行到安全區(qū)域里面的代碼時(shí),首先標(biāo)識(shí)自己進(jìn)入安全區(qū)域,當(dāng)這段時(shí)間里虛擬機(jī)要發(fā)起垃圾收集時(shí)就不必去管這些已經(jīng)聲明自己在安全區(qū)域內(nèi)的線程了。當(dāng)離開(kāi)安全區(qū)域時(shí),它要檢查虛擬機(jī)是否已經(jīng)完成了根節(jié)點(diǎn)枚舉(或者其他需要暫停用戶線程的行為),如果完成了,那線程就當(dāng)沒(méi)事發(fā)生過(guò),繼續(xù)執(zhí)行否則它就必須一直等待,直到收到可以離開(kāi)安全區(qū)域的信號(hào)為止。
    4)記憶集與卡表
    1.原因:解決對(duì)象跨代引用所帶來(lái)的問(wèn)題。
    2.垃圾收集器在新生代建立記憶集,以避免把整個(gè)老年代加入GC Roots掃描范圍
    3.記憶集是一種用于記錄從非收集區(qū)域指向收集區(qū)域的指針集合的抽象數(shù)據(jù)結(jié)構(gòu)。
    4.列舉一些可供選擇的記錄精度:
    字長(zhǎng)精度:每個(gè)記錄精確到一個(gè)機(jī)器字長(zhǎng)(就是處理器的尋址位數(shù),如32位/64位,這個(gè)精度決定了機(jī)器訪問(wèn)物理內(nèi)存地址的指針長(zhǎng)度),該字包含跨代指針。
    對(duì)象精度:每個(gè)記錄精確到一個(gè)對(duì)象,該對(duì)象里有字段含有跨代指針。
    卡精度:每個(gè)記錄精確到一塊內(nèi)存區(qū)域,該區(qū)域內(nèi)有對(duì)象含有跨代指針。
    第三種“卡精度”所指的就是用一種稱為“卡表”的方式去實(shí)現(xiàn)記憶集。它定義了記憶集的記錄精度、與堆內(nèi)存的映射關(guān)系等??ū碜詈?jiǎn)單的形式可以只是一個(gè)字節(jié)數(shù)組,字節(jié)數(shù)組的每一個(gè)元素都對(duì)應(yīng)著其標(biāo)識(shí)的內(nèi)存區(qū)域中的一塊特定大小的內(nèi)存塊,這個(gè)內(nèi)存塊被稱作“卡頁(yè)”???yè)的大小都是以2的N次冪的字節(jié)數(shù),例如Hotspot中卡頁(yè)是2的9次冪,512字節(jié)
    一個(gè)卡頁(yè)的內(nèi)存中通常包含不止一個(gè)對(duì)象,只要卡頁(yè)內(nèi)有一個(gè)或者更多的對(duì)象字段存在跨代指針,那就將對(duì)應(yīng)卡表的數(shù)組元素的值標(biāo)識(shí)為1,稱為元素變臟,沒(méi)有則標(biāo)識(shí)為0。在垃圾收集發(fā)生時(shí),只要篩選出卡表中變臟的元素就能輕易得出哪些卡頁(yè)內(nèi)存塊包含跨代指針,把他們加入GC Roots中一并掃描。
    5)寫屏障/偽共享
    1.原因:卡表元素如何維護(hù)問(wèn)題,例如他們何時(shí)變臟、誰(shuí)來(lái)把他們變臟。
    2.有其他分代區(qū)域中對(duì)象引用了本區(qū)域?qū)ο髸r(shí),其對(duì)應(yīng)的卡表元素就應(yīng)該變臟,時(shí)間點(diǎn)應(yīng)該發(fā)生在引用類型字段賦值的那一刻。
    3.Hotspot虛擬機(jī)通過(guò)寫屏障技術(shù)維護(hù)卡表狀態(tài),寫屏障可以看作在虛擬機(jī)層面對(duì)“引用類型字段賦值”這個(gè)動(dòng)作的AOP切面,寫后屏障和寫前屏障。
    偽共享:高并發(fā)場(chǎng)景下產(chǎn)生,偽共享是處理并發(fā)底層細(xì)節(jié)時(shí),一種經(jīng)常要考慮的問(wèn)題,現(xiàn)代中央處理器的緩存系統(tǒng)中是以緩存行(Cache Line)為單位存儲(chǔ)的,當(dāng)多線程修改互相獨(dú)立的變量時(shí),如果這些變量恰好共享同一個(gè)緩存行,就會(huì)彼此影響(寫回、無(wú)效化或者同步)而導(dǎo)致性能降低,這就是偽共享問(wèn)題。
    解決方法:不采用無(wú)條件的寫屏障,而是先檢查卡表標(biāo)記,只有當(dāng)該卡表元素未被標(biāo)記過(guò)時(shí)才將其標(biāo)記為變臟,不過(guò)會(huì)增加額外的開(kāi)銷,但能夠避免偽共享問(wèn)題,兩者各有性能損耗。
    6)并發(fā)可達(dá)性分析/增量更新/原始快照
    1.垃圾收集器基本上都是依靠可達(dá)性分析算法來(lái)判定對(duì)象是否存活,可達(dá)性分析算法理論上全過(guò)程都基于一個(gè)能保障一致性的快照才能分析,這意味著必須全程凍結(jié)用戶線程的運(yùn)行。在根節(jié)點(diǎn)枚舉這個(gè)步驟中,由于GC Roots相比起整個(gè)Java堆中全部對(duì)象畢竟還是少數(shù),且在優(yōu)化技巧(OopMap)的加持下,非常短暫了。但是從GC Roots 再繼續(xù)往下遍歷對(duì)象圖,這一步驟的停頓時(shí)間就必然和Java堆容量直接成正比例關(guān)系了。
    2.標(biāo)記階段是所有追蹤式垃圾收集算法的共同特征,如果這個(gè)階段會(huì)隨著堆變大而等比例增加停頓時(shí)間,必須削減這部分停頓時(shí)間。先搞清楚為什么必須在一個(gè)能保障一致性的快照上才能進(jìn)行對(duì)象圖的遍歷?
    白色:對(duì)象尚未被垃圾收集器訪問(wèn)過(guò),顯然在剛開(kāi)始的階段,所有對(duì)象都是白色的,若分析結(jié)束的階段,仍然是白色的對(duì)象,即代表不可達(dá)。
    黑色:表示對(duì)象已經(jīng)被垃圾收集器訪問(wèn)過(guò),且這個(gè)對(duì)象的所有引用都已經(jīng)掃描過(guò)。黑色的對(duì)象代表已經(jīng)掃描過(guò),它是安全存活的,如果有其他對(duì)象引用指向了黑色對(duì)象,無(wú)須重新掃描一遍。
    灰色:表示對(duì)象已經(jīng)被垃圾收集器訪問(wèn)過(guò),但這個(gè)對(duì)象上至少存在一個(gè)引用還沒(méi)被掃描過(guò)。
    如果不暫停用戶線程,可能會(huì)導(dǎo)致收集器在對(duì)象圖上標(biāo)記顏色,同時(shí)用戶線程在修改引用關(guān)系——即修改對(duì)象圖結(jié)構(gòu),可能出現(xiàn)兩種后果:一種是把原本消亡的對(duì)象錯(cuò)誤標(biāo)記為存活,只不過(guò)產(chǎn)生了一點(diǎn)逃過(guò)本次收集的浮動(dòng)垃圾而已。第二種是把原本存活的對(duì)象錯(cuò)誤標(biāo)記為已消亡,這是致命的后果。
    Wilson于1994年在理論上證明了,當(dāng)且僅當(dāng)以下兩個(gè)條件同時(shí)滿足時(shí),會(huì)產(chǎn)生對(duì)象消失問(wèn)題,即原本應(yīng)該是黑色的對(duì)象被誤標(biāo)為白色:
    1.賦值器插入了一條或多條從黑色對(duì)象到白色對(duì)象的新引用;
    2.賦值器刪除了全部從灰色對(duì)象到該白色對(duì)象的直接或間接引用
    只需要破壞一個(gè)條件,就可以避免對(duì)象消失,產(chǎn)生兩種解決方案:增量更新和原始快照
    增量更新:當(dāng)黑色對(duì)象插入新的白色對(duì)象的引用關(guān)系時(shí),就將這個(gè)新插入的引用記錄下來(lái),等并發(fā)掃描結(jié)束之后,再將這些記錄過(guò)的引用關(guān)系中的黑色對(duì)象為根,重新掃描一次。
    原始快照:當(dāng)灰色對(duì)象要?jiǎng)h除指向白色對(duì)象的引用關(guān)系時(shí),就將這個(gè)要?jiǎng)h除的引用記錄下來(lái),等并發(fā)掃描結(jié)束之后,再將這些記錄過(guò)的引用關(guān)系中的灰色對(duì)象為根,重新掃描一次。
    備注:無(wú)論是對(duì)引用關(guān)系的插入還是刪除,虛擬機(jī)的記錄操作都是通過(guò)寫屏障實(shí)現(xiàn)的。CMS是基于增量更新來(lái)做并發(fā)標(biāo)記的、G1和Shenandoah則是用原始快照來(lái)實(shí)現(xiàn)。
  4. 幾種經(jīng)典的垃圾收集器
    吞吐量 = 運(yùn)行用戶代碼時(shí)間 / (運(yùn)行用戶代碼時(shí)間 + 運(yùn)行垃圾收集時(shí)間)
    1)Serial
    1.一個(gè)單線程收集器,啟用時(shí)必須 stop the world,采取標(biāo)記-復(fù)制算法實(shí)現(xiàn)。
    2.迄今為止,它仍然是Hotspot虛擬機(jī)運(yùn)行再客戶端模式下的默認(rèn)新生代收集器,他是所有收集器里額外內(nèi)存消耗最小的,且沒(méi)有線程交互的開(kāi)銷,專心做垃圾收集。
    3.在桌面場(chǎng)景以及近年來(lái)流行的部分微服務(wù)應(yīng)用中,分配給虛擬機(jī)管理的內(nèi)存一般不會(huì)特別大,收集幾十兆甚至一百兆的新生代,垃圾收集的停頓時(shí)間完全可以控制在十幾、幾十毫秒,最多100毫秒以內(nèi),所以Serial對(duì)于運(yùn)行在客戶端模式下的虛擬機(jī)來(lái)說(shuō)是一個(gè)很好的選擇。
    2)ParNew
    1.實(shí)質(zhì)上是Serial的多線程并行版本,新生代采用復(fù)制算法,多線程并行,暫停用戶線程。
    2.只能和CMS搭配使用,在服務(wù)端常用,無(wú)法與Parallel Scavenge搭配使用,因?yàn)橐粋€(gè)面向低延遲一個(gè)面向高吞吐量,除此之外就是,Parallel沒(méi)有分代框架,而CMS又是基于這種強(qiáng)分代框架下。
    3)CMS
    1.CMS收集器是一種以獲取最短回收停頓時(shí)間為目標(biāo)的收集器,基于標(biāo)記-清除算法,其中包含幾個(gè)階段:
    初始標(biāo)記:需要stop the world 。初始標(biāo)記僅僅只是標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對(duì)象,速度很快;
    并發(fā)標(biāo)記:就是從GC Roots的直接關(guān)聯(lián)對(duì)象開(kāi)始遍歷整個(gè)對(duì)象圖的過(guò)程,雖然耗時(shí)較長(zhǎng)但是不需要停頓用戶線程;
    重新標(biāo)記:需要stop the world ,則是為了修正并發(fā)標(biāo)記期間,因用戶線程線程繼續(xù)運(yùn)轉(zhuǎn)而導(dǎo)致的標(biāo)記變動(dòng)的記錄;
    并發(fā)清除:清理掉標(biāo)記階段已經(jīng)死亡的對(duì)象,由于不需要移動(dòng)存活對(duì)象,所以也是并發(fā)清除。
    2.缺點(diǎn):
    ①:CMS對(duì)處理器資源非常敏感,因占用一部分線程而導(dǎo)致程序變慢,降低總吞吐量。
    ②:CMS收集器無(wú)法處理浮動(dòng)垃圾,有可能導(dǎo)致Concurrent Mode Failure失敗進(jìn)而導(dǎo)致另一次完全的Stop the world 的Full GC產(chǎn)生。由于由于垃圾收集階段用戶線程還在持續(xù)運(yùn)行,那還需要預(yù)留足夠的內(nèi)存空間提供給用戶線程使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全填滿了再進(jìn)行收集,必須預(yù)留一部分空間供并發(fā)收集時(shí)的程序運(yùn)行,JDK6時(shí),CMS收集器啟動(dòng)閾值已經(jīng)默認(rèn)提升至92%,但又會(huì)面臨另外一種風(fēng)險(xiǎn):要是CMS運(yùn)行期間預(yù)留的內(nèi)存無(wú)法滿足程序分配新對(duì)象的需要,就會(huì)出現(xiàn)一次并發(fā)失敗,這時(shí)虛擬機(jī)不得不啟動(dòng)后備預(yù)案:凍結(jié)用戶線程,臨時(shí)啟動(dòng)Serial Old 收集器來(lái)重新進(jìn)行老年代的垃圾收集。
    ③:產(chǎn)生大量碎片空間,空間碎片過(guò)多時(shí),將會(huì)給大對(duì)象分配帶來(lái)麻煩,往往會(huì)出現(xiàn)老年代還有很多剩余空間,但就是無(wú)法找到足夠大的連續(xù)空間來(lái)分配當(dāng)前對(duì)象,而不得不提前觸發(fā)一次Full GC時(shí)開(kāi)啟內(nèi)存碎片的合并整理過(guò)程,且必須移動(dòng)存活對(duì)象,是無(wú)法并發(fā)的。
    4)G1
    1.G1是里程碑式的收集器,弱化分代概念,開(kāi)創(chuàng)了面向局部收集和基于Region的內(nèi)存布局形式,主要面向服務(wù)端應(yīng)用。
    2.設(shè)計(jì)者們希望能建立起“停頓時(shí)間模型”的收集器,停頓時(shí)間模型的意思是能夠支持指定在一個(gè)長(zhǎng)度為M毫秒的時(shí)間片段內(nèi),消耗在垃圾收集上的時(shí)間大概率不超過(guò)N毫秒這樣的目標(biāo)。
    3.G1面向堆內(nèi)存任何部分來(lái)組成回收集,進(jìn)行回收,衡量標(biāo)準(zhǔn)不再是它屬于哪一個(gè)分代,而是哪塊內(nèi)存存放的垃圾數(shù)量最多,回收收益最大,這就是G1的Mixed GC模式。
    4.G1把連續(xù)的Java堆劃分成多個(gè)大小相等的獨(dú)立區(qū)域(Region),每一個(gè)Region根據(jù)需要扮演新生代Eden空間、Survivor空間或者老年代空間。收集器能夠?qū)Π缪莶煌巧腞egion采用不同的策略去處理,這樣就能達(dá)到很好的收集效果。
    5.Region中還有一類特殊的Humongous區(qū)域,專門用來(lái)存儲(chǔ)大對(duì)象,G1認(rèn)為超過(guò)了一個(gè)Region大小的一半即可判定為大對(duì)象,每個(gè)Region取值范圍是1~32MB,為2的N次冪。對(duì)于超過(guò)了整個(gè)Region容量的超大對(duì)象,將會(huì)被存放在N個(gè)連續(xù)的Humongous Region中,G1的大多數(shù)行為都把Humongous Region作為老年代的一部分開(kāi)看待。
    6.每次垃圾收集根據(jù)用戶設(shè)定的允許的收集停頓時(shí)間,優(yōu)先處理回收價(jià)值最大的那部分Region。
    7.G1收集器面臨的問(wèn)題:
    ①:多個(gè)Region的跨代引用問(wèn)題,每個(gè)Region都會(huì)維護(hù)自己的記憶集,這些記憶集會(huì)記錄下別的Region指向自己的指針,并且標(biāo)記這些指針?lè)謩e在哪個(gè)范圍之內(nèi),本質(zhì)是G1記憶集是一個(gè)hash表,Key是別的Region的起始地址,Value是一個(gè)集合,里面存儲(chǔ)的元素是卡表的索引號(hào)。這種雙向卡表結(jié)構(gòu)(卡表是我指向誰(shuí),這個(gè)結(jié)構(gòu)還有誰(shuí)指向我),所以維護(hù)起來(lái),就有著更高的內(nèi)存占用負(fù)擔(dān),G1至少要消耗大約相當(dāng)于Java堆容量10%到20%的額外內(nèi)存來(lái)維持收集器工作。
    ②:并發(fā)標(biāo)記階段如何保證收集線程與用戶線程互不干擾的運(yùn)行。CMS采用增量更新的算法實(shí)現(xiàn),而G1采用原始快照算法(SATB)實(shí)現(xiàn)。此外垃圾收集對(duì)用戶線程的影響還體現(xiàn)在回收過(guò)程中新創(chuàng)建對(duì)象的內(nèi)存分配上,程序要繼續(xù)運(yùn)行就肯定會(huì)持續(xù)由新對(duì)象被創(chuàng)建,G1為每一個(gè)Region設(shè)計(jì)了兩個(gè)名為TAMS的指針,把Region中的一部分空間劃分出來(lái)用于并發(fā)回收過(guò)程中的新對(duì)象分配,并發(fā)回收時(shí)新分配的對(duì)象地址都必須要在這兩個(gè)指針位置以上。G1收集器默認(rèn)在這個(gè)地址以上的對(duì)象是被隱式標(biāo)記過(guò)的,即默認(rèn)它們是存活的。如果內(nèi)存回收速度趕不上內(nèi)存分配速度,G1收集器也要被迫凍結(jié)用戶線程,導(dǎo)致Stop the world。Full GC。
    ③:如何建立起可靠的停頓預(yù)測(cè)模型?用戶通過(guò)參數(shù)指定的停頓時(shí)間只意味著垃圾收集發(fā)生之前的期望值,G1收集器的停頓時(shí)間預(yù)測(cè)模型是以衰減均值為理論基礎(chǔ)來(lái)實(shí)現(xiàn)的,在垃圾收集的過(guò)程中,G1收集器會(huì)記錄每個(gè)Region的回收耗時(shí)、每個(gè)Region記憶集里臟卡數(shù)量等各個(gè)可測(cè)量的步驟花費(fèi)成本,并分析得出平均值、標(biāo)準(zhǔn)偏差、置信度等統(tǒng)計(jì)信息。這里強(qiáng)調(diào)的“衰減平均值”是指它會(huì)比普通的平均值更容易受到新數(shù)據(jù)的影響,平均值代表整體平均狀態(tài),但衰減平均值更準(zhǔn)確代表“最近的”平均狀態(tài)。換句話說(shuō),Region的統(tǒng)計(jì)狀態(tài)越新越能決定回收的價(jià)值。然后通過(guò)這些信息預(yù)測(cè)現(xiàn)在開(kāi)始回收的話,由哪些Region組成回收集才可以在不超過(guò)期望停頓時(shí)間的約束下獲得最高的收益。
    8.運(yùn)作步驟:
    初始標(biāo)記:僅僅只是標(biāo)記一下GC Root能直接關(guān)聯(lián)到的對(duì)象,并且修改TAMS指針的值,讓下一階段用戶線程并發(fā)運(yùn)行時(shí),能正確地在可用的Region中分配新對(duì)象,需要停頓線程。
    并發(fā)標(biāo)記:從GC Root開(kāi)始對(duì)堆中對(duì)象進(jìn)行可達(dá)性分析,遞歸掃描整個(gè)堆里地對(duì)象圖,當(dāng)掃描完成以后,還需要重新處理SATB記錄下地在并發(fā)時(shí)有引用變動(dòng)的對(duì)象。
    最終標(biāo)記:對(duì)用戶線程做另外一個(gè)短暫的暫停,用于處理并發(fā)階段結(jié)束后遺留下來(lái)的最后那少量的SATB記錄。
    篩選回收:負(fù)責(zé)更新Region統(tǒng)計(jì)數(shù)據(jù),對(duì)各個(gè)Region的回收價(jià)值和成本進(jìn)行排序,根據(jù)用戶所期望的停頓時(shí)間來(lái)制定回收計(jì)劃;可以自由選擇任意個(gè)Region構(gòu)成回收集,然后把決定回收的那一部分Region的存活對(duì)象復(fù)制到空的Region中,再清理掉整個(gè)舊的Region的全部空間,這里的操作涉及存活對(duì)象的移動(dòng),是必須暫停用戶線程,由多條收集器線程并行完成的。
    由此可看到,G1除了并發(fā)標(biāo)記之外,其余階段也是要暫停用戶線程的。并非純粹追求低延遲,官方的設(shè)計(jì)目標(biāo)是在延遲可控的情況下,盡可能獲得高的吞吐量。
    9.用戶指定期望的停頓時(shí)間是G1很強(qiáng)大的一個(gè)功能。通常設(shè)定100~300ms。
    10.從G1開(kāi)始最先進(jìn)的垃圾收集器的設(shè)計(jì)導(dǎo)向都不約而同地變?yōu)槟軌驊?yīng)付應(yīng)用地內(nèi)存分配速率,而不追求一次把整個(gè)Java堆全部清理干凈。這樣,應(yīng)用在分配,同時(shí)收集器在收集,只要收集器的速度能跟得上對(duì)象分配的速度,那一切就能運(yùn)作得很完美
    11.CMS與G1收集器的對(duì)比
    G1優(yōu)點(diǎn):指定最大停頓時(shí)間、分region的內(nèi)存布局、按收益動(dòng)態(tài)確定回收集。G1從整體上基于標(biāo)記整理算法,局部(兩個(gè)Region之間)看又是基于標(biāo)記復(fù)制算法,不會(huì)產(chǎn)生內(nèi)存空間碎片,收集完成后能提供規(guī)整的可用內(nèi)存。
    G1相比于CMS缺點(diǎn):在用戶程序運(yùn)行過(guò)程中,G1無(wú)論是為了垃圾收集產(chǎn)生的內(nèi)存占用還是程序運(yùn)行時(shí)的額外執(zhí)行負(fù)載都比CMS高。G1的記憶集可能會(huì)占整個(gè)堆容量的20%乃至更多的內(nèi)存空間;從執(zhí)行負(fù)載的角度來(lái)看,CMS用寫后屏障更新維護(hù)卡表,而G1除了使用寫后屏障來(lái)維護(hù)卡表之外,為了實(shí)現(xiàn)原始快照算法(SATB),還要使用寫前屏障來(lái)跟蹤并發(fā)時(shí)的指針變化情況。(相比較增量更新算法,原始快照算法能減少并發(fā)標(biāo)記和重新標(biāo)記階段的消耗,避免CMS在最終標(biāo)記階段停頓時(shí)間過(guò)長(zhǎng)的缺點(diǎn),但會(huì)產(chǎn)生額外的負(fù)擔(dān)。CMS寫屏障是同步操作,而G1就不得不將其實(shí)現(xiàn)為類似消息隊(duì)列的結(jié)構(gòu),把寫前屏障和寫后屏障要做的事情放在消息隊(duì)列里,然后再異步處理。
    結(jié)論:小內(nèi)存上CMS好一點(diǎn),大內(nèi)存G1上大多能發(fā)揮其優(yōu)勢(shì),6GB到8GB之間。
  5. low_delay垃圾收集器
    1)ZGC
    1.ZGC收集器是一款基于Region內(nèi)存布局的,(暫時(shí))不設(shè)分代的,使用了讀屏障、染色指針和內(nèi)存多重映射等技術(shù)來(lái)實(shí)現(xiàn)的標(biāo)記-整理算法,以低延遲為首要目標(biāo)的一款垃圾收集器。
    2.ZGC的Region具有動(dòng)態(tài)性——?jiǎng)討B(tài)創(chuàng)建和銷毀,以及動(dòng)態(tài)的區(qū)域容量大?。?br> 小型Region:容量固定2MB,用于放置小于256KB的小對(duì)象。
    中型Region:容量固定為32MB,用于放置大于等于256KB但小于4MB的對(duì)象。
    大型Region:容量不固定,可以動(dòng)態(tài)變化,但必須為2MB的整數(shù)倍,用于放置4MB以上的大對(duì)象。每個(gè)大型的Region中只會(huì)存放一個(gè)大對(duì)象,雖然名字是大型Region但最小容量可低至4MB。大型Region在ZGC的實(shí)現(xiàn)中是不會(huì)被重分配,因?yàn)閺?fù)制一個(gè)大對(duì)象的代價(jià)極高。
    3.ZGC的核心問(wèn)題——并發(fā)整理算法
    ZGC標(biāo)志性設(shè)計(jì)——染色指針技術(shù):如果我們要在對(duì)象上存儲(chǔ)一些額外的、只供收集器、或者虛擬機(jī)本身使用的數(shù)據(jù),通常會(huì)在對(duì)象頭中增加額外的存儲(chǔ)字段,如對(duì)象的哈希碼、分代年齡、鎖記錄等就是這樣存儲(chǔ)的。這種記錄方式在有對(duì)象訪問(wèn)的場(chǎng)景下是很自然流暢的,不會(huì)有什么額外的負(fù)擔(dān)。但如果對(duì)象存在被移動(dòng)過(guò)的可能性,即不能保證對(duì)象訪問(wèn)能夠成功呢?能不能從指針或者與對(duì)象內(nèi)存無(wú)關(guān)的地方得到這些信息——追蹤式收集算法的標(biāo)記階段就可能存在只跟指針打交道而不必涉及指針?biāo)玫膶?duì)象本身的場(chǎng)景。例如對(duì)象標(biāo)記階段過(guò)程需要給對(duì)象打上三色標(biāo)記,這些標(biāo)記本質(zhì)上就只和對(duì)象的引用有關(guān),而與對(duì)象本身無(wú)關(guān)。
  6. 實(shí)戰(zhàn):內(nèi)存分配與回收策略

第二部分 虛擬機(jī)執(zhí)行子系統(tǒng) 代碼編譯的結(jié)果從本地機(jī)器碼轉(zhuǎn)變?yōu)樽止?jié)碼,是存儲(chǔ)格式發(fā)展的一小步,卻是編程語(yǔ)言發(fā)展的一大步

第六章 類文件結(jié)構(gòu)

這里就不做整理了,第一次看書(shū)時(shí)就打開(kāi)了一個(gè)編譯后的.class文件查看了,具體位置等,傳一張照片。



  1. Class類文件結(jié)構(gòu)
  2. 字節(jié)碼指令簡(jiǎn)介
  3. 公有設(shè)計(jì),私有實(shí)現(xiàn)

第七章 虛擬機(jī)類加載機(jī)制(鑒于字?jǐn)?shù)過(guò)多,從第七章開(kāi)始分小文章整理)

代碼編譯的結(jié)果從本地機(jī)器碼轉(zhuǎn)變?yōu)樽止?jié)碼,是存儲(chǔ)格式發(fā)展的一小步,卻是編程語(yǔ)言發(fā)展的一大步。
Java虛擬機(jī)把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機(jī)直接使用的Java類型,這個(gè)過(guò)程被稱作虛擬機(jī)的類加載機(jī)制。
與那些在編譯時(shí)需要連接的語(yǔ)言不同,在Java語(yǔ)言里面,類型的加載、連接和初始化過(guò)程都是在程序運(yùn)行期間完成的,這種策略讓Java語(yǔ)言進(jìn)行提前編譯會(huì)面臨額外的困難,也會(huì)讓類加載時(shí)稍微增加一點(diǎn)額外的開(kāi)銷,但卻為Java應(yīng)用提供了極高的擴(kuò)展性和靈活性,Java天生可以動(dòng)態(tài)擴(kuò)展的語(yǔ)言特性就是依賴運(yùn)行期間動(dòng)態(tài)加載和動(dòng)態(tài)鏈接實(shí)現(xiàn)的。

  1. 類加載的時(shí)機(jī)
  2. 類加載的過(guò)程
  3. 類加載器
  4. Java模塊化系統(tǒng)

第八章 虛擬機(jī)字節(jié)碼執(zhí)行引擎

  1. 運(yùn)行時(shí)棧幀結(jié)構(gòu)
  2. 方法調(diào)用
  3. 動(dòng)態(tài)類型語(yǔ)言支持
  4. 基于棧的字節(jié)碼解釋執(zhí)行引擎

第九章 類加載及執(zhí)行子系統(tǒng)的案例與實(shí)戰(zhàn)

  1. TomCat
  2. OSGI
  3. 字節(jié)碼生成技術(shù)與動(dòng)態(tài)代理技術(shù)
  4. Backport工具:Java的時(shí)光

前后端編譯 這里就只看了,泛型 自動(dòng)拆箱裝箱技術(shù)

第十章 前端編譯與優(yōu)化

1.泛型、自動(dòng)拆箱、裝箱與foreach循環(huán)

第十一章 后端編譯與優(yōu)化

  1. 即時(shí)編譯器
  2. 提前編譯器
  3. 編譯器優(yōu)化技術(shù)
  4. 深入理解Graal編譯器
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容