引用《深入理解Java虛擬機(jī)》書里的一句話:
Java與C++之間有一堵由內(nèi)存動(dòng)態(tài)分配和垃圾收集技術(shù)所圍成的“高墻”苛白,墻外面的人想進(jìn)去,墻里面的人卻想出來焚虱。
概述
上一篇文章深入理解Java虛擬機(jī)(一)java運(yùn)行時(shí)數(shù)據(jù)區(qū)域中购裙,講到程序計(jì)數(shù)器、虛擬機(jī)棧鹃栽、本地方法棧的生滅都隨線程的生命周期躏率,也就是內(nèi)存的分配和回收都有確定性。而Java堆和方法區(qū)都為線程共享,具有不確定性薇芝,這兩個(gè)區(qū)域如何回收也是垃圾收集器所關(guān)注的蓬抄。
對(duì)象已死?
在進(jìn)行垃圾收集之前夯到,首先要判斷哪些對(duì)象是存活的倡鲸,哪些對(duì)象需要回收。
引用計(jì)數(shù)算法
在對(duì)象中添加一個(gè)引用計(jì)數(shù)器黄娘,當(dāng)有地方引用它時(shí),計(jì)數(shù)器就加一克滴,當(dāng)引用失效時(shí)逼争,計(jì)數(shù)器就減一。如果計(jì)數(shù)器為零劝赔,代表對(duì)象不可能再被使用了誓焦。這種算法的缺點(diǎn)是,很難解決循環(huán)引用的問題着帽,Java虛擬機(jī)不使用這種算法杂伟。
可達(dá)性分析算法
可達(dá)性分析算法的基本思路是,以一系列稱為GC Roots的根對(duì)象開始仍翰,根據(jù)引用關(guān)系向下搜索赫粥,搜索走過的路徑稱為引用鏈。如果某個(gè)對(duì)象到GC Roots沒有引用鏈相連接予借,也就是說從GC Roots到這個(gè)對(duì)象不可達(dá)越平,說明對(duì)象不可能再被使用了。固定可作為GC Roots的對(duì)象包含以下幾種:
在虛擬機(jī)棧(棧幀中的本地變量表)引用的對(duì)象灵迫,例如各個(gè)線程被調(diào)用的方法堆棧中使用的參數(shù)秦叛、局部變量、臨時(shí)變量等
在方法區(qū)中類靜態(tài)屬性引用的對(duì)象瀑粥,例如Java類的引用類型靜態(tài)變量
在方法區(qū)中常量引用的對(duì)象挣跋,例如字符串常量引用的對(duì)象
本地方法棧中JNI(也就是Native方法)引用的對(duì)象
所有被同步鎖(Synchronized關(guān)鍵字)引用的對(duì)象
Java虛擬機(jī)的內(nèi)部引用,如基本數(shù)據(jù)類型對(duì)于的Class對(duì)象狞换、常駐的異常對(duì)象避咆、系統(tǒng)類加載器
映Java虛擬機(jī)內(nèi)部情況的JMXBean、JVMTI中注冊(cè)的回調(diào)修噪、本地代碼緩存等牌借。
垃圾收集算法
標(biāo)記-復(fù)制算法
將可用內(nèi)存平均劃分為兩塊,每次只使用其中一塊割按。當(dāng)這一塊內(nèi)存用完時(shí)膨报,就將存活的對(duì)象復(fù)制到另一塊,再清理它。
這種算法的優(yōu)點(diǎn)是簡(jiǎn)單高效现柠,但如果多數(shù)對(duì)象都是存活的院领,那么會(huì)產(chǎn)生大量的復(fù)制開銷,而且由于內(nèi)存空間等分為兩塊够吩,對(duì)空間的使用率不高比然。
標(biāo)記-清除算法
首先標(biāo)記出需要回收的對(duì)象,標(biāo)記完成后周循,對(duì)所有標(biāo)記的對(duì)象統(tǒng)一回收强法。或者標(biāo)記存活對(duì)象湾笛,再統(tǒng)一回收未被標(biāo)記的對(duì)象饮怯。
這種算法的缺點(diǎn)是執(zhí)行效率不穩(wěn)定,如果有大量對(duì)象是要被標(biāo)記的嚎研,那么標(biāo)記和清除的效率都會(huì)因?qū)ο髷?shù)量的增長(zhǎng)而降低蓖墅。其次,它會(huì)造成內(nèi)存空間的碎片化临扮,不利于后續(xù)大對(duì)象的分配论矾。
標(biāo)記-整理算法
標(biāo)記-整理算法的標(biāo)記過程,和標(biāo)記-清除算法一樣杆勇,但是在進(jìn)行回收時(shí)贪壳,先將所有存活對(duì)象向內(nèi)存的一端移動(dòng),再進(jìn)行垃圾收集蚜退,這樣就保證了內(nèi)存空間使用的連續(xù)性寥袭。它的缺點(diǎn)便是移動(dòng)對(duì)象比較耗時(shí),需要暫停用戶應(yīng)用程序关霸。
因此传黄,移動(dòng)對(duì)象與否具有兩面性,不移動(dòng)則會(huì)帶來內(nèi)存空間碎片化队寇,但也可以在因碎片導(dǎo)致的對(duì)象無法分配時(shí)膘掰,再進(jìn)行整理;而移動(dòng)對(duì)象雖然會(huì)降低垃圾回收效率佳遣,但涉及到分配大對(duì)象時(shí)更劃算识埋。
分代收集理論
虛擬機(jī)通常將Java堆分為新生代和老年代。
新生代
新生代的目標(biāo)是盡可能快速的回收生命周期比較短的對(duì)象零渐,通常來說窒舟,新創(chuàng)建的對(duì)象會(huì)在新生代中。通常新生代還會(huì)細(xì)分為Eden區(qū)诵盼、Survivor 0區(qū)惠豺、Survivor 1區(qū)(8:1:1的比例)银还。當(dāng)Eden區(qū)空間不足時(shí),會(huì)發(fā)生一次Minor GC洁墙。此時(shí)會(huì)將Eden區(qū)的存活對(duì)象復(fù)制到Survivor 0區(qū)蛹疯,再清空Eden區(qū)。當(dāng)Eden區(qū)再次填滿時(shí)热监,同時(shí)回收Eden區(qū)和Survivor 0區(qū)捺弦,將存活對(duì)象放入Survivor 1區(qū),再清理另外兩個(gè)區(qū)域孝扛。也就是說列吼,必定會(huì)有一個(gè)Survivor區(qū)保持為空,供復(fù)制時(shí)使用苦始。但如果某個(gè)Survivor區(qū)被填滿寞钥,且有對(duì)象未復(fù)制完成,或者經(jīng)歷N次垃圾回收(-XX:MaxTenuringThreshold參數(shù)設(shè)置)后依然存活的對(duì)象盈简,會(huì)被放入老年代中。如果設(shè)置了 - XX:PretenureSizeThreshold 參數(shù)太示,超過這個(gè)大小的對(duì)象會(huì)直接進(jìn)入老年代柠贤,為的是避免大對(duì)象在新生代中來回復(fù)制。
老年代
老年代一般存放生命周期較長(zhǎng)的對(duì)象类缤,通常在新生代經(jīng)歷N次垃圾回收后依然存活的對(duì)象臼勉,會(huì)被放到老年代中。老年代的收集稱為Major GC餐弱。
其它回收方式
Mixed GC:只有G1收集器有這種方式宴霸,回收整個(gè)新生代以及部分老年代。
Full GC:對(duì)整個(gè)堆和方法區(qū)進(jìn)行垃圾回收
HotSpot的算法實(shí)現(xiàn)細(xì)節(jié)
根節(jié)點(diǎn)枚舉
現(xiàn)在Java應(yīng)用越來越龐大膏蚓,類废岂、常量等數(shù)量就像恒河沙數(shù)帜矾,從GC Roots集合高效查找引用鏈并非易事。
雖然可達(dá)性分析算法耗時(shí)最長(zhǎng)的查找引用鏈的過程,已經(jīng)可以做到與用戶線程一起并發(fā)该贾。
但迄今為止的所有收集器在根節(jié)點(diǎn)枚舉時(shí),都必須暫停用戶線程徘意,也就是"Stop The World"懂昂。根節(jié)點(diǎn)枚舉是導(dǎo)致垃圾收集過程必須停頓所有用戶線程的一個(gè)重要原因。
目前主流Java虛擬機(jī)使用的都是準(zhǔn)確式垃圾收集狂魔,所以當(dāng)用戶線程停頓下來之后蒜埋,其實(shí)并不需要一個(gè)不漏地檢查完所有執(zhí)行上下文(例如棧幀中的本地變量表)和全局引用(例如常量、類靜態(tài)屬性)的位置最楷,有專門的地方來存放對(duì)象引用的整份。
在HotSpot中待错,使用一組稱為OopMap的數(shù)據(jù)結(jié)構(gòu)來達(dá)到這個(gè)目的。一旦類加載動(dòng)作完成皂林,HotSpot就會(huì)把對(duì)象內(nèi)什么偏移量上是什么類型的數(shù)據(jù)計(jì)算處理朗鸠,在即時(shí)編譯過程中,也會(huì)在特定位置記錄下棧里和寄存器里哪些位置是引用础倍。
安全點(diǎn)
如果每條指令都生成對(duì)應(yīng)的OopMap烛占,那么將需要大量額外的存儲(chǔ)空間。HotSpot虛擬機(jī)也沒有這么做沟启,只是在特定位置記錄這些信息忆家,這些位置稱為“安全點(diǎn)”。安全點(diǎn)的設(shè)定強(qiáng)制要求代碼指令流必須執(zhí)行到安全點(diǎn)后才能暫停開始垃圾收集德迹,而非在任意位置都能停頓下來芽卿。
總結(jié)來說,安全點(diǎn)是用來解決如何停頓用戶線程胳搞,讓虛擬機(jī)進(jìn)入垃圾回收狀態(tài)卸例。
在垃圾收集發(fā)生時(shí)讓所有線程都跑到最近的安全點(diǎn),然后停頓下來肌毅,有兩種方案:
搶先式中斷:在垃圾收集發(fā)生時(shí)筷转,先把所有用戶線程全部中斷,如果有線程沒有到達(dá)安全點(diǎn)悬而,則讓它繼續(xù)執(zhí)行到安全點(diǎn)上呜舒。但是虛擬機(jī)一般不用這種方式。
主動(dòng)式中斷:當(dāng)垃圾收集發(fā)生時(shí)笨奠,不直接對(duì)線程操作袭蝗,而是設(shè)定一個(gè)標(biāo)志位也就是安全點(diǎn),各個(gè)線程執(zhí)行過程中不斷主動(dòng)輪詢般婆,一旦發(fā)現(xiàn)中斷標(biāo)志為真到腥,就主動(dòng)掛起。
安全區(qū)域
當(dāng)用戶線程處于Sleep狀態(tài)或者Blocked狀態(tài)蔚袍,將無法響應(yīng)虛擬機(jī)的中斷請(qǐng)求左电,也不無法自己執(zhí)行到安全點(diǎn)。這種情況使用安全區(qū)域來處理页响。
安全區(qū)域是指能夠確保在某一段代碼片段之中篓足,引用關(guān)系不會(huì)發(fā)生變化,因此這塊區(qū)域內(nèi)任意地方開始垃圾回收都是安全的闰蚕。
記憶集與卡表
為解決跨代引用所帶來的問題栈拖,垃圾收集器在新生代中建立了名為記憶集(Remembered Set)的數(shù)據(jù)結(jié)構(gòu),用以避免把整個(gè)老年代加進(jìn)GC Roots掃描范圍没陡。(整個(gè)老年代加入掃描將嚴(yán)重影響性能)
記憶集是一種用于記錄從非收集區(qū)域指向收集區(qū)域的指針集合的抽象數(shù)據(jù)結(jié)構(gòu)涩哟。底層用非收集區(qū)域中所有含跨代引用的對(duì)象數(shù)組來實(shí)現(xiàn)這個(gè)數(shù)據(jù)結(jié)構(gòu)索赏。
有三種記錄精度供選擇:
字長(zhǎng)精度:每個(gè)記錄精確到一個(gè)機(jī)器字長(zhǎng)
對(duì)象精度:每個(gè)記錄精確到一個(gè)對(duì)象,該對(duì)象里有字段含有跨代指針
卡精度:每個(gè)記錄精確到一個(gè)內(nèi)存區(qū)域贴彼,該區(qū)域內(nèi)含有的對(duì)象含有跨代指針
目前最常用第三種方式潜腻,也就是用“卡表”(Card Table)來實(shí)現(xiàn)記憶集。
寫屏障
寫屏障用來維護(hù)卡表元素的狀態(tài)器仗,也就是卡頁對(duì)應(yīng)的內(nèi)存區(qū)域中是否存在跨代引用融涣。
寫屏障的實(shí)現(xiàn)原理是在虛擬機(jī)層面對(duì)“引用類型字段賦值”這個(gè)動(dòng)作的AOP切面,賦值時(shí)會(huì)產(chǎn)生一個(gè)環(huán)形(Around)通知精钮,供程序執(zhí)行額外動(dòng)作威鹿。
經(jīng)典的垃圾收集器
圖中展示了七種作用于不同分代的收集器,如果兩個(gè)收集器之間存在連線轨香,就說明它們可以搭配使用忽你,圖中收集器所處的區(qū)域,則表示它是屬于新生代收集器抑或是老年代收集器臂容。
JDK9標(biāo)識(shí)的線條科雳,表示在JDK9時(shí)取消了搭配使用。
下圖對(duì)垃圾收集器做了歸類和總結(jié)脓杉,本文僅介紹CMS和G1收集器的算法原理糟秘。
CMS收集器
CMS(Concurrent Mark Sweep)收集器以獲取最短停頓時(shí)間為目標(biāo),它基于標(biāo)記-清除算法丽已,整個(gè)過程分為四個(gè)步驟:
初始標(biāo)記:僅標(biāo)記GC Roots能夠直接關(guān)聯(lián)到的對(duì)象蚌堵,速度很快买决,需要停止用戶線程
并發(fā)標(biāo)記:就是從GC Roots直接關(guān)聯(lián)的對(duì)象開始遍歷整個(gè)對(duì)象樹沛婴,不需要停止用戶線程
重新標(biāo)記:修正在并發(fā)標(biāo)記期間,由于用戶線程繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那部分對(duì)象的標(biāo)記記錄督赤,需要停止用戶線程
并發(fā)清除:清理標(biāo)記階段判斷為已死亡的對(duì)象嘁灯,由于不需要移動(dòng)存活對(duì)象,因此不需要停止用戶線程
說明
并行(Parallel):并行描述的是多條垃圾收集器線程之間的關(guān)系躲舌,說明同一時(shí)間有多條這樣的線程在協(xié)同工作丑婿,通常默認(rèn)此時(shí)用戶線程是處于等待狀態(tài)。
并發(fā)(Concurrent):并發(fā)描述的是垃圾收集器線程與用戶線程之間的關(guān)系没卸,說明同一時(shí)間垃圾收集器線程與用戶線程都在運(yùn)行羹奉。
G1收集器
G1收集器開創(chuàng)了面向局部收集的設(shè)計(jì)思路和基于Region的內(nèi)存布局形式,它把連續(xù)的Java堆劃分為大小相等的獨(dú)立區(qū)域(Region)约计,每一個(gè)Region都可以根據(jù)需要扮演新生代的Eden空間诀拭,Survivor空間,或者老年代空間煤蚌。還有一類特殊的Humongous區(qū)域耕挨,專門用來存儲(chǔ)大對(duì)象细卧。G1為每個(gè)Region分別兩個(gè)名為TAMS的指針,因?yàn)榛厥者^程與用戶線程會(huì)并發(fā)執(zhí)行筒占,期間可能有新對(duì)象創(chuàng)建贪庙,新分別的對(duì)象地址必須在兩個(gè)指針位置以上。
G1收集器整體上是用標(biāo)記-整理算法來實(shí)現(xiàn)的翰苫,但從局部來看(兩個(gè)Region之間)是基于標(biāo)記-復(fù)制算法來實(shí)現(xiàn)的止邮,具體分為四個(gè)步驟:
初始標(biāo)記:僅標(biāo)記GC Roots能直接關(guān)聯(lián)的對(duì)象,并移動(dòng)TAMS指針革骨,保證下一階段用戶線程執(zhí)行時(shí)能正確創(chuàng)建對(duì)象农尖。此階段用戶線程需要停頓,但耗時(shí)很短
并發(fā)標(biāo)記:從GC Roots開始遞歸掃描對(duì)象圖良哲,找到要回收的對(duì)象盛卡,這個(gè)過程耗時(shí)比較長(zhǎng),但可以與用戶線程并發(fā)執(zhí)行
最終標(biāo)記:短暫暫停用戶線程筑凫,處理上一步中引用發(fā)生改變的對(duì)象的SATB記錄
篩選回收:更新Region的統(tǒng)計(jì)數(shù)據(jù)滑沧,對(duì)Region的回收價(jià)值和成本進(jìn)行排序,再根據(jù)用戶配置的停頓時(shí)間巍实,將決定回收的Region中的存活對(duì)象滓技,復(fù)制到空的Region中。由于此過程涉及對(duì)象的移動(dòng)棚潦,因此需要暫停用戶線程令漂。由此也可以看出它并非只關(guān)注低延遲,還關(guān)注吞吐量丸边。
本文主要為閱讀《深入理解Java虛擬機(jī) 第三版》所做的筆記總結(jié)叠必,如有錯(cuò)漏歡迎支持,謝謝