許多高級編程語言都帶有自動垃圾回收特性仲吏,以將程序員從繁瑣復(fù)雜的內(nèi)存分配和釋放工作中解脫。本文將概述常見的垃圾回收算法正什,并詳細(xì)介紹JVM眾多垃圾回收器的特性、區(qū)別和選擇指北号杏。
垃圾回收算法
垃圾回收算法本質(zhì)上是“識別已不會再被使用的內(nèi)存數(shù)據(jù)并將其清除釋放”的算法婴氮。其中“識別”這一階段,主要有兩種方式:
引用計數(shù)法
統(tǒng)計每個存活對象的引用次數(shù)盾致,引用次數(shù)為0的對象視為待回收的垃圾對象主经。
引用計數(shù)法的弊端是無法回收循環(huán)引用的對象。如A引用B庭惜,B引用A罩驻,同時A和B都沒有其他引用,此時A和B應(yīng)該都是待回收的垃圾對象护赊,但引用計數(shù)法無法識別惠遏。
可達性分析法
從GC Roots開始遍歷內(nèi)存池中的所有對象,任何能夠從GC Roots直接或間接引用到的對象便被標(biāo)記為“可達”骏啰,待遍歷完成后节吮,所有未被標(biāo)記為“可達”的對象即為待回收的垃圾對象
GC Roots:
垃圾回收器用于進行可達性分析的根對象,GC Roots通常會有多個判耕,以Java為例透绩,一個運行中的JVM,其GC Roots可能包括:
Class - 由系統(tǒng)類加載器(system class loader)加載的對象
Thread - 所有活著的線程
Stack Local - Java方法的local變量或參數(shù)
JNI Local - JNI方法的local變量或參數(shù)
JNI Global - 全局JNI引用
Monitor Used - 用于同步的監(jiān)控對象
Held by JVM - 用于JVM特殊目的由GC保留的對象,但實際上這個與JVM的實現(xiàn)是有關(guān)的帚豪√季梗可能已知的一些類型是:系統(tǒng)類加載器、一些JVM知道的重要的異常類狸臣、一些用于處理異常的預(yù)分配對象以及一些自定義的類加載器等
由于引用計數(shù)法的弊端莹桅,已幾乎不再被現(xiàn)代的編程語言使用。而可達性分析法則得到了廣泛使用固棚,幾乎是所有垃圾回收算法標(biāo)配的垃圾識別方式统翩。
接下來,詳細(xì)介紹一下常用的垃圾回收算法:
標(biāo)記-清除算法(Mark-Sweep)
也被稱為Mark-Clean算法
標(biāo)記階段:從GC Roots開始進行可達性分析此洲,將所有可達對象標(biāo)記出來
清除階段:將所有未標(biāo)記可達的對象清理掉
標(biāo)記-清除算法帶來的問題是內(nèi)存碎片厂汗,被清除掉的對象會在內(nèi)存中留下很多的“洞”,這些“洞”的大小可能不足以容下新創(chuàng)建的對象呜师,隨著碎片的增多娶桦,內(nèi)存池中實際可用的內(nèi)存可能會變得越來越少
拷貝算法
- 將內(nèi)存劃分為兩個區(qū)域,一次只使用其中一個(暫稱為from區(qū))
- 在進行可達性分析的同時汁汗,將可達對象復(fù)制到另一個空白的內(nèi)存區(qū)域中(暫稱為to區(qū))
- 可達性分析完成后衷畦,將to區(qū)與from區(qū)的身份互換,同時將原先的from區(qū)(現(xiàn)在的to區(qū))中的對象清除
拷貝算法可以解決內(nèi)存碎片的問題知牌,因為向to區(qū)中復(fù)制的對象使用的都是連續(xù)的內(nèi)存空間祈争。同時拷貝算法的效率要比標(biāo)記-清除算法高很多(尤其是在存活對象少、垃圾對象多的場景下)角寸,但代價是占用了冗余的內(nèi)存空間菩混。
標(biāo)記-壓縮算法(Mark-Sweep-Compact)
也被稱為標(biāo)記-整理算法、Mark-Compact算法扁藕、MSC算法
與標(biāo)記-清除算法類似沮峡,區(qū)別在于在標(biāo)記階段完成后,將所有可達對象移動到內(nèi)存池的另一端亿柑,形成一整塊連續(xù)的內(nèi)存空間邢疙,然后再將范圍外的內(nèi)存池清空。
標(biāo)記-壓縮算法的好處是不會產(chǎn)生內(nèi)存碎片望薄,也不會占用冗余的內(nèi)存空間疟游,但代價是需要付出額外的工作將存活對象移動,效率低于標(biāo)記-清除算法式矫。
增量回收(Incremental Collecting)
增量回收與上述的垃圾回收算法不是相互替代的關(guān)系乡摹,可以將其理解為一種標(biāo)準(zhǔn)垃圾回收算法的使用方式。
在垃圾回收可達性分析的過程中采转,為了避免分析過程中產(chǎn)生新的對象影響分析的準(zhǔn)確性聪廉,通常會將進程置于STW(Stop-The-World)狀態(tài)瞬痘,即在垃圾回收時掛起所有線程,暫停除垃圾回收外的一切工作板熊。進程占用的內(nèi)存空間越大框全,待回收的垃圾對象越多,STW持續(xù)的時間就越長干签。
增量回收的基本思想是每次只收集一小片區(qū)域的內(nèi)存空間津辩,然后恢復(fù)其他線程的工作,依次反復(fù)容劳,直到垃圾收集完成喘沿。使用這種方式能夠避免長時間的STW。但是竭贩,因為線程切換和上下文轉(zhuǎn)換的消耗蚜印,會使得垃圾回收的總體成本上升,造成系統(tǒng)吞吐量的下降留量。
分代回收(Generational Collecting)
不同的垃圾回收算法有不同的優(yōu)勢和代價窄赋,簡單地選用其中一種無法應(yīng)對可能出現(xiàn)的各種場景。分代回收的思想是將內(nèi)存池按特點劃分為多個區(qū)域楼熄,針對每個區(qū)域的特點應(yīng)用不同的垃圾回收算法忆绰。
以Hotspot VM為例,將堆內(nèi)存劃分為新生代(Young Generation)和老年代(Old Generation)兩個區(qū)域可岂。新生代中存儲新誕生的對象错敢,老年代中存儲長時間存活的對象。這樣可以讓新生代始終處于存活對象少缕粹、垃圾對象多的狀態(tài)伐债,需要比較頻繁的進行垃圾回收,適合使用拷貝算法致开;而老年代則始終處于存活對象多,垃圾對象少的狀態(tài)萎馅,不需要頻繁進行垃圾回收双戳,適合使用標(biāo)記-壓縮算法。
JVM垃圾回收器
本節(jié)將以HotSpot VM為例糜芳,介紹JVM中各種垃圾回收器
Serial
使用拷貝算法的垃圾回收器飒货,只能用于新生代。Serial回收器使用單線程進行垃圾回收峭竣。
SerialOld
使用標(biāo)記-壓縮算法的垃圾回收器塘辅,只能用于老年代。使用單線程進行垃圾回收皆撩。
Serial+SerialOld回收器的組合是JVM最基礎(chǔ)的垃圾回收器組合扣墩,只使用單CPU哲银,STW時間較長,適用于處理能力不強的主機和對STW時長要求不高的軟件呻惕,JVM如以client模式啟動荆责,則默認(rèn)使用Serial+SerialOld回收器。
在啟動參數(shù)中指定-XX:+UseSerialGC亚脆,會啟用Serial+SerialOld組合
Serial和SerialOld回收器適用于只有單核CPU的運行環(huán)境做院,效率比較低,一般情況下濒持,只用于開發(fā)環(huán)境或桌面程序
ParNew
Serial回收器的多線程版本键耕,只能用于新生代。使用拷貝算法柑营,多線程并行工作屈雄。在多CPU主機上的性能高于Serial,單CPU主機上的性能低于Serial由境。
在啟動參數(shù)中指定-XX:+UseParNewGC棚亩,會使用ParNew+SerialOld的回收器組合
Parallel Scavenge
與ParNew一樣,都是用于新生代的并行拷貝算法回收器虏杰。區(qū)別在于Parallel Scavenge回收器可以控制新生代垃圾回收的STW時間讥蟆。
可以通過參數(shù)-XX:MaxGCPauseMillis設(shè)置最大的STW時間,這樣Parallel Scavenge回收器就會通過增加或減少回收次數(shù)的方式把每次STW時長盡可能控制在指定的范圍內(nèi)纺阔。還可以通過-XX:GCTimeRatio參數(shù)指定用于垃圾回收的時間最大占比瘸彤。
在使用Parallel Scavenge回收器時,最好通過參數(shù)-XX:+UseAdaptiveSizePolicy開啟回收器的自適應(yīng)策略笛钝,開啟后JVM可以根據(jù)實際的情況動態(tài)調(diào)整新生代中Eden质况、S0、S1三個分區(qū)的大小比例玻靡,以及晉升老年代對象的年齡等參數(shù)结榄,以滿足指定的最大STW市場和最大垃圾回收時間占比要求。
在啟動參數(shù)中指定-XX:+UseParallelGC囤捻,會使用Parallel Scavenge+SerialOld的回收器組合臼朗。(Hotspot VM中,此時老年代的回收器被稱為PS MarkSweep蝎土,其實本質(zhì)上就是SerialOld视哑,具體可見R大對此的說明:http://hllvm.group.iteye.com/group/topic/27629)
Parallel Old
Parallel Scavenge的老年代版本,于JDK1.6中面世誊涯。在Parallel Old誕生之前挡毅,Parallel Scavenge處于一個比較尷尬的境地,由于沒有對應(yīng)的老年代回收器與其配合暴构,僅在新生代使用Parallel算法很難達到對整體垃圾回收時長和STW時長的控制目的跪呈。
而現(xiàn)在段磨,我們可以使用Parallel Scavenge+Parallel Old這一組合來解決這一問題
在啟動參數(shù)中指定-XX:+UseParallelOldGC,會使用Parallel Scavenge+Parallel Old的回收器組合
作為服務(wù)端的JAVA程序庆械,使用Parallel Scavenge+Parallel Old的回收器組合算是基礎(chǔ)標(biāo)配了薇溃,這兩款回收器的組合能夠有效地利用服務(wù)器的多CPU配置,提高垃圾回收效率缭乘。并且足夠簡單明了沐序,不怎么需要進行精細(xì)的參數(shù)調(diào)優(yōu)。
然而堕绩,如果應(yīng)用程序?qū)TW時長有非常嚴(yán)格的要求策幼,那么HotSpot VM還提供了更強大的垃圾回收器:
Concurrent Mark Sweep(CMS)
CMS是標(biāo)記-清除的改進算法,用于老年代奴紧,能夠有效減少STW時長特姐。
CMS是一種比較復(fù)雜的垃圾回收算法,此處盡可能進行簡明扼要的介紹:
CMS將標(biāo)記-清除細(xì)分為6個階段:
- 初始標(biāo)記
- 并發(fā)標(biāo)記
- 并發(fā)預(yù)清理
- 重新標(biāo)記
- 并發(fā)清理
- 并發(fā)重置
詳細(xì)過程:
- 初始標(biāo)記階段中黍氮,CMS回收器標(biāo)記出被GC Roots直接引用的對象唐含,這一過程需要STW,但由于只進行深度為1的遍歷沫浆,耗時很短捷枯。
- 并發(fā)標(biāo)記階段中,CMS回收器以初始標(biāo)記階段標(biāo)記出的存活對象為根進行可達性遍歷专执。在這一階段中淮捆,不需要STW,其他線程可正常運行本股。
- 并發(fā)預(yù)清理階段中攀痊,CMS回收器對并發(fā)標(biāo)記階段中老年代新增的對象重新進行標(biāo)記。這一階段存在的目的是盡可能減少下一階段“重新標(biāo)記”的STW時長拄显。
- 重新標(biāo)記階段會進入STW苟径,然后進行一次完整的可達性分析,由于前面三個階段已經(jīng)完成了絕大部分的工作躬审,所以這一階段的STW會很短涩笤。
- 并發(fā)清理階段不需要STW,垃圾回收線程清理標(biāo)記出的垃圾對象盒件,同時其他線程可以正常工作。
- 并發(fā)重置階段中舱禽,重置CMS回收器的數(shù)據(jù)結(jié)構(gòu)炒刁,等待下一次垃圾回收。
可以看出來誊稚,CMS回收器的思路是把標(biāo)記-清除算法的工作拆分成多個步驟翔始,其中可以并行的盡可能并行罗心,以達到STW時長最小化的目標(biāo)。
同樣地城瞎,CMS回收器也存在著弊端:
- 對CPU要求高渤闷,由于部分標(biāo)記和清除階段是免STW并且多線程并行的,這就給CPU增加了很大的線程切換壓力脖镀,核數(shù)少的CPU使用CMS回收器的效果并不好飒箭,多核多CPU的高性能主機更加適合使用
- 垃圾回收的同時老年代中仍然在產(chǎn)生新的對象,這是CMS的并行機制導(dǎo)致的蜒灰。所以CMS回收器不能在老年代滿時才開始工作弦蹂,Hotspot VM 6/7中,CMS回收器在老年代使用率92%時便開始工作强窖。如果在CMS垃圾回收的過程中凸椿,新增的對象占滿了剩余的8%空間,便會導(dǎo)致CMS回收失敗翅溺,自動降級至SerialOld重新進行垃圾回收脑漫。(CMS垃圾回收觸發(fā)的時機可以使用-XX:CMSInitiatingOccupancyFraction參數(shù)進行設(shè)置)
- CMS使用的是標(biāo)記-清除算法,而不是標(biāo)記-壓縮算法咙崎,這就導(dǎo)致會出現(xiàn)大量的內(nèi)存碎片优幸。隨著內(nèi)存碎片的增多,最終勢必會出現(xiàn)垃圾回收的并發(fā)階段中內(nèi)存不足的情況叙凡,如上所說劈伴,此時CMS回收器會自動降級為SerialOld回收器,以標(biāo)記-壓縮算法進行垃圾回收握爷,同時也就會整理好內(nèi)存碎片跛璧。
綜上所述可以看出,CMS回收器的機制比前述的任何一種都要精細(xì)和復(fù)雜新啼,同時對CPU資源的要求也要高出很多追城,以犧牲性能的代價換取最少的STW時長。
在啟動參數(shù)中指定-XX:+UseConcMarkSweepGC燥撞,會開啟ParNew+CMS的回收器組合座柱。
CMS回收器在控制STW時長的表現(xiàn)上要比Parallel Old好很多,然而代價是占用更多的計算資源物舒,如果服務(wù)器的計算資源有冗余色洞,使用CMS是更好的選擇。
Garbage-First(G1)
G1回收器誕生于Hotspot VM的7update4版本冠胯,這一最新型的垃圾回收器吸取了CMS回收器的經(jīng)驗和教訓(xùn)火诸,旨在解決CMS回收器的各類弊端,同時提供更短更可控的STW時長荠察。
G1的機制比CMS更加復(fù)雜置蜀,此處同樣盡可能簡明的進行介紹:
與其他回收器不同奈搜,G1是一個全代回收器,同時負(fù)責(zé)新生代和老年代的垃圾回收工作盯荤。
G1回收器打破了Hotspot VM以往的分代概念馋吗,新生代的Eden、S0秋秤、S1宏粤,以及老年代不再是物理分隔,而變成了靈活的邏輯分隔航缀。G1將堆內(nèi)存劃分為2000個左右相等的內(nèi)存塊商架,每個內(nèi)存塊的大小為1-32Mb。每個內(nèi)存塊可以作為Eden芥玉、S0蛇摸、S1或老年代使用,也就是說這些塊的身份是不固定的灿巧。隨著每次垃圾回收的完成赶袄,有些塊的內(nèi)存會被完全釋放掉,成為空白塊抠藕,而這些空白塊在接下來可能成為任何一種角色饿肺。
G1在進行Young GC(對新生代進行的垃圾回收)時,會觸發(fā)STW盾似,并行地將所有Eden塊和Survivor From塊中的存活對象拷貝至一或多個Survivor To塊中敬辣,然后將Eden塊和Survivor From塊清除成為空白塊。接下來零院,G1會根據(jù)各種信息動態(tài)地計算并重新分配接下來Eden區(qū)和Survivor區(qū)應(yīng)占的內(nèi)存空間大小溉跃。
G1在進行Old GC(對老年代進行的垃圾回收)時,會采用類似于CMS回收器的多個階段進行:
- 初始標(biāo)記:標(biāo)記出對老年代對象有引用的內(nèi)存塊告抄,需要STW
- 并行標(biāo)記:針對初始標(biāo)記出的內(nèi)存塊進行可達性分析撰茎,并計算每個內(nèi)存塊的活躍度(可以理解為該塊中的可達對象占比)。如果在這一環(huán)節(jié)中發(fā)現(xiàn)某個內(nèi)存塊中所有的對象都不可達打洼,則將該塊標(biāo)記出來龄糊。
- 重新標(biāo)記:將并行標(biāo)記階段中發(fā)現(xiàn)的整體不可達內(nèi)存塊徹底清除,同時進行一次完整的可達性分析和活躍度分析募疮,需要STW(由于1炫惩、2環(huán)節(jié)的工作,這一STW過程會很短)
- 拷貝/清除:這一階段中阿浓,G1會選取活躍度較低的內(nèi)存塊他嚷,對其進行并行的垃圾回收,具體的垃圾回收算法和G1的Young GC一樣,即將待清理的內(nèi)存塊中的存活對象拷貝至一或多個內(nèi)存塊爸舒。需要STW
值得注意的是,G1的垃圾回收并非每次都將所有不可達對象完全清除稿蹲,G1傾向于優(yōu)先清除活躍度低的內(nèi)存塊扭勉,因為這些塊中的存活對象少(或者壓根沒有存活對象),清理速度更快苛聘。對于那些活躍度高的內(nèi)存塊涂炎,G1會放置不管,直至某個時間點該塊的活躍度足夠低時再進行回收设哗。
正因為如此唱捣,G1進行垃圾回收的時機更早,默認(rèn)在堆內(nèi)存使用率達到45%時就會開始垃圾回收网梢。這體現(xiàn)了G1(Garbage-First/垃圾回收優(yōu)先)這一名字的含義震缭。
至于活躍度多低才會進行回收,則是由G1決定的战虏,G1會調(diào)整自己的回收策略來盡可能滿足用戶設(shè)置的最大STW時長拣宰。
同時,由于使用拷貝算法烦感,G1回收器不會產(chǎn)生內(nèi)存碎片巡社,這也是相較CMS的巨大優(yōu)勢之一。
G1回收器能夠根據(jù)需要動態(tài)調(diào)整各個分代的內(nèi)存占比手趣,在快速實例化大量對象時表現(xiàn)尤佳晌该。
G1回收器接收一個最大STW時長的任務(wù)指標(biāo),并會努力控制每次垃圾回收的STW時長來完成這一指標(biāo)绿渣。默認(rèn)的指標(biāo)是200ms朝群,注意不要給G1設(shè)置過于過分的任務(wù)指標(biāo),否則它為了完成任務(wù)可能是會亂來的……
G1適合運行在配有強大CPU的主機上的怯晕,且占用較大堆內(nèi)存空間的應(yīng)用程序中潜圃。如果你想要得到最佳的STW時長,并且愿意為此進行一些參數(shù)調(diào)優(yōu)工作的話舟茶,使用G1回收器通常來說是最好的選擇谭期。
通過參數(shù)-XX:+UseG1GC 來啟用G1回收器,并使用-XX:MaxGCPauseMillis=n來設(shè)置最大的STW時長吧凉,-XX:InitiatingHeapOccupancyPercent=n來設(shè)置觸發(fā)垃圾回收的堆內(nèi)存占用比