如果說收集算法是內(nèi)存回收的方法論攒钳,那垃圾收集器就是內(nèi)存回收的實踐者挽荠〗裕《Java虛擬機規(guī)范》中對垃圾收集器應(yīng)該如何實現(xiàn)并沒有做出任何規(guī)定德挣,因此不同的廠商、不同版本的虛擬機所包含的垃圾收集器都可能會有很大差別快毛,不同的虛擬機一般也都會提供各種參數(shù)供用戶根據(jù)自己的應(yīng)用特點和要求組合出各個內(nèi)存分代所使用的收集器格嗅。
圖3-6展示了七種作用于不同分代的收集器,如果兩個收集器之間存在連線唠帝,就說明它們可以搭配使用屯掖,圖中收集器所處的區(qū)域,則表示它是屬于新生代收集器抑或是老年代收集器襟衰。接下來將逐一介紹這些收集器的目標(biāo)贴铜、特性、原理和使用場景瀑晒,并重點分析CMS和G1這兩款相對復(fù)雜而又廣泛使用的收集器绍坝,深入了解它們的部分運作細(xì)節(jié)。
在介紹這些收集器各自的特性之前苔悦,讓我們先來明確一個觀點:雖然我們會對各個收集器進行比較轩褐,但并非為了挑選一個最好的收集器出來,雖然垃圾收集器的技術(shù)在不斷進步玖详,但直到現(xiàn)在還沒有最好的收集器出現(xiàn)把介,更加不存在“萬能”的收集器,所以我們選擇的只是對具體應(yīng)用最合適的收集器竹宋。如果有一種放之四海皆準(zhǔn)劳澄、任何場景下都適用的完美收集器存在,HotSpot虛擬機完全沒必要實現(xiàn)那么多種不同的收集器了蜈七。
Serial收集器
Serial收集器是最基礎(chǔ)秒拔、歷史最悠久的收集器,曾經(jīng)(在JDK 1.3.1之前)是HotSpot虛擬機新生代收集器的唯一選擇飒硅。大家只看名字就能夠猜到砂缩,這個收集器是一個單線程工作的收集器,但它的“單線程”的意義并不僅僅是說明它只會使用一個處理器或一條收集線程去完成垃圾收集工作三娩,更重要的是強調(diào)在它進行垃圾收集時庵芭,必須暫停其他所有工作線程,直到它收集結(jié)束雀监∷海“Stop The World”這個詞語也許聽起來很酷眨唬,但這項工作是由虛擬機在后臺自動發(fā)起和自動完成的,在用戶不可知好乐、不可控的情況下把用戶的正常工作的線程全部停掉匾竿,這對很多應(yīng)用來說都是不能接受的。讀者不妨試想一下蔚万,要是你的電腦每運行一個小時就會暫停響應(yīng)五分鐘岭妖,你會有什么樣的心情?圖3-7示意了Serial/Serial Old收集器的運行過程反璃。
對于“Stop The World”帶給用戶的惡劣體驗昵慌,早期HotSpot虛擬機的設(shè)計者們表示完全理解,但也同時表示非常委屈:“你媽媽在給你打掃房間的時候淮蜈,肯定也會讓你老老實實地在椅子上或者房間外待著斋攀,如果她一邊打掃,你一邊亂扔紙屑礁芦,這房間還能打掃完蜻韭?”這確實是一個合情合理的矛盾,雖然垃圾收集這項工作聽起來和打掃房間屬于一個工種柿扣,但實際上肯定還要比打掃房間復(fù)雜得多!
從JDK 1.3開始闺魏,一直到現(xiàn)在最新的JDK 13未状,HotSpot虛擬機開發(fā)團隊為消除或者降低用戶線程因垃圾收集而導(dǎo)致停頓的努力一直持續(xù)進行著,從Serial收集器到Parallel收集器析桥,再到Concurrent MarkSweep(CMS)和Garbage First(G1)收集器司草,最終至現(xiàn)在垃圾收集器的最前沿成果Shenandoah和ZGC等,我們看到了一個個越來越構(gòu)思精巧泡仗,越來越優(yōu)秀埋虹,也越來越復(fù)雜的垃圾收集器不斷涌現(xiàn),用戶線程的停頓時間在持續(xù)縮短娩怎,但是仍然沒有辦法徹底消除搔课,探索更優(yōu)秀垃圾收集器的工作仍在繼續(xù)。
寫到這里截亦,似乎已經(jīng)把Serial收集器描述成一個最早出現(xiàn)爬泥,但目前已經(jīng)老而無用,食之無味崩瓤,棄之可惜的“雞肋”了袍啡,但事實上,迄今為止却桶,它依然是HotSpot虛擬機運行在客戶端模式下的默認(rèn)新生代收集器境输,有著優(yōu)于其他收集器的地方,那就是簡單而高效(與其他收集器的單線程相比),對于內(nèi)存資源受限的環(huán)境嗅剖,它是所有收集器里額外內(nèi)存消耗(Memory Footprint)最小的辩越;對于單核處理器或處理器核心數(shù)較少的環(huán)境來說,Serial收集器由于沒有線程交互的開銷窗悯,專心做垃圾收集自然可以獲得最高的單線程收集效率区匣。在用戶桌面的應(yīng)用場景以及近年來流行的部分微服務(wù)應(yīng)用中,分配給虛擬機管理的內(nèi)存一般來說并不會特別大蒋院,收集幾十兆甚至一兩百兆的新生代(僅僅是指新生代使用的內(nèi)存亏钩,桌面應(yīng)用甚少超過這個容量),垃圾收集的停頓時間完全可以控制在十幾欺旧、幾十毫秒姑丑,最多一
百多毫秒以內(nèi),只要不是頻繁發(fā)生收集辞友,這點停頓時間對許多用戶來說是完全可以接受的栅哀。所以,Serial收集器對于運行在客戶端模式下的虛擬機來說是一個很好的選擇称龙。
ParNew收集器
ParNew收集器實質(zhì)上是Serial收集器的多線程并行版本留拾,除了同時使用多條線程進行垃圾收集之外,其余的行為包括Serial收集器可用的所有控制參數(shù)(例如:-XX:SurvivorRatio鲫尊、-XX:PretenureSizeThreshold痴柔、-XX:HandlePromotionFailure等)、收集算法疫向、Stop The World咳蔚、對象分配規(guī)則、回收策略等都與Serial收集器完全一致搔驼,在實現(xiàn)上這兩種收集器也共用了相當(dāng)多的代碼谈火。ParNew收集器的工作過程如圖3-8所示。
ParNew收集器除了支持多線程并行收集之外舌涨,其他與Serial收集器相比并沒有太多創(chuàng)新之處糯耍,,但它卻是不少運行在服務(wù)端模式下的HotSpot虛擬機泼菌,尤其是JDK 7之前的遺留系統(tǒng)中首選的新生代收集器谍肤,其中有一個與功能、性能無關(guān)但其實很重要的原因是:除了Serial收集器外哗伯,目前只有它能與CMS收集器配合工作荒揣。
在JDK 5發(fā)布時,HotSpot推出了一款在強交互應(yīng)用中幾乎可稱為具有劃時代意義的垃圾收集器——CMS收集器焊刹。這款收集器是HotSpot虛擬機中第一款真正意義上支持并發(fā)的垃圾收集器系任,它首次實現(xiàn)了讓垃圾收集線程與用戶線程(基本上)同時工作恳蹲。
遺憾的是,CMS作為老年代的收集器俩滥,卻無法與JDK 1.4.0中已經(jīng)存在的新生代收集器Parallel Scavenge配合工作[1]嘉蕾,所以在JDK 5中使用CMS來收集老年代的時候,新生代只能選擇ParNew或者Serial收集器中的一個霜旧。ParNew收集器是激活CMS后(使用-XX:+UseConcMarkSweepGC選項)的默認(rèn)新生代收集器错忱,也可以使用-XX:+/-UseParNewGC選項來強制指定或者禁用它。
可以說直到CMS的出現(xiàn)才鞏固了ParNew的地位挂据,但成也蕭何敗也蕭何以清,隨著垃圾收集器技術(shù)的不斷改進,更先進的G1收集器帶著CMS繼承者和替代者的光環(huán)登場崎逃。G1是一個面向全堆的收集器掷倔,不再需要其他新生代收集器的配合工作。所以自JDK 9開始个绍,ParNew加CMS收集器的組合就不再是官方推薦的服務(wù)端模式下的收集器解決方案了勒葱。官方希望它能完全被G1所取代,甚至還取消了ParNew加Serial Old以及Serial加CMS這兩組收集器組合的支持(其實原本也很少人這樣使用)巴柿,并直接取消了XX:+UseParNewGC參數(shù)凛虽,這意味著ParNew和CMS從此只能互相搭配使用,再也沒有其他收集器能夠和它們配合了广恢。讀者也可以理解為從此以后涩维,ParNew合并入CMS,成為它專門處理新生代的組成部分袁波。ParNew可以說是HotSpot虛擬機中第一款退出歷史舞臺的垃圾收集器。
ParNew收集器在單核心處理器的環(huán)境中絕對不會有比Serial收集器更好的效果蜗侈,甚至由于存在線程交互的開銷篷牌,該收集器在通過超線程(Hyper-Threading)技術(shù)實現(xiàn)的偽雙核處理器環(huán)境中都不能百分之百保證超越Serial收集器。當(dāng)然踏幻,隨著可以被使用的處理器核心數(shù)量的增加枷颊,ParNew對于垃圾收集時系統(tǒng)資源的高效利用還是很有好處的。它默認(rèn)開啟的收集線程數(shù)與處理器核心數(shù)量相同该面,在處理器核心非常多(譬如32個夭苗,現(xiàn)在CPU都是多核加超線程設(shè)計,服務(wù)器達到或超過32個邏輯核心的情況非常普遍)的環(huán)境中隔缀,可以使用-XX:ParallelGCThreads參數(shù)來限制垃圾收集的線程數(shù)题造。
Parallel Scavenge收集器
Parallel Scavenge收集器也是一款新生代收集器,它同樣是基于標(biāo)記-復(fù)制算法實現(xiàn)的收集器猾瘸,也是能夠并行收集的多線程收集器……Parallel Scavenge的諸多特性從表面上看和ParNew非常相似界赔,那它有什么特別之處呢丢习?
Parallel Scavenge收集器的特點是它的關(guān)注點與其他收集器不同,CMS等收集器的關(guān)注點是盡可能地縮短垃圾收集時用戶線程的停頓時間淮悼,而Parallel Scavenge收集器的目標(biāo)則是達到一個可控制的吞吐量(Throughput)咐低。所謂吞吐量就是處理器用于運行用戶代碼的時間與處理器總消耗時間的比值,即
如果虛擬機完成某個任務(wù)袜腥,用戶代碼加上垃圾收集總共耗費了100分鐘见擦,其中垃圾收集花掉1分鐘,那吞吐量就是99%羹令。停頓時間越短就越適合需要與用戶交互或需要保證服務(wù)響應(yīng)質(zhì)量的程序鲤屡,良好的響應(yīng)速度能提升用戶體驗;而高吞吐量則可以最高效率地利用處理器資源特恬,盡快完成程序的運算任務(wù)执俩,主要適合在后臺運算而不需要太多交互的分析任務(wù)。
Parallel Scavenge收集器提供了兩個參數(shù)用于精確控制吞吐量癌刽,分別是控制最大垃圾收集停頓時間的-XX:MaxGCPauseMillis參數(shù)以及直接設(shè)置吞吐量大小的-XX:GCTimeRatio參數(shù)役首。
-XX:MaxGCPauseMillis參數(shù)允許的值是一個大于0的毫秒數(shù),收集器將盡力保證內(nèi)存回收花費的時間不超過用戶設(shè)定值显拜。不過大家不要異想天開地認(rèn)為如果把這個參數(shù)的值設(shè)置得更小一點就能使得系統(tǒng)的垃圾收集速度變得更快衡奥,垃圾收集停頓時間縮短是以犧牲吞吐量和新生代空間為代價換取的:系統(tǒng)把新生代調(diào)得小一些,收集300MB新生代肯定比收集500MB快远荠,但這也直接導(dǎo)致垃圾收集發(fā)生得
更頻繁矮固,原來10秒收集一次、每次停頓100毫秒譬淳,現(xiàn)在變成5秒收集一次档址、每次停頓70毫秒。停頓時間的確在下降邻梆,但吞吐量也降下來了守伸。
-XX:GCTimeRatio參數(shù)的值則應(yīng)當(dāng)是一個大于0小于100的整數(shù),也就是垃圾收集時間占總時間的比率浦妄,相當(dāng)于吞吐量的倒數(shù)尼摹。譬如把此參數(shù)設(shè)置為19,那允許的最大垃圾收集時間就占總時間的5%(即1/(1+19))剂娄,默認(rèn)值為99蠢涝,即允許最大1%(即1/(1+99))的垃圾收集時間。
由于與吞吐量關(guān)系密切阅懦,Parallel Scavenge收集器也經(jīng)常被稱作“吞吐量優(yōu)先收集器”和二。除上述兩個參數(shù)之外,Parallel Scavenge收集器還有一個參數(shù)-XX:+UseAdaptiveSizePolicy值得我們關(guān)注故黑。這是一個開關(guān)參數(shù)儿咱,當(dāng)這個參數(shù)被激活之后庭砍,就不需要人工指定新生代的大小(-Xmn)混埠、Eden與Survivor區(qū)的比例(-XX:SurvivorRatio)怠缸、晉升老年代對象大小(-XX:PretenureSizeThreshold)等細(xì)節(jié)參數(shù)了钳宪,虛擬機會根據(jù)當(dāng)前系統(tǒng)的運行情況收集性能監(jiān)控信息揭北,動態(tài)調(diào)整這些參數(shù)以提供最合適的停頓時間或者最大的吞吐量。這種調(diào)節(jié)方式稱為垃圾收集的自適應(yīng)的調(diào)節(jié)策略(GC Ergonomics)
如果讀者對于收集器運作不太了解吏颖,手工優(yōu)化存在困難的話搔体,使用Parallel Scavenge收集器配合自適應(yīng)調(diào)節(jié)策略,把內(nèi)存管理的調(diào)優(yōu)任務(wù)交給虛擬機去完成也許是一個很不錯的選擇半醉。只需要把基本的內(nèi)存數(shù)據(jù)設(shè)置好(如-Xmx設(shè)置最大堆)疚俱,然后使用-XX:MaxGCPauseMillis參數(shù)(更關(guān)注最大停頓時間)或XX:GCTimeRatio(更關(guān)注吞吐量)參數(shù)給虛擬機設(shè)立一個優(yōu)化目標(biāo),那具體細(xì)節(jié)參數(shù)的調(diào)節(jié)工作就由虛擬機完成了缩多。自適應(yīng)調(diào)節(jié)策略也是Parallel Scavenge收集器區(qū)別于ParNew收集器的一個重要特性呆奕。
Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同樣是一個單線程收集器衬吆,使用標(biāo)記-整理算法梁钾。這個收集器的主要意義也是供客戶端模式下的HotSpot虛擬機使用。如果在服務(wù)端模式下逊抡,它也可能有兩種用途:一種是在JDK 5以及之前的版本中與Parallel Scavenge收集器搭配使用(需要說明一下姆泻,Parallel Scavenge收集器架構(gòu)中本身有PS MarkSweep收集器來進行老年代收集,并非直接調(diào)用Serial Old收集器冒嫡,但是這個PS MarkSweep收集器與Serial Old的實現(xiàn)幾乎是一樣的拇勃,所以在官方的許多資料中都是直接以Serial Old代替PS MarkSweep進行講解),另外一種就是作為CMS收集器發(fā)生失敗時的后備預(yù)案孝凌,在并發(fā)收集發(fā)生Concurrent Mode Failure時使用潜秋。這兩點都將在后面的內(nèi)容中繼續(xù)講解。Serial Old收集器的工作過程如圖3-9所示胎许。
Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,支持多線程并發(fā)收集罗售,基于標(biāo)記-整理算法實現(xiàn)辜窑。這個收集器是直到JDK 6時才開始提供的,在此之前寨躁,新生代的Parallel Scavenge收集器一直處于相當(dāng)尷尬的狀態(tài)穆碎,原因是如果新生代選擇了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器以外別無選擇职恳,其他表現(xiàn)良好的老年代收集器所禀,如CMS無法與它配合工作方面。由于
老年代Serial Old收集器在服務(wù)端應(yīng)用性能上的“拖累”,使用Parallel Scavenge收集器也未必能在整體上獲得吞吐量最大化的效果色徘。同樣恭金,由于單線程的老年代收集中無法充分利用服務(wù)器多處理器的并行處理能力,在老年代內(nèi)存空間很大而且硬件規(guī)格比較高級的運行環(huán)境中褂策,這種組合的總吞吐量甚至不一定比ParNew加CMS的組合來得優(yōu)秀横腿。
直到Parallel Old收集器出現(xiàn)后,“吞吐量優(yōu)先”收集器終于有了比較名副其實的搭配組合斤寂,在注重吞吐量或者處理器資源較為稀缺的場合耿焊,都可以優(yōu)先考慮Parallel Scavenge加Parallel Old收集器這個組合。Parallel Old收集器的工作過程如圖3-10所示遍搞。
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標(biāo)的收集器罗侯。目前很大一部分的Java應(yīng)用集中在互聯(lián)網(wǎng)網(wǎng)站或者基于瀏覽器的B/S系統(tǒng)的服務(wù)端上,這類應(yīng)用通常都會較為關(guān)注服務(wù)的響應(yīng)速度溪猿,希望系統(tǒng)停頓時間盡可能短钩杰,以給用戶帶來良好的交互體驗。CMS收集器就非常符合這類應(yīng)用的需求再愈。
從名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于標(biāo)記-清除算法實現(xiàn)的榜苫,它的運作過程相對于前面幾種收集器來說要更復(fù)雜一些,整個過程分為四個步驟翎冲,包括:
1)初始標(biāo)記(CMS initial mark)
2)并發(fā)標(biāo)記(CMS concurrent mark)
3)重新標(biāo)記(CMS remark)
4)并發(fā)清除(CMS concurrent sweep)
其中初始標(biāo)記垂睬、重新標(biāo)記這兩個步驟仍然需要“Stop The World”。初始標(biāo)記僅僅只是標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對象抗悍,速度很快驹饺;并發(fā)標(biāo)記階段就是從GC Roots的直接關(guān)聯(lián)對象開始遍歷整個對象圖的過程,這個過程耗時較長但是不需要停頓用戶線程缴渊,可以與垃圾收集線程一起并發(fā)運行赏壹;而重新標(biāo)記階段則是為了修正并發(fā)標(biāo)記期間,因用戶程序繼續(xù)運作而導(dǎo)致標(biāo)記產(chǎn)生變動的那一部分對象的標(biāo)記記錄衔沼,這個階段的停頓時間通常會比初始標(biāo)記階段稍長一些蝌借,但也遠比并發(fā)標(biāo)記階段的時間短;最后是并發(fā)清除階段指蚁,清理刪除掉標(biāo)記階段判斷的已經(jīng)死亡的對象菩佑,由于不需要移動存活對象,所以這個階段也是可以與用戶線程同時并發(fā)的凝化。
由于在整個過程中耗時最長的并發(fā)標(biāo)記和并發(fā)清除階段中稍坯,垃圾收集器線程都可以與用戶線程一起工作,所以從總體上來說搓劫,CMS收集器的內(nèi)存回收過程是與用戶線程一起并發(fā)執(zhí)行的瞧哟。通過圖3-11可以比較清楚地看到CMS收集器的運作步驟中并發(fā)和需要停頓的階段混巧。
CMS是一款優(yōu)秀的收集器,它最主要的優(yōu)點在名字上已經(jīng)體現(xiàn)出來:并發(fā)收集勤揩、低停頓咧党,一些官方公開文檔里面也稱之為“并發(fā)低停頓收集器”(Concurrent Low Pause Collector)。CMS收集器是HotSpot虛擬機追求低停頓的第一次成功嘗試雄可,但是它還遠達不到完美的程度凿傅,至少有以下三個明顯的缺點:
首先,CMS收集器對處理器資源非常敏感数苫。事實上聪舒,面向并發(fā)設(shè)計的程序都對處理器資源比較敏感。在并發(fā)階段虐急,它雖然不會導(dǎo)致用戶線程停頓箱残,但卻會因為占用了一部分線程(或者說處理器的計算能力)而導(dǎo)致應(yīng)用程序變慢,降低總吞吐量。CMS默認(rèn)啟動的回收線程數(shù)是(處理器核心數(shù)量+3)/4,也就是說雹熬,如果處理器核心數(shù)在四個或以上,并發(fā)回收時垃圾收集線程只占用不超過25%的處理器運算資源盼理,并且會隨著處理器核心數(shù)量的增加而下降。但是當(dāng)處理器核心數(shù)量不足四個時俄删,CMS對用戶程序的影響就可能變得很大宏怔。如果應(yīng)用本來的處理器負(fù)載就很高,還要分出一半的運算能力去執(zhí)行收集器線程畴椰,就可能導(dǎo)致用戶程序的執(zhí)行速度忽然大幅降低臊诊。為了緩解這種情況,虛擬機提供了一種稱為“增量式并發(fā)收集器”(Incremental Concurrent Mark Sweep/i-CMS)的CMS收集器變種斜脂,所做的事情和以前單核處理器年代PC機操作系統(tǒng)靠搶占式多任務(wù)來模擬多核并行多任務(wù)的思想一樣抓艳,是在并發(fā)標(biāo)記、清理的時候讓收集器線程帚戳、用戶線程交替運行玷或,盡量減少垃圾收集線程的獨占資源的時間,這樣整個垃圾收集的過程會更長片任,但對用戶程序的影響就會顯得較少一些庐椒,直觀感受是速度變慢的時間更多了,但速度下降幅度就沒有那么明顯蚂踊。實踐證明增量式的CMS收集器效果很一般,從
JDK 7開始笔宿,i-CMS模式已經(jīng)被聲明為“deprecated”犁钟,即已過時不再提倡用戶使用棱诱,到JDK 9發(fā)布后iCMS模式被完全廢棄。
然后涝动,由于CMS收集器無法處理“浮動垃圾”(Floating Garbage)迈勋,有可能出現(xiàn)“Con-current Mode Failure”失敗進而導(dǎo)致另一次完全“Stop The World”的Full GC的產(chǎn)生。在CMS的并發(fā)標(biāo)記和并發(fā)清理階段醋粟,用戶線程是還在繼續(xù)運行的靡菇,程序在運行自然就還會伴隨有新的垃圾對象不斷產(chǎn)生。同樣也是由于在垃圾收集階段用戶線程還需要持續(xù)運行米愿,那就還需要預(yù)留足夠內(nèi)存空間提供給用戶線程使用厦凤,因此CMS收集器不能像其他收集器那樣等待到老年代幾乎完全被填滿了再進行收集,必須預(yù)留一部分空間供并發(fā)收集時的程序運作使用育苟。在JDK5的默認(rèn)設(shè)置下较鼓,CMS收集器當(dāng)老年代使用了68%的空間后就會被激活,這是一個偏保守的設(shè)置违柏,如果在實際應(yīng)用中老年代增長并不是太快博烂,可以適當(dāng)調(diào)高參數(shù)-XX:CMSInitiatingOccu-pancyFraction的值來提高CMS的觸發(fā)百分比,降低內(nèi)存回收頻率漱竖,獲取更好的性能禽篱。到了JDK 6時,CMS收集器的啟動閾值就已經(jīng)默認(rèn)提升至92%馍惹。但這又會更容易面臨另一種風(fēng)險:要是CMS運行期間預(yù)留的內(nèi)存無法滿
足程序分配新對象的需要躺率,就會出現(xiàn)一次“并發(fā)失敗”(Concurrent Mode Failure),這時候虛擬機將不得不啟動后備預(yù)案:凍結(jié)用戶線程的執(zhí)行讼积,臨時啟用Serial Old收集器來重新進行老年代的垃圾收集肥照,但這樣停頓時間就很長了。所以參數(shù)-XX:CMSInitiatingOccupancyFraction設(shè)置得太高將會很容易導(dǎo)致
大量的并發(fā)失敗產(chǎn)生勤众,性能反而降低舆绎,用戶應(yīng)在生產(chǎn)環(huán)境中根據(jù)實際應(yīng)用情況來權(quán)衡設(shè)置。
還有最后一個缺點们颜,在本節(jié)的開頭曾提到吕朵,CMS是一款基于“標(biāo)記-清除”算法實現(xiàn)的收集器,如果以前文章對前面這部分介紹還有印象的話窥突,就可能想到這意味著收集結(jié)束時會有大量空間碎片產(chǎn)生努溃。空間碎片過多時阻问,將會給大對象分配帶來很大麻煩梧税,往往會出現(xiàn)老年代還有很多剩余空間,但就是無法找到足夠大的連續(xù)空間來分配當(dāng)前對象,而不得不提前觸發(fā)一次Full GC的情況第队。為了解決這個問題哮塞,
CMS收集器提供了一個-XX:+UseCMS-CompactAtFullCollection開關(guān)參數(shù)(默認(rèn)是開啟的,此參數(shù)從JDK 9開始廢棄)凳谦,用于在CMS收集器不得不進行Full GC時開啟內(nèi)存碎片的合并整理過程忆畅,由于這個內(nèi)存整理必須移動存活對象,(在Shenandoah和ZGC出現(xiàn)前)是無法并發(fā)的尸执。這樣空間碎片問題是解決了家凯,但停頓時間又會變長,因此虛擬機設(shè)計者們還提供了另外一個參數(shù)-XX:CMSFullGCsBeforeCompaction(此參數(shù)從JDK 9開始廢棄)如失,這個參數(shù)的作用是要求CMS收集器在執(zhí)行過若干次(數(shù)量由參數(shù)值決定)不整理空間的Full GC之后绊诲,下一次進入Full GC前會先進行碎片整理(默認(rèn)值為0,表示每次進入Full GC時都進行碎片整理)岖常。
Garbage First收集器
Garbage First(簡稱G1)收集器是垃圾收集器技術(shù)發(fā)展歷史上的里程碑式的成果驯镊,它開創(chuàng)了收集器面向局部收集的設(shè)計思路和基于Region的內(nèi)存布局形式。早在JDK 7剛剛確立項目目標(biāo)竭鞍、Oracle公司制定的JDK 7 RoadMap里面板惑,G1收集器就被視作JDK 7中HotSpot虛擬機的一項重要進化特征。從JDK6 Update 14開始就有Early Access版本的G1收集器供開發(fā)人員實驗和試用偎快,但由此開始G1收集器的“實驗狀態(tài)”(Experimental)持續(xù)了數(shù)年時間冯乘,直至JDK 7 Update 4,Oracle才認(rèn)為它達到足夠成熟的商用程度晒夹,移除了“Experimental”的標(biāo)識裆馒;到了JDK 8 Update 40的時候,G1提供并發(fā)的類卸載的支持丐怯,補全了其計劃功能的最后一塊拼圖喷好。這個版本以后的G1收集器才被Oracle官方稱為“全功能的垃圾收集器”(Fully-Featured Garbage Collector)。
G1是一款主要面向服務(wù)端應(yīng)用的垃圾收集器读跷。HotSpot開發(fā)團隊最初賦予它的期望是(在比較長期的)未來可以替換掉JDK 5中發(fā)布的CMS收集器」=粒現(xiàn)在這個期望目標(biāo)已經(jīng)實現(xiàn)過半了,JDK 9發(fā)布之日效览,G1宣告取代Parallel Scavenge加Parallel Old組合无切,成為服務(wù)端模式下的默認(rèn)垃圾收集器,而CMS則淪落至被聲明為不推薦使用(Deprecate)的收集器丐枉。如果對JDK 9及以上版本的HotSpot虛擬機使用參數(shù)-XX:+UseConcMarkSweepGC來開啟CMS收集器的話哆键,用戶會收到一個警告信息,提示CMS未來將會被廢棄:
Java HotSpot(TM) 64-Bit Server VM warning: Option UseConcMarkSweepGC was deprecated in version 9.0 and will likely be removed in a future release.
但作為一款曾被廣泛運用過的收集器瘦锹,經(jīng)過多個版本的開發(fā)迭代后籍嘹,CMS(以及之前幾款收集器)的代碼與HotSpot的內(nèi)存管理闪盔、執(zhí)行、編譯辱士、監(jiān)控等子系統(tǒng)都有千絲萬縷的聯(lián)系锭沟,這是歷史原因?qū)е碌模⒉环下氊?zé)分離的設(shè)計原則识补。為此,規(guī)劃JDK 10功能目標(biāo)時辫红,HotSpot虛擬機提出了“統(tǒng)一垃圾收集器接口”凭涂,將內(nèi)存回收的“行為”與“實現(xiàn)”進行分離,CMS以及其他收集器都重構(gòu)成基于這套接口的一種實現(xiàn)贴妻。以此為基礎(chǔ)切油,日后要移除或者加入某一款收集器,都會變得容易許多名惩,風(fēng)險也可以控制澎胡,這算是在為CMS退出歷史舞臺鋪下最后的道路了。
作為CMS收集器的替代者和繼承人娩鹉,設(shè)計者們希望做出一款能夠建立起“停頓時間模型”(Pause Prediction Model)的收集器攻谁,停頓時間模型的意思是能夠支持指定在一個長度為M毫秒的時間片段內(nèi),消耗在垃圾收集上的時間大概率不超過N毫秒這樣的目標(biāo)弯予,那具體要怎么做才能實現(xiàn)這個目標(biāo)呢戚宦?首先要有一個思想上的改變,在G1收集器出現(xiàn)之前的所有其他收集器锈嫩,包括CMS在內(nèi)受楼,垃圾收集的目標(biāo)范圍要么是整個新生代(Minor GC),要么就是整個老年代(Major GC)呼寸,再要么就是整個Java堆(Full GC)艳汽。而G1跳出了這個樊籠,它可以面向堆內(nèi)存任何部分來組成回收集(Collection Set对雪,一般簡稱CSet)進行回收河狐,衡量標(biāo)準(zhǔn)不再是它屬于哪個分代,而是哪塊內(nèi)存中存放的垃圾數(shù)量最多慌植,回收收益最大甚牲,這就是G1收集器的Mixed GC模式。
G1開創(chuàng)的基于Region的堆內(nèi)存布局是它能夠?qū)崿F(xiàn)這個目標(biāo)的關(guān)鍵蝶柿。雖然G1也仍是遵循分代收集理論設(shè)計的丈钙,但其堆內(nèi)存的布局與其他收集器有非常明顯的差異:G1不再堅持固定大小以及固定數(shù)量的分代區(qū)域劃分,而是把連續(xù)的Java堆劃分為多個大小相等的獨立區(qū)域(Region)交汤,每一個Region都可以根據(jù)需要雏赦,扮演新生代的Eden空間劫笙、Survivor空間,或者老年代空間星岗。收集器能夠?qū)Π缪莶煌巧腞egion采用不同的策略去處理填大,這樣無論是新創(chuàng)建的對象還是已經(jīng)存活了一段時間、熬過多次收集的舊對象都能獲取很好的收集效果俏橘。
Region中還有一類特殊的Humongous區(qū)域允华,專門用來存儲大對象。G1認(rèn)為只要大小超過了一個Region容量一半的對象即可判定為大對象寥掐。每個Region的大小可以通過參數(shù)-XX:G1HeapRegionSize設(shè)定靴寂,取值范圍為1MB~32MB,且應(yīng)為2的N次冪召耘。而對于那些超過了整個Region容量的超級大對象百炬,將會被存放在N個連續(xù)的Humongous Region之中,G1的大多數(shù)行為都把Humongous Region作為老年代的一部分來進行看待污它,如圖3-12所示剖踊。
雖然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了衫贬,它們都是一系列區(qū)域(不需要連續(xù))的動態(tài)集合德澈。G1收集器之所以能建立可預(yù)測的停頓時間模型,是因為它將Region作為單次回收的最小單元祥山,即每次收集到的內(nèi)存空間都是Region大小的整數(shù)倍圃验,這樣可以有計劃地避免在整個Java堆中進行全區(qū)域的垃圾收集。更具體的處理思路是讓G1收集器去跟蹤各個Region里面的垃
圾堆積的“價值”大小缝呕,價值即回收所獲得的空間大小以及回收所需時間的經(jīng)驗值澳窑,然后在后臺維護一個優(yōu)先級列表,每次根據(jù)用戶設(shè)定允許的收集停頓時間(使用參數(shù)-XX:MaxGCPauseMillis指定供常,默認(rèn)值是200毫秒)摊聋,優(yōu)先處理回收價值收益最大的那些Region,這也就是“Garbage First”名字的由來栈暇。這種使用Region劃分內(nèi)存空間麻裁,以及具有優(yōu)先級的區(qū)域回收方式,保證了G1收集器在有限的時間內(nèi)獲取盡可能高的收集效率源祈。
G1將堆內(nèi)存“化整為零”的“解題思路”煎源,看起來似乎沒有太多令人驚訝之處,也完全不難理解香缺,但其中的實現(xiàn)細(xì)節(jié)可是遠遠沒有想象中那么簡單手销,否則就不會從2004年Sun實驗室發(fā)表第一篇關(guān)于G1的論文后一直拖到2012年4月JDK 7 Update 4發(fā)布,用將近10年時間才倒騰出能夠商用的G1收集器來图张。G1收集器至少有(不限于)以下這些關(guān)鍵的細(xì)節(jié)問題需要妥善解決:
·譬如锋拖,將Java堆分成多個獨立Region后诈悍,Region里面存在的跨Region引用對象如何解決?解決的思路我們已經(jīng)知道:使用記憶集避免全堆作為GC Roots掃描兽埃,但在G1收集器上記憶集的應(yīng)用其實要復(fù)雜很多侥钳,它的每個Region都維護有自己的記憶集,這些記憶集會記錄下別的Region指向自己的指針柄错,并標(biāo)記這些指針分別在哪些卡頁的范圍之內(nèi)舷夺。G1的記憶集在存儲結(jié)構(gòu)的本質(zhì)上是一種哈希表,Key是別的Region的起始地址售貌,Value是一個集合冕房,里面存儲的元素是卡表的索引號。這種“雙向”的卡表結(jié)構(gòu)(卡表是“我指向誰”趁矾,這種結(jié)構(gòu)還記錄了“誰指向我”)比原來的卡表實現(xiàn)起來更復(fù)雜,同時由于Region數(shù)量比傳統(tǒng)收集器的分代數(shù)量明顯要多得多给僵,因此G1收集器要比其他的傳統(tǒng)垃圾收集器有著更高的內(nèi)存占用負(fù)擔(dān)毫捣。根據(jù)經(jīng)驗,G1至少要耗費大約相當(dāng)于Java堆容量10%至20%的額外內(nèi)存來維持收集器工作
譬如帝际,在并發(fā)標(biāo)記階段如何保證收集線程與用戶線程互不干擾地運行蔓同?這里首先要解決的是用戶線程改變對象引用關(guān)系時,必須保證其不能打破原本的對象圖結(jié)構(gòu)蹲诀,導(dǎo)致標(biāo)記結(jié)果出現(xiàn)錯誤斑粱,該問題的解決辦法前面文章已經(jīng)抽出獨立來講解過:CMS收集器采用增量更新算法實現(xiàn),而G1收集器則是通過原始快照(SATB)算法來實現(xiàn)的脯爪。此外则北,垃圾收集對用戶線程的影響還體現(xiàn)在回收過
程中新創(chuàng)建對象的內(nèi)存分配上,程序要繼續(xù)運行就肯定會持續(xù)有新對象被創(chuàng)建痕慢,G1為每一個Region設(shè)計了兩個名為TAMS(Top at Mark Start)的指針尚揣,把Region中的一部分空間劃分出來用于并發(fā)回收過程中的新對象分配,并發(fā)回收時新分配的對象地址都必須要在這兩個指針位置以上掖举。G1收集器默認(rèn)在這個地址以上的對象是被隱式標(biāo)記過的快骗,即默認(rèn)它們是存活的,不納入回收范圍塔次。與CMS中
的“Concurrent Mode Failure”失敗會導(dǎo)致Full GC類似方篮,如果內(nèi)存回收的速度趕不上內(nèi)存分配的速度,G1收集器也要被迫凍結(jié)用戶線程執(zhí)行励负,導(dǎo)致Full GC而產(chǎn)生長時間“Stop The World”藕溅。·譬如,怎樣建立起可靠的停頓預(yù)測模型熄守?用戶通過-XX:MaxGCPauseMillis參數(shù)指定的停頓時間只意味著垃圾收集發(fā)生之前的期望值蜈垮,但G1收集器要怎么做才能滿足用戶的期望呢耗跛?G1收集器的停頓預(yù)測模型是以衰減均值(Decaying Average)為理論基礎(chǔ)來實現(xiàn)的,在垃圾收集過程中攒发,G1收集器會記錄每個Region的回收耗時调塌、每個Region記憶集里的臟卡數(shù)量等各個可測量的步驟花費的成本,并分析得出平均值惠猿、標(biāo)準(zhǔn)偏差羔砾、置信度等統(tǒng)計信息。這里強調(diào)的“衰減平均值”是指它會比普通的平均值更容易受到新數(shù)據(jù)的影響偶妖,平均值代表整體平均狀態(tài)姜凄,但衰減平均值更準(zhǔn)確地代表“最近的”平均狀態(tài)。換句話說趾访,Region的統(tǒng)計狀態(tài)越新越能決定其回收的價值态秧。然后通過這些信息預(yù)測現(xiàn)在開始回收的話,由
哪些Region組成回收集才可以在不超過期望停頓時間的約束下獲得最高的收益扼鞋。
如果我們不去計算用戶線程運行過程中的動作(如使用寫屏障維護記憶集的操作)申鱼,G1收集器的運作過程大致可劃分為以下四個步驟:
初始標(biāo)記(Initial Marking):僅僅只是標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對象,并且修改TAMS指針的值云头,讓下一階段用戶線程并發(fā)運行時捐友,能正確地在可用的Region中分配新對象。這個階段需要停頓線程溃槐,但耗時很短匣砖,而且是借用進行Minor GC的時候同步完成的,所以G1收集器在這個階段實際并沒有額外的停頓昏滴。
·并發(fā)標(biāo)記(Concurrent Marking):從GC Root開始對堆中對象進行可達性分析猴鲫,遞歸掃描整個堆里的對象圖,找出要回收的對象谣殊,這階段耗時較長变隔,但可與用戶程序并發(fā)執(zhí)行。當(dāng)對象圖掃描完成以后蟹倾,還要重新處理SATB記錄下的在并發(fā)時有引用變動的對象匣缘。
最終標(biāo)記(Final Marking):對用戶線程做另一個短暫的暫停,用于處理并發(fā)階段結(jié)束后仍遺留下來的最后那少量的SATB記錄
篩選回收(Live Data Counting and Evacuation):負(fù)責(zé)更新Region的統(tǒng)計數(shù)據(jù)鲜棠,對各個Region的回收價值和成本進行排序肌厨,根據(jù)用戶所期望的停頓時間來制定回收計劃,可以自由選擇任意多個Region構(gòu)成回收集豁陆,然后把決定回收的那一部分Region的存活對象復(fù)制到空的Region中柑爸,再清理掉整個舊Region的全部空間。這里的操作涉及存活對象的移動盒音,是必須暫停用戶線程表鳍,由多條收集器線程并行完成的馅而。
從上述階段的描述可以看出,G1收集器除了并發(fā)標(biāo)記外譬圣,其余階段也是要完全暫停用戶線程的瓮恭,換言之,它并非純粹地追求低延遲厘熟,官方給它設(shè)定的目標(biāo)是在延遲可控的情況下獲得盡可能高的吞吐量屯蹦,所以才能擔(dān)當(dāng)起“全功能收集器”的重任與期望。
從Oracle官方透露出來的信息可獲知绳姨,回收階段(Evacuation)其實本也有想過設(shè)計成與用戶程序一起并發(fā)執(zhí)行登澜,但這件事情做起來比較復(fù)雜,考慮到G1只是回收一部分Region飘庄,停頓時間是用戶可控制的脑蠕,所以并不迫切去實現(xiàn),而選擇把這個特性放到了G1之后出現(xiàn)的低延遲垃圾收集器(即ZGC)中跪削。另外空郊,還考慮到G1不是僅僅面向低延遲,停頓用戶線程能夠最大幅度提高垃圾收集效率切揭,為了保
證吞吐量所以才選擇了完全暫停用戶線程的實現(xiàn)方案。通過圖3-13可以比較清楚地看到G1收集器的運作步驟中并發(fā)和需要停頓的階段锁摔。
毫無疑問廓旬,可以由用戶指定期望的停頓時間是G1收集器很強大的一個功能,設(shè)置不同的期望停頓時間谐腰,可使得G1在不同應(yīng)用場景中取得關(guān)注吞吐量和關(guān)注延遲之間的最佳平衡孕豹。不過,這里設(shè)置的“期望值”必須是符合實際的十气,不能異想天開励背,畢竟G1是要凍結(jié)用戶線程來復(fù)制對象的,這個停頓時間再怎么低也得有個限度砸西。它默認(rèn)的停頓目標(biāo)為兩百毫秒叶眉,一般來說,回收階段占到幾十到一百甚至接近兩百毫秒都很正常芹枷,但如果我們把停頓時間調(diào)得非常低衅疙,譬如設(shè)置為二十毫秒,很可能出現(xiàn)的結(jié)果就是由于停頓目標(biāo)時間太短鸳慈,導(dǎo)致每次選出來的回收集只占堆內(nèi)存很小的一部分饱溢,收集器收集的速度逐漸跟不上分配器分配的速度,導(dǎo)致垃圾慢慢堆積走芋。很可能一開始收集器還能從空閑的堆內(nèi)存中獲得一些喘息的時間绩郎,但應(yīng)用運行時間一長就不行了潘鲫,最終占滿堆引發(fā)Full GC反而降低性能,所以通常把期望停頓時間設(shè)置為一兩百毫秒或者兩三百毫秒會是比較合理的肋杖。
從G1開始溉仑,最先進的垃圾收集器的設(shè)計導(dǎo)向都不約而同地變?yōu)樽非竽軌驊?yīng)付應(yīng)用的內(nèi)存分配速(Allocation Rate),而不追求一次把整個Java堆全部清理干凈兽愤。這樣彼念,應(yīng)用在分配,同時收集器在收集浅萧,只要收集的速度能跟得上對象分配的速度逐沙,那一切就能運作得很完美。這種新的收集器設(shè)計思路從工程實現(xiàn)上看是從G1開始興起的洼畅,所以說G1是收集器技術(shù)發(fā)展的一個里程碑吩案。
G1收集器常會被拿來與CMS收集器互相比較,畢竟它們都非常關(guān)注停頓時間的控制帝簇,官方資料中將它們兩個并稱為“The Mostly Concurrent Collectors”徘郭。在未來,G1收集器最終還是要取代CMS的丧肴,而當(dāng)下它們兩者并存的時間里残揉,分個高低優(yōu)劣就無可避免。
相比CMS芋浮,G1的優(yōu)點有很多抱环,暫且不論可以指定最大停頓時間、分Region的內(nèi)存布局纸巷、按收益動態(tài)確定回收集這些創(chuàng)新性設(shè)計帶來的紅利镇草,單從最傳統(tǒng)的算法理論上看,G1也更有發(fā)展?jié)摿α鲋肌EcCMS的“標(biāo)記-清除”算法不同梯啤,G1從整體來看是基于“標(biāo)記-整理”算法實現(xiàn)的收集器,但從局部(兩個Region之間)上看又是基于“標(biāo)記-復(fù)制”算法實現(xiàn)存哲,無論如何因宇,這兩種算法都意味著G1運作期間不會產(chǎn)生內(nèi)存空間碎片,垃圾收集完成之后能提供規(guī)整的可用內(nèi)存祟偷。這種特性有利于程序長時間運行羽嫡,在程序為大對象分配內(nèi)存時不容易因無法找到連續(xù)內(nèi)存空間而提前觸發(fā)下一次收集。
不過肩袍,G1相對于CMS仍然不是占全方位杭棵、壓倒性優(yōu)勢的,從它出現(xiàn)幾年仍不能在所有應(yīng)用場景中代替CMS就可以得知這個結(jié)論。比起CMS魂爪,G1的弱項也可以列舉出不少先舷,如在用戶程序運行過程中,G1無論是為了垃圾收集產(chǎn)生的內(nèi)存占用(Footprint)還是程序運行時的額外執(zhí)行負(fù)載(Overload)都要比CMS要高滓侍。
就內(nèi)存占用來說蒋川,雖然G1和CMS都使用卡表來處理跨代指針,但G1的卡表實現(xiàn)更為復(fù)雜撩笆,而且堆中每個Region捺球,無論扮演的是新生代還是老年代角色,都必須有一份卡表夕冲,這導(dǎo)致G1的記憶集(和其他內(nèi)存消耗)可能會占整個堆容量的20%乃至更多的內(nèi)存空間氮兵;相比起來CMS的卡表就相當(dāng)簡單,只有唯一一份歹鱼,而且只需要處理老年代到新生代的引用泣栈,反過來則不需要,由于新生代的對象具有朝生夕滅的不穩(wěn)定性弥姻,引用變化頻繁南片,能省下這個區(qū)域的維護開銷是很劃算的。
在執(zhí)行負(fù)載的角度上庭敦,同樣由于兩個收集器各自的細(xì)節(jié)實現(xiàn)特點導(dǎo)致了用戶程序運行時的負(fù)載會有不同疼进,譬如它們都使用到寫屏障,CMS用寫后屏障來更新維護卡表秧廉;而G1除了使用寫后屏障來進行同樣的(由于G1的卡表結(jié)構(gòu)復(fù)雜伞广,其實是更煩瑣的)卡表維護操作外,為了實現(xiàn)原始快照搜索(SATB)算法定血,還需要使用寫前屏障來跟蹤并發(fā)時的指針變化情況。相比起增量更新算法诞外,原始快照搜索能夠減少并發(fā)標(biāo)記和重新標(biāo)記階段的消耗澜沟,避免CMS那樣在最終標(biāo)記階段停頓時間過長的缺點,但是在用戶程序運行過程中確實會產(chǎn)生由跟蹤引用變化帶來的額外負(fù)擔(dān)峡谊。由于G1對寫屏障的復(fù)雜操作要比CMS消耗更多的運算資源茫虽,所以CMS的寫屏障實現(xiàn)是直接的同步操作,而G1就不得不將其實現(xiàn)為類似于消息隊列的結(jié)構(gòu)既们,把寫前屏障和寫后屏障中要做的事情都放到隊列里濒析,然后再異步處理。
以上的優(yōu)缺點對比僅僅是針對G1和CMS兩款垃圾收集器單獨某方面的實現(xiàn)細(xì)節(jié)的定性分析啥纸,通常我們說哪款收集器要更好号杏、要好上多少,往往是針對具體場景才能做的定量比較。按照實踐經(jīng)驗盾致,目前在小內(nèi)存應(yīng)用上CMS的表現(xiàn)大概率仍然要會優(yōu)于G1主经,而在大內(nèi)存應(yīng)用上G1則大多能發(fā)揮其優(yōu)勢,這個優(yōu)劣勢的Java堆容量平衡點通常在6GB至8GB之間庭惜,當(dāng)然罩驻,以上這些也僅是經(jīng)驗之談,不同應(yīng)用需要量體裁衣地實際測試才能得出最合適的結(jié)論护赊,隨著HotSpot的開發(fā)者對G1的不斷優(yōu)化惠遏,也會讓對比結(jié)果繼續(xù)向G1傾斜。
低延遲垃圾收集器
HotSpot的垃圾收集器從Serial發(fā)展到CMS再到G1骏啰,經(jīng)歷了逾二十年時間节吮,經(jīng)過了數(shù)百上千萬臺服務(wù)器上的應(yīng)用實踐,已經(jīng)被淬煉得相當(dāng)成熟了器一,不過它們距離“完美”還是很遙遠课锌。怎樣的收集器才算是“完美”呢?這聽起來像是一道主觀題祈秕,其實不然渺贤,完美難以實現(xiàn)剑按,但是我們確實可以把它客觀描述出來庆揩。
衡量垃圾收集器的三項最重要的指標(biāo)是:內(nèi)存占用(Footprint)容达、吞吐量(Throughput)和延遲(Latency)瘫筐,三者共同構(gòu)成了一個“不可能三角”睛榄。三者總體的表現(xiàn)會隨技術(shù)進步而越來越好筏养,但是要在這三個方面同時具有卓越表現(xiàn)的“完美”收集器是極其困難甚至是不可能的都弹,一款優(yōu)秀的收集器通常最多可以同時達成其中的兩項仁锯。
在內(nèi)存占用仙蚜、吞吐量和延遲這三項指標(biāo)里此洲,延遲的重要性日益凸顯,越發(fā)備受關(guān)注委粉。其原因是隨著計算機硬件的發(fā)展呜师、性能的提升,我們越來越能容忍收集器多占用一點點內(nèi)存贾节;硬件性能增長汁汗,對軟件系統(tǒng)的處理能力是有直接助益的,硬件的規(guī)格和性能越高栗涂,也有助于降低收集器運行時對應(yīng)用程序的影響知牌,換句話說,吞吐量會更高斤程。但對延遲則不是這樣角寸,硬件規(guī)格提升,準(zhǔn)確地說是內(nèi)存的擴大,對延遲反而會帶來負(fù)面的效果袭厂,這點也是很符合直觀思維的:虛擬機要回收完整的1TB的堆內(nèi)存墨吓,毫無疑問要比回收1GB的堆內(nèi)存耗費更多時間。由此纹磺,我們就不難理解為何延遲會成為垃圾收集器最被重視的性能指標(biāo)了√妫現(xiàn)在我們來觀察一下現(xiàn)在已接觸過的垃圾收集器的停頓狀況,如圖3-14所示橄杨。
圖3-14中淺色階段表示必須掛起用戶線程秘症,深色表示收集器線程與用戶線程是并發(fā)工作的。由圖3-14可見式矫,在CMS和G1之前的全部收集器乡摹,其工作的所有步驟都會產(chǎn)生“Stop The World”式的停頓;CMS和G1分別使用增量更新和原始快照技術(shù)采转,實現(xiàn)了標(biāo)記階段的并發(fā)聪廉,不會因管理的堆內(nèi)存變大,要標(biāo)記的對象變多而導(dǎo)致停頓時間隨之增長故慈。但是對于標(biāo)記階段之后的處理板熊,仍未得到妥善解決。CMS使用標(biāo)記-清除算法察绷,雖然避免了整理階段收集器帶來的停頓干签,但是清除算法不論如何優(yōu)化改進,在設(shè)計原理上避免不了空間碎片的產(chǎn)生拆撼,隨著空間碎片不斷淤積最終依然逃不過“Stop The World”的命運容劳。G1雖然可以按更小的粒度進行回收,從而抑制整理階段出現(xiàn)時間過長的停頓闸度,但畢竟也還是要暫停的竭贩。
大家肯定也從圖3-14中注意到了,最后的兩款收集器莺禁,Shenandoah和ZGC留量,幾乎整個工作過程全部都是并發(fā)的,只有初始標(biāo)記睁宰、最終標(biāo)記這些階段有短暫的停頓肪获,這部分停頓的時間基本上是固定的寝凌,與堆的容量柒傻、堆中對象的數(shù)量沒有正比例關(guān)系。實際上较木,它們都可以在任意可管理的(譬如現(xiàn)在ZGC只能管理4TB以內(nèi)的堆)堆容量下红符,實現(xiàn)垃圾收集的停頓都不超過十毫秒這種以前聽起來是天方
夜譚、匪夷所思的目標(biāo)。這兩款目前仍處于實驗狀態(tài)的收集器预侯,被官方命名為“低延遲垃圾收集器”(Low-Latency Garbage Collector或者Low-Pause-Time Garbage Collector) 這里就不具體展開介紹了致开。
收集器的權(quán)衡
我們應(yīng)該如何選擇一款適合自己應(yīng)用的收集器呢?這個問題的答案主要受以下三個因素影響:
應(yīng)用程序的主要關(guān)注點是什么萎馅?如果是數(shù)據(jù)分析双戳、科學(xué)計算類的任務(wù),目標(biāo)是能盡快算出結(jié)果糜芳,那吞吐量就是主要關(guān)注點飒货;如果是SLA應(yīng)用,那停頓時間直接影響服務(wù)質(zhì)量峭竣,嚴(yán)重的甚至?xí)?dǎo)致事務(wù)超時塘辅,這樣延遲就是主要關(guān)注點;而如果是客戶端應(yīng)用或者嵌入式應(yīng)用皆撩,那垃圾收集的內(nèi)存占用則是不可忽視的扣墩。
運行應(yīng)用的基礎(chǔ)設(shè)施如何?譬如硬件規(guī)格扛吞,要涉及的系統(tǒng)架構(gòu)是x86-32/64呻惕、SPARC還是ARM/Aarch64;處理器的數(shù)量多少喻粹,分配內(nèi)存的大畜∪凇;選擇的操作系統(tǒng)是Linux守呜、Solaris還是Windows等型酥。
·使用JDK的發(fā)行商是什么?版本號是多少查乒?是ZingJDK/Zulu弥喉、OracleJDK、Open-JDK玛迄、OpenJ9抑或是其他公司的發(fā)行版由境?該JDK對應(yīng)了《Java虛擬機規(guī)范》的哪個版本?
一般來說蓖议,收集器的選擇就從以上這幾點出發(fā)來考慮虏杰。舉個例子,假設(shè)某個直接面向用戶提供服務(wù)的B/S系統(tǒng)準(zhǔn)備選擇垃圾收集器勒虾,一般來說延遲時間是這類應(yīng)用的主要關(guān)注點纺阔,那么:
如果你有充足的預(yù)算但沒有太多調(diào)優(yōu)經(jīng)驗,那么一套帶商業(yè)技術(shù)支持的專有硬件或者軟件解決方案是不錯的選擇修然,Azul公司以前主推的Vega系統(tǒng)和現(xiàn)在主推的Zing VM是這方面的代表笛钝,這樣你就可以使用傳說中的C4收集器了
如果你雖然沒有足夠預(yù)算去使用商業(yè)解決方案质况,但能夠掌控軟硬件型號,使用較新的版本玻靡,同時又特別注重延遲结榄,那ZGC很值得嘗試。
·如果你接手的是遺留系統(tǒng)囤捻,軟硬件基礎(chǔ)設(shè)施和JDK版本都比較落后臼朗,那就根據(jù)內(nèi)存規(guī)模衡量一下,對于大概4GB到6GB以下的堆內(nèi)存蝎土,CMS一般能處理得比較好依溯,而對于更大的堆內(nèi)存,可重點考察一下G1瘟则。
虛擬機及垃圾收集器日志
閱讀分析虛擬機和垃圾收集器的日志是處理Java虛擬機內(nèi)存問題必備的基礎(chǔ)技能黎炉,垃圾收集器日志是一系列人為設(shè)定的規(guī)則,多少有點隨開發(fā)者編碼時的心情而定醋拧,沒有任何的“業(yè)界標(biāo)準(zhǔn)”可言慷嗜,換句話說,每個收集器的日志格式都可能不一樣丹壕。除此以外還有一個麻煩庆械,在JDK 9以前,HotSpot并沒有提供統(tǒng)一的日志處理框架菌赖,虛擬機各個功能模塊的日志開關(guān)分布在不同的參數(shù)上缭乘,日志級別、循環(huán)
日志大小琉用、輸出格式堕绩、重定向等設(shè)置在不同功能上都要單獨解決。直到JDK 9邑时,這種混亂不堪的局面才終于消失奴紧,HotSpot所有功能的日志都收歸到了“-Xlog”參數(shù)上,這個參數(shù)的能力也相應(yīng)被極大拓展了:
-Xlog[:[selector][:[output][:[decorators][:output-options]]]]
命令行中最關(guān)鍵的參數(shù)是選擇器(Selector)晶丘,它由標(biāo)簽(Tag)和日志級別(Level)共同組成黍氮。標(biāo)簽可理解為虛擬機中某個功能模塊的名字,它告訴日志框架用戶希望得到虛擬機哪些功能的日志輸出浅浮。垃圾收集器的標(biāo)簽名稱為“gc”沫浆,由此可見,垃圾收集器日志只是HotSpot眾多功能日志的其中一項滚秩,全部支持的功能模塊標(biāo)簽名如下所示:
add专执,age,alloc叔遂,annotation他炊,aot,arguments已艰,attach痊末,barrier,biasedlocking哩掺,blocks凿叠,bot,breakpoint嚼吞,bytecode盒件,cens
日志級別從低到高,共有Trace舱禽,Debug炒刁,Info,Warning誊稚,Error翔始,Off六種級別,日志級別決定了輸出信息的詳細(xì)程度里伯,默認(rèn)級別為Info城瞎,HotSpot的日志規(guī)則與Log4j、SLF4j這類Java日志框架大體上是一致的疾瓮。另外脖镀,還可以使用修飾器(Decorator)來要求每行日志輸出都附加上額外的內(nèi)容,支持附加在日志行上的信息包括:
·time:當(dāng)前日期和時間狼电。
·uptime:虛擬機啟動到現(xiàn)在經(jīng)過的時間蜒灰,以秒為單位。
·timemillis:當(dāng)前時間的毫秒數(shù)肩碟,相當(dāng)于System.currentTimeMillis()的輸出卷员。
·uptimemillis:虛擬機啟動到現(xiàn)在經(jīng)過的毫秒數(shù)。
·timenanos:當(dāng)前時間的納秒數(shù)腾务,相當(dāng)于System.nanoTime()的輸出毕骡。
·uptimenanos:虛擬機啟動到現(xiàn)在經(jīng)過的納秒數(shù)。
·pid:進程ID岩瘦。
·tid:線程ID未巫。
·level:日志級別。
·tags:日志輸出的標(biāo)簽集启昧。
如果不指定叙凡,默認(rèn)值是uptime、level密末、tags這三個握爷,此時日志輸出類似于以下形式:
[3.080s][info][gc,cpu] GC(5) User=0.03s Sys=0.00s Real=0.01s
下面筆者舉幾個例子跛璧,展示在JDK 9統(tǒng)一日志框架前、后是如何獲得垃圾收集器過程的相關(guān)信息新啼,以下均以JDK 9的G1收集器(JDK 9下默認(rèn)收集器就是G1追城,所以命令行中沒有指定收集器)為例。
1)查看GC基本信息燥撞,在JDK 9之前使用-XX:+PrintGC座柱,JDK 9后使用-Xlog:gc:
bash-3.2$ java -Xlog:gc GCTest
[0.222s][info][gc] Using G1
[2.825s][info][gc] GC(0) Pause Young (G1 Evacuation Pause) 26M->5M(256M) 355.623ms
[3.096s][info][gc] GC(1) Pause Young (G1 Evacuation Pause) 14M->7M(256M) 50.030ms
[3.385s][info][gc] GC(2) Pause Young (G1 Evacuation Pause) 17M->10M(256M) 40.576ms
2)查看GC詳細(xì)信息,在JDK 9之前使用-XX:+PrintGCDetails物舒,在JDK 9之后使用-X-log:gc色洞,用通配符將GC標(biāo)簽下所有細(xì)分過程都打印出來,如果把日志級別調(diào)整到Debug或者Trace(基于版面篇幅考慮冠胯,例子中并沒有)火诸,還將獲得更多細(xì)節(jié)信息:
bash-3.2$ java -Xlog:gc* GCTest
[0.233s][info][gc,heap] Heap region size: 1M
[0.383s][info][gc ] Using G1
[0.383s][info][gc,heap,coops] Heap address: 0xfffffffe50400000, size: 4064 MB, Compressed Oops mode: Non-zero based:
0xfffffffe50000000, Oop shift amount: 3
[3.064s][info][gc,start ] GC(0) Pause Young (G1 Evacuation Pause)
gc,task ] GC(0) Using 23 workers of 23 for evacuation
[3.420s][info][gc,phases ] GC(0) Pre Evacuate Collection Set: 0.2ms
[3.421s][info][gc,phases ] GC(0) Evacuate Collection Set: 348.0ms
gc,phases ] GC(0) Post Evacuate Collection Set: 6.2ms
[3.421s][info][gc,phases ] GC(0) Other: 2.8ms
gc,heap ] GC(0) Eden regions: 24->0(9)
[3.421s][info][gc,heap ] GC(0) Survivor regions: 0->3(3)
[3.421s][info][gc,heap ] GC(0) Old regions: 0->2
[3.421s][info][gc,heap ] GC(0) Humongous regions: 2->1
[3.421s][info][gc,metaspace ] GC(0) Metaspace: 4719K->4719K(1056768K)
[3.421s][info][gc ] GC(0) Pause Young (G1 Evacuation Pause) 26M->5M(256M) 357.743ms
[3.422s][info][gc,cpu ] GC(0) User=0.70s Sys=5.13s Real=0.36s
[3.648s][info][gc,start ] GC(1) Pause Young (G1 Evacuation Pause)
[3.648s][info][gc,task ] GC(1) Using 23 workers of 23 for evacuation
[3.699s][info][gc,phases ] GC(1) Pre Evacuate Collection Set: 0.3ms
gc,phases ] GC(1) Evacuate Collection Set: 45.6ms
gc,phases ] GC(1) Post Evacuate Collection Set: 3.4ms
gc,phases ] GC(1) Other: 1.7ms
gc,heap ] GC(1) Eden regions: 9->0(10)
[3.699s][info][gc,heap ] GC(1) Survivor regions: 3->2(2)
[3.699s][info][gc,heap ] GC(1) Old regions: 2->5
[3.700s][info][gc,heap ] GC(1) Humongous regions: 1->1
[3.700s][info][gc,metaspace ] GC(1) Metaspace: 4726K->4726K(1056768K)
[3.700s][info][gc ] GC(1) Pause Young (G1 Evacuation Pause) 14M->7M(256M) 51.872ms
[3.700s][info][gc,cpu ] GC(1) User=0.56s Sys=0.46s Real=0.05s
.....
內(nèi)存分配與回收策略 實戰(zhàn)
Java技術(shù)體系的自動內(nèi)存管理,最根本的目標(biāo)是自動化地解決兩個問題:自動給對象分配內(nèi)存以及自動回收分配給對象的內(nèi)存荠察。關(guān)于回收內(nèi)存這方面惭蹂,已經(jīng)使用了大量篇幅去介紹虛擬機中的垃圾收集器體系以及運作原理,現(xiàn)在我們來探討一下關(guān)于給對象分配內(nèi)存的那些事兒割粮。
對象的內(nèi)存分配盾碗,從概念上講,應(yīng)該都是在堆上分配(而實際上也有可能經(jīng)過即時編譯后被拆散為標(biāo)量類型并間接地在棧上分配)舀瓢。在經(jīng)典分代的設(shè)計下廷雅,新生對象通常會分配在新生代中,少數(shù)情況下(例如對象大小超過一定閾值)也可能會直接分配在老年代京髓。對象分配的規(guī)則并不是固定的航缀,《Java虛擬機規(guī)范》并未規(guī)定新對象的創(chuàng)建和存儲細(xì)節(jié),這取決于虛擬機當(dāng)前使用的是哪一種垃圾收集器堰怨,以及虛擬機中與內(nèi)存相關(guān)的參數(shù)的設(shè)定芥玉。
對象優(yōu)先在Eden分配
大多數(shù)情況下,對象在新生代Eden區(qū)中分配备图。當(dāng)Eden區(qū)沒有足夠空間進行分配時灿巧,虛擬機將發(fā)起一次Minor GC。
HotSpot虛擬機提供了-XX:+PrintGCDetails這個收集器日志參數(shù)揽涮,告訴虛擬機在發(fā)生垃圾收集行為時打印內(nèi)存回收日志抠藕,并且在進程退出的時候輸出當(dāng)前的內(nèi)存各區(qū)域分配情況。在實際的問題排查中蒋困,收集器日志常會打印到文件后通過工具進行分析盾似,不過本節(jié)實驗的日志并不多,直接閱讀就能看得很清楚雪标。
在代碼清單3-7的testAllocation()方法中零院,嘗試分配三個2MB大小和一個4MB大小的對象溉跃,在運行時通過-Xms20M、-Xmx20M告抄、-Xmn10M這三個參數(shù)限制了Java堆大小為20MB撰茎,不可擴展,其中10MB分配給新生代玄妈,剩下的10MB分配給老年代。-XX:Survivor-Ratio=8決定了新生代中Eden區(qū)與一個Survivor區(qū)的空間比例是8∶1髓梅,從輸出的結(jié)果也清晰地看到“eden space 8192K拟蜻、from space 1024K、to
space 1024K”的信息枯饿,新生代總可用空間為9216KB(Eden區(qū)+1個Survivor區(qū)的總?cè)萘浚?br>
執(zhí)行testAllocation()中分配allocation4對象的語句時會發(fā)生一次Minor GC酝锅,這次回收的結(jié)果是新生代6651KB變?yōu)?48KB,而總內(nèi)存占用量則幾乎沒有減少(因為allocation1奢方、2搔扁、3三個對象都是存活的,虛擬機幾乎沒有找到可回收的對象)蟋字。產(chǎn)生這次垃圾收集的原因是為allocation4分配內(nèi)存時稿蹲,發(fā)現(xiàn)Eden已經(jīng)被占用了6MB,剩余空間已不足以分配allocation4所需的4MB內(nèi)存鹊奖,因此發(fā)生Minor GC苛聘。垃圾收集期間虛擬機又發(fā)現(xiàn)已有的三個2MB大小的對象全部無法放入Survivor空間(Survivor空間只有1MB大小)忠聚,所以只好通過分配擔(dān)保機制提前轉(zhuǎn)移到老年代去设哗。
這次收集結(jié)束后,4MB的allocation4對象順利分配在Eden中两蟀。因此程序執(zhí)行完的結(jié)果是Eden占用4MB(被allocation4占用)网梢,Survivor空閑,老年代被占用6MB(被allocation1赂毯、2战虏、3占用)。通過GC日志可以證實這一點党涕。
代碼清單3-7 新生代Minor GC
private static final int _1MB = 1024 * 1024;
/**
* VM參數(shù):-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
*/
public static void testAllocation() {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[2 * _1MB];
allocation2 = new byte[2 * _1MB];
allocation3 = new byte[2 * _1MB];
allocation4 = new byte[4 * _1MB]; // 出現(xiàn)一次Minor GC
}
[GC [DefNew: 6651K->148K(9216K), 0.0070106 secs] 6651K->6292K(19456K), 0.0070426 secs] [Times: user=0.00 s
Heap
def new generation total 9216K, used 4326K [0x029d0000, 0x033d0000, 0x033d0000)
eden space 8192K, 51% used [0x029d0000, 0x02de4828, 0x031d0000)
在jdk8中輸出結(jié)果是:
[GC (Allocation Failure) [PSYoungGen: 7010K->946K(9216K)] 7010K->5050K(19456K), 0.0043399 secs] [Times: user=0.02 sys=0.01, real=0.00 secs]
Heap
PSYoungGen total 9216K, used 8837K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
eden space 8192K, 96% used [0x00000007bf600000,0x00000007bfdb4b58,0x00000007bfe00000)
from space 1024K, 92% used [0x00000007bfe00000,0x00000007bfeec9c8,0x00000007bff00000)
to space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
ParOldGen total 10240K, used 4104K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
object space 10240K, 40% used [0x00000007bec00000,0x00000007bf002020,0x00000007bf600000)
Metaspace used 3882K, capacity 4568K, committed 4864K, reserved 1056768K
class space used 430K, capacity 460K, committed 512K, reserved 1048576K
貌似是提前進行了minor gc活烙?。
大對象直接進入老年代
大對象就是指需要大量連續(xù)內(nèi)存空間的Java對象遣鼓,最典型的大對象便是那種很長的字符串啸盏,或者元素數(shù)量很龐大的數(shù)組,本節(jié)例子中的byte[]數(shù)組就是典型的大對象骑祟。大對象對虛擬機的內(nèi)存分配來說就是一個不折不扣的壞消息回懦,比遇到一個大對象更加壞的消息就是遇到一群“朝生夕滅”的“短命大對象”气笙,我們寫程序的時候應(yīng)注意避免。在Java虛擬機中要避免大對象的原因是怯晕,在分配空間時潜圃,它容易導(dǎo)致內(nèi)存明明還有不少空間時就提前觸發(fā)垃圾收集,以獲取足夠的連續(xù)空間才能安置好它們舟茶,而當(dāng)復(fù)制對象時谭期,大對象就意味著高額的內(nèi)存復(fù)制開銷。HotSpot虛擬機提供了-XX:PretenureSizeThreshold參數(shù)吧凉,指定大于該設(shè)置值的對象直接在老年代分配隧出,這樣做的目的就是避免在Eden區(qū)及兩個Survivor區(qū)之間來回復(fù)制,產(chǎn)生大量的內(nèi)存復(fù)制操作阀捅。
執(zhí)行代碼清單3-8中的testPretenureSizeThreshold()方法后胀瞪,我們看到Eden空間幾乎沒有被使用,而老年代的10MB空間被使用了40%饲鄙,也就是4MB的allocation對象直接就分配在老年代中凄诞,這是因為XX:PretenureSizeThreshold被設(shè)置為3MB(就是3145728,這個參數(shù)不能與-Xmx之類的參數(shù)一樣直接寫3MB)忍级,因此超過3MB的對象都會直接在老年代進行分配帆谍。
注意 -XX:PretenureSizeThreshold參數(shù)只對Serial和ParNew兩款新生代收集器有效,HotSpot的其他新生代收集器轴咱,如Parallel Scavenge并不支持這個參數(shù)既忆。如果必須使用此參數(shù)進行調(diào)優(yōu),可考慮ParNew加CMS的收集器組合嗦玖。
private static final int _1MB = 1024 * 1024;
/**
* VM參數(shù):-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
* -XX:PretenureSizeThreshold=3145728
*/
public static void testPretenureSizeThreshold() {
byte[] allocation;
allocation = new byte[4 * _1MB]; //直接分配在老年代中
}
Heap
par new generation total 9216K, used 3282K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
eden space 8192K, 40% used [0x00000007bec00000, 0x00000007bef34a18, 0x00000007bf400000)
from space 1024K, 0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
to space 1024K, 0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
tenured generation total 10240K, used 4096K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
the space 10240K, 40% used [0x00000007bf600000, 0x00000007bfa00010, 0x00000007bfa00200, 0x00000007c0000000)
Metaspace used 3363K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 372K, capacity 388K, committed 512K, reserved 1048576K
長期存活的對象將進入老年代
HotSpot虛擬機中多數(shù)收集器都采用了分代收集來管理堆內(nèi)存患雇,那內(nèi)存回收時就必須能決策哪些存活對象應(yīng)當(dāng)放在新生代,哪些存活對象放在老年代中宇挫。為做到這點苛吱,虛擬機給每個對象定義了一個對象年齡(Age)計數(shù)器,存儲在對象頭中.對象通常在Eden區(qū)里誕生器瘪,如果經(jīng)過第一次Minor GC后仍然存活翠储,并且能被Survivor容納的話,該對象會被移動到Survivor空間中橡疼,并且將其對象年齡設(shè)為1歲援所。對象在Survivor區(qū)中每熬過一次Minor GC,年齡就增加1歲欣除,當(dāng)它的年齡增加到一定程度(默認(rèn)為15)住拭,就會被晉升到老年代中。對象晉升老年代的年齡閾值,可以通過參數(shù)-XX:MaxTenuringThreshold設(shè)置滔岳。
讀者可以試試分別以-XX:MaxTenuringThreshold=1和-XX:MaxTenuringThreshold=15兩種設(shè)置來執(zhí)行代碼清單3-9中的testTenuringThreshold()方法杠娱,此方法中allocation1對象需要256KB內(nèi)存,Survivor空間可以容納谱煤。當(dāng)-XX:MaxTenuringThreshold=1時摊求,allocation1對象在第二次GC發(fā)生時進入老年代,新生代已使用的內(nèi)存在垃圾收集以后非常干凈地變成0KB刘离。而當(dāng)-XX:MaxTenuringThreshold=15時室叉,第二次GC發(fā)生后,allocation1對象則還留在新生代Survivor空間硫惕,這時候新生代仍然有404KB被占用茧痕。
代碼清單3-9 長期存活的對象進入老年代
private static final int _1MB = 1024 * 1024;
/**
* VM參數(shù):-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1
* -XX:+PrintTenuringDistribution
*/
@SuppressWarnings("unused")
public static void testTenuringThreshold() {
byte[] allocation1, allocation2, allocation3;
allocation1 = new byte[_1MB / 4]; // 什么時候進入老年代決定于XX:MaxTenuringThreshold設(shè)置
allocation2 = new byte[4 * _1MB];
allocation3 = new byte[4 * _1MB];
allocation3 = null;
allocation3 = new byte[4 * _1MB];
}
以-XX:MaxTenuringThreshold=1參數(shù)來運行的結(jié)果:
以-XX:MaxTenuringThreshold=15參數(shù)來運行的結(jié)果:
動態(tài)對象年齡判定
為了能更好地適應(yīng)不同程序的內(nèi)存狀況,HotSpot虛擬機并不是永遠要求對象的年齡必須達到XX:MaxTenuringThreshold才能晉升老年代疲憋,如果在Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半凿渊,年齡大于或等于該年齡的對象就可以直接進入老年代梁只,無須等到-XX:MaxTenuringThreshold中要求的年齡缚柳。
空間分配擔(dān)保
在發(fā)生Minor GC之前,虛擬機必須先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對象總空間搪锣,如果這個條件成立秋忙,那這一次Minor GC可以確保是安全的。如果不成立构舟,則虛擬機會先查看XX:HandlePromotionFailure參數(shù)的設(shè)置值是否允許擔(dān)保失敾易贰(Handle Promotion Failure);如果允許狗超,那會繼續(xù)檢查老年代最大可用的連續(xù)空間是否大于歷次晉升到老年代對象的平均大小弹澎,如果大
于,將嘗試進行一次Minor GC努咐,盡管這次Minor GC是有風(fēng)險的苦蒿;如果小于,或者-XX:HandlePromotionFailure設(shè)置不允許冒險渗稍,那這時就要改為進行一次Full GC佩迟。
解釋一下“冒險”是冒了什么風(fēng)險:前面提到過,新生代使用復(fù)制收集算法竿屹,但為了內(nèi)存利用率报强,只使用其中一個Survivor空間來作為輪換備份,因此當(dāng)出現(xiàn)大量對象在Minor GC后仍然存活的情況——最極端的情況就是內(nèi)存回收后新生代中所有對象都存活拱燃,需要老年代進行分配擔(dān)保秉溉,把Survivor無法容納的對象直接送入老年代,這與生活中貸款擔(dān)保類似。老年代要進行這樣的擔(dān)保坚嗜,前提是老年代
本身還有容納這些對象的剩余空間夯膀,但一共有多少對象會在這次回收中活下來在實際完成內(nèi)存回收之前是無法明確知道的,所以只能取之前每一次回收晉升到老年代對象容量的平均大小作為經(jīng)驗值苍蔬,與老年代的剩余空間進行比較诱建,決定是否進行Full GC來讓老年代騰出更多空間。
取歷史平均值來比較其實仍然是一種賭概率的解決辦法碟绑,也就是說假如某次Minor GC存活后的對象突增俺猿,遠遠高于歷史平均值的話,依然會導(dǎo)致?lián)J「裰佟H绻霈F(xiàn)了擔(dān)保失敗押袍,那就只好老老實實地重新發(fā)起一次Full GC,這樣停頓時間就很長了凯肋。雖然擔(dān)保失敗時繞的圈子是最大的谊惭,但通常情況下都還是會將-XX:HandlePromotionFailure開關(guān)打開