談垃圾回收器之前,要先講講垃圾回收算法,以及JVM對(duì)垃圾的認(rèn)定策略,JVM垃圾回收器是垃圾回收算法的具體實(shí)現(xiàn),了解了前面的前置知識(shí),有利于對(duì)垃圾回收器的理解。 什么是垃圾?垃圾,主要是指堆上的對(duì)象,那么如何確定這些對(duì)象是可以被回收的呢? 大概思路就是,如果一個(gè)對(duì)象永遠(yuǎn)不可能被訪問到,那么就是垃圾,可以被回收了如何確定對(duì)象永遠(yuǎn)不會(huì)被使用呢? 引用計(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ī)里面都沒有選用引用計(jì)數(shù)算法來管理內(nèi)存,主要原因是,這個(gè)看似簡(jiǎn)單的算法有很多例外情況要考慮,必須要配合大量額外處理才能保證正確地工作,譬如單純的引用計(jì)數(shù)就很難解決對(duì)象之間相互循環(huán)引用的問題。 如圖,每一個(gè)對(duì)象的引用都是1,構(gòu)成了循環(huán)引用,但是并不能被其他對(duì)象訪問,這兩個(gè)對(duì)象再無任何引用,引用計(jì)數(shù)算法也就無法回收它們。 代碼驗(yàn)證:
執(zhí)行結(jié)果:
為了篇幅,我將部分打印內(nèi)容省略了,可見System.gc()后內(nèi)存占用由14M->0M,將對(duì)象這10M釋放了。也就是JVM里面并沒使用引用計(jì)數(shù)法來標(biāo)記垃圾。 根可達(dá)算法這個(gè)算法的基本思路就是通過一系列稱為“GC Roots”的根對(duì)象作為起始節(jié)點(diǎn)集,從這些節(jié)點(diǎn)開始,根據(jù)引用關(guān)系向下搜索,搜索過程所走過的路徑稱為“引用鏈”(Reference Chain),如果某個(gè)對(duì)象到GC Roots間沒有任何引用鏈相連,或者用圖論的話來說就是從GC Roots到這個(gè)對(duì)象不可達(dá)時(shí),則證明此對(duì)象是不可能再被使用的。 在Java技術(shù)體系里面,固定可作為GC Roots的對(duì)象包括以下幾種:
垃圾回收算法本文介紹了常見的三種垃圾回收算法(mark-sweep,mark-compact,mark-copy),是java虛擬機(jī)各種垃圾收集器的算法基礎(chǔ)。 垃圾回收算法思想當(dāng)前商業(yè)虛擬機(jī)的垃圾收集器,大多數(shù)都遵循了“分代收集”(Generational Collection)的理論進(jìn)行設(shè)計(jì),分代收集名為理論,實(shí)質(zhì)是一套符合大多數(shù)程序運(yùn)行實(shí)際情況的經(jīng)驗(yàn)法則,它建立在兩個(gè)分代假說之上: 這兩個(gè)分代假說共同奠定了多款常用的垃圾收集器的一致的設(shè)計(jì)原則:收集器應(yīng)該將Java堆劃分出不同的區(qū)域,然后將回收對(duì)象依據(jù)其年齡(年齡即對(duì)象熬過垃圾收集過程的次數(shù))分配到不同的區(qū)域之中存儲(chǔ)顯而易見,如果一個(gè)區(qū)域中大多數(shù)對(duì)象都是朝生夕滅,難以熬過垃圾收集過程的話,那么把它們集中放在一起,每次回收時(shí)只關(guān)注如何保留少量存活而不是去標(biāo)記那些大量將要被回收的對(duì)象,就能以較低代價(jià)回收到大量的空間; 如果剩下的都是難以消亡的對(duì)象,那把它們集中放在一塊,虛擬機(jī)便可以使用較低的頻率來回收這個(gè)區(qū)域,這就同時(shí)兼顧了垃圾收集的時(shí)間開銷和內(nèi)存的空間有效利用。 標(biāo)記-清除算法 Mark-Sweep算法分為“標(biāo)記”和“清除”兩個(gè)階段:首先標(biāo)記出所有需要回收的對(duì)象,在標(biāo)記完成后,統(tǒng)一回收掉所有被標(biāo)記的對(duì)象,也可以反過來,標(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è)過程的執(zhí)行效率都隨對(duì)象數(shù)量增長(zhǎng)而降低; 第二個(gè)是內(nèi)存空間的碎片化問題,標(biāo)記、清除之后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會(huì)導(dǎo)致當(dāng)以后在程序運(yùn)行過程中需要分配較大對(duì)象時(shí)無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動(dòng)作。 標(biāo)記-復(fù)制 Mark-Copy標(biāo)記-復(fù)制算法常被簡(jiǎn)稱為復(fù)制算法它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對(duì)象復(fù)制到另外一塊上面,然后再把已使用過的內(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)行高效,不過其缺陷也顯而易見,這種復(fù)制回收算法的代價(jià)是將可用內(nèi)存縮小為了原來的一半,空間浪費(fèi)未免太多了。 標(biāo)記-壓縮 Mark-Compact標(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%存活的極端情況,所以在老年代一般不能直接選用這種算法。 標(biāo)記-壓縮算法其中的標(biāo)記過程仍然與“標(biāo)記-清除”算法一樣,但后續(xù)步驟不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有存活的對(duì)象都向內(nèi)存空間一端移動(dòng),然后直接清理掉邊界以外的內(nèi)存: 標(biāo)記-清除算法與標(biāo)記-整理算法的本質(zhì)差異在于前者是一種非移動(dòng)式的回收算法,而后者是移動(dòng)式的。是否移動(dòng)回收后的存活對(duì)象是一項(xiàng)優(yōu)缺點(diǎn)并存的風(fēng)險(xiǎn)決策:如果移動(dòng)存活對(duì)象,尤其是在老年代這種每次回收都有大量對(duì)象存活區(qū)域,移動(dòng)存活對(duì)象并更新所有引用這些對(duì)象的地方將會(huì)是一種極為負(fù)重的操作,而且這種對(duì)象移動(dòng)操作必須全程暫停用戶應(yīng)用程序(STW問題)才能進(jìn)行 。 垃圾處理器基于上面的三種垃圾回收算法,衍生出7種垃圾回收器: Serial收集器這個(gè)收集器是一個(gè)單線程工作的收集器,但它的“單線程”的意義并不僅僅是說明它只會(huì)使用一個(gè)處理器或一條收集線程去完成垃圾收集工作,更重要的是強(qiáng)調(diào)在它進(jìn)行垃圾收集時(shí),必須暫停其他所有工作線程,直到它收集結(jié)束。 迄今為止,它依然是HotSpot虛擬機(jī)運(yùn)行在客戶端模式下的默認(rèn)新生代收集器,有著優(yōu)于其他收集器的地方,那就是簡(jiǎn)單而高效(與其他收集器的單線程相比),對(duì)于內(nèi)存資源受限的環(huán)境,它是所有收集器里額外內(nèi)存消耗(Memory Footprint) [1] 最小的;對(duì)于單核處理器或處理器核心數(shù)較少的環(huán)境來說,Serial收集器由于沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率。Serial收集器對(duì)于運(yùn)行在客戶端模式下的虛擬機(jī)來說是一個(gè)很好的選擇。 ParNew收集器ParNew收集器實(shí)質(zhì)上是Serial收集器的多線程并行版本,除了同時(shí)使用多條線程進(jìn)行垃圾收集之 ParNew收集器除了支持多線程并行收集之外,其他與Serial收集器相比并沒有太多創(chuàng)新之處,但它 ParNew收集器在單核心處理器的環(huán)境中絕對(duì)不會(huì)有比Serial收集器更好的效果,甚至由于存在線程 Parallel Scavenge收集器Parallel Scavenge收集器也是一款新生代收集器,它同樣是基于標(biāo)記-復(fù)制算法實(shí)現(xiàn)的收集器,也是能夠并行收集的多線程收集器……Parallel Scavenge的諸多特性從表面上看和ParNew非常相似,那它有什么特別之處呢? Parallel Scavenge收集器的特點(diǎn)是它的關(guān)注點(diǎn)與其他收集器不同,CMS等收集器的關(guān)注點(diǎn)是盡可能地縮短垃圾收集時(shí)用戶線程的停頓時(shí)間,而Parallel Scavenge收集器的目標(biāo)則是達(dá)到一個(gè)可控制的吞吐量(Throughput)。所謂吞吐量就是處理器用于運(yùn)行用戶代碼的時(shí)間與處理器總消耗時(shí)間的比值,即: \[吞吐量 = \frac{運(yùn)行用戶代碼時(shí)間}{運(yùn)行用戶代碼時(shí)間 運(yùn)行垃圾收集器的時(shí)間} \] 如果虛擬機(jī)完成某個(gè)任務(wù),用戶代碼加上垃圾收集總共耗費(fèi)了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。停頓時(shí)間越短就越適合需要與用戶交互或需要保證服務(wù)響應(yīng)質(zhì)量的程序,良好的響應(yīng)速度能提升用戶體驗(yàn);而高吞吐量則可以最高效率地利用處理器資源,盡快完成程序的運(yùn)算任務(wù),主要適合在后臺(tái)運(yùn)算而不需要太多交互的分析任務(wù)。 由于與吞吐量關(guān)系密切,Parallel Scavenge收集器也經(jīng)常被稱作“吞吐量?jī)?yōu)先收集器”。 Serial Old收集器Serial Old是Serial收集器的老年代版本,它同樣是一個(gè)單線程收集器,使用標(biāo)記-整理算法。這個(gè)收集器的主要意義也是供客戶端模式下的HotSpot虛擬機(jī)使用。如果在服務(wù)端模式下,它也可能有兩種用途:一種是在JDK 5以及之前的版本中與Parallel Scavenge收集器搭配使用,另外一種就是作為CMS收集器發(fā)生失敗時(shí)的后備預(yù)案,在并發(fā)收集發(fā)生Concurrent Mode Failure時(shí)使用。 Parallel Old收集器Parallel Old是Parallel Scavenge收集器的老年代版本,支持多線程并發(fā)收集,基于標(biāo)記-整理算法實(shí)現(xiàn)。這個(gè)收集器是直到JDK 6時(shí)才開始提供的,在此之前,新生代的Parallel Scavenge收集器一直處于相當(dāng)尷尬的狀態(tài),原因是如果新生代選擇了Parallel Scavenge收集器,老年代除了Serial Old(PSMarkSweep)收集器以外別無選擇,其他表現(xiàn)良好的老年代收集器,如CMS無法與它配合工作。 由于老年代Serial Old收集器在服務(wù)端應(yīng)用性能上的“拖累”,使用Parallel Scavenge收集器也未必能在整體上獲得吞吐量最大化的效果。 同樣,由于單線程的老年代收集中無法充分利用服務(wù)器多處理器的并行處理能力,在老年代內(nèi)存空間很大而且硬件規(guī)格比較高級(jí)的運(yùn)行環(huán)境中,這種組合的總吞吐量甚至不一定比ParNew加CMS的組合來得優(yōu)秀。直到Parallel Old收集器出現(xiàn)后,“吞吐量?jī)?yōu)先”收集器終于有了比較名副其實(shí)的搭配組合,在注重吞吐量或者處理器資源較為稀缺的場(chǎng)合,都可以優(yōu)先考慮Parallel Scavenge加Parallel Old收集器這個(gè)組合。 CMS收集器CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時(shí)間為目標(biāo)的收集器。目前很大一部分的Java應(yīng)用集中在互聯(lián)網(wǎng)網(wǎng)站或者基于瀏覽器的B/S系統(tǒng)的服務(wù)端上,這類應(yīng)用通常都會(huì)較為關(guān)注服務(wù)的響應(yīng)速度,希望系統(tǒng)停頓時(shí)間盡可能短,以給用戶帶來良好的交互體驗(yàn)。CMS收集器就非常符合這類應(yīng)用的需求。 從名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于標(biāo)記-清除算法實(shí)現(xiàn)的,它的運(yùn)作過程相對(duì)于前面幾種收集器來說要更復(fù)雜一些,整個(gè)過程分為四個(gè)步驟,包括: 其中初始標(biāo)記、重新標(biāo)記這兩個(gè)步驟仍然需要“Stop The World”。初始標(biāo)記僅僅只是標(biāo)記一下GCRoots能直接關(guān)聯(lián)到的對(duì)象,速度很快;并發(fā)標(biāo)記階段就是從GC Roots的直接關(guān)聯(lián)對(duì)象開始遍歷整個(gè)對(duì)象圖的過程,這個(gè)過程耗時(shí)較長(zhǎng)但是不需要停頓用戶線程,可以與垃圾收集線程一起并發(fā)運(yùn)行;而重新標(biāo)記階段則是為了修正并發(fā)標(biāo)記期間,因用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分對(duì)象的標(biāo)記記錄,這個(gè)階段的停頓時(shí)間通常會(huì)比初始標(biāo)記階段稍長(zhǎng)一些,但也遠(yuǎn)比并發(fā)標(biāo)記階段的時(shí)間短;最后是并發(fā)清除階段,清理刪除掉標(biāo)記階段判斷的已經(jīng)死亡的對(duì)象,由于不需要移動(dòng)存活對(duì)象,所以這個(gè)階段也是可以與用戶線程同時(shí)并發(fā)的。由于在整個(gè)過程中耗時(shí)最長(zhǎng)的并發(fā)標(biāo)記和并發(fā)清除階段中,垃圾收集器線程都可以與用戶線程一起工作,所以從總體上來說,CMS收集器的內(nèi)存回收過程是與用戶線程一起并發(fā)執(zhí)行的。 優(yōu)點(diǎn):并發(fā)收集、低停頓 ?2.無法處理“浮動(dòng)垃圾”(Floating Garbage) ?3.空間碎片 Garbage First收集器Garbage First(簡(jiǎn)稱G1)收集器是垃圾收集器技術(shù)發(fā)展歷史上的里程碑式的成果,它開創(chuàng)了收集器面向局部收集的設(shè)計(jì)思路和基于Region的內(nèi)存布局形式。G1是一款主要面向服務(wù)端應(yīng)用的垃圾收集器。 在G1收集器出現(xiàn)之前的所有其他收集器,包括CMS在內(nèi),垃圾收集的目標(biāo)范圍要么是整個(gè)新生代(Minor GC),要么就是整個(gè)老年代(Major GC),再要么就是整個(gè)Java堆(Full GC)。而G1跳出了這個(gè)樊籠,它可以面向堆內(nèi)存任何部分來組成回收集(Collection Set,一般簡(jiǎn)稱CSet)進(jìn)行回收,衡量標(biāo)準(zhǔn)不再是它屬于哪個(gè)分代,而是哪塊內(nèi)存中存放的垃圾數(shù)量最多,回收收益最大,這就是G1收集器的Mixed GC模式。G1開創(chuàng)的基于Region的堆內(nèi)存布局是它能夠?qū)崿F(xiàn)這個(gè)目標(biāo)的關(guān)鍵。雖然G1也仍是遵循分代收集理論設(shè)計(jì)的,但其堆內(nèi)存的布局與其他收集器有非常明顯的差異:G1不再堅(jiān)持固定大小以及固定數(shù)量的分代區(qū)域劃分,而是把連續(xù)的Java堆劃分為多個(gè)大小相等的獨(dú)立區(qū)域(Region),每一個(gè)Region都可以根據(jù)需要,扮演新生代的Eden空間、Survivor空間,或者老年代空間。收集器能夠?qū)Π缪莶煌巧腞egion采用不同的策略去處理,這樣無論是新創(chuàng)建的對(duì)象還是已經(jīng)存活了一段時(shí)間、熬過多次收集的舊對(duì)象都能獲取很好的收集效果。 Region中還有一類特殊的Humongous區(qū)域,專門用來存儲(chǔ)大對(duì)象。G1認(rèn)為只要大小超過了一個(gè)Region容量一半的對(duì)象即可判定為大對(duì)象。每個(gè)Region的大小可以通過參數(shù)-XX:G1HeapRegionSize設(shè)定,取值范圍為1MB~32MB,且應(yīng)為2的N次冪。而對(duì)于那些超過了整個(gè)Region容量的超級(jí)大對(duì)象,將會(huì)被存放在N個(gè)連續(xù)的Humongous Region之中,G1的大多數(shù)行為都把Humongous Region作為老年代的一部分來進(jìn)行看待,如圖3-12所示。 雖然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它們都是一系列區(qū)域(不需要連續(xù))的動(dòng)態(tài)集合。G1收集器之所以能建立可預(yù)測(cè)的停頓時(shí)間模型,是因?yàn)樗鼘egion作為單次回收的最小單元,即每次收集到的內(nèi)存空間都是Region大小的整數(shù)倍,這樣可以有計(jì)劃地避免在整個(gè)Java堆中進(jìn)行全區(qū)域的垃圾收集。更具體的處理思路是讓G1收集器去跟蹤各個(gè)Region里面的垃圾堆積的“價(jià)值”大小,價(jià)值即回收所獲得的空間大小以及回收所需時(shí)間的經(jīng)驗(yàn)值,然后在后臺(tái)維護(hù)一個(gè)優(yōu)先級(jí)列表,每次根據(jù)用戶設(shè)定允許的收集停頓時(shí)間(使用參數(shù)-XX:MaxGCPauseMillis指定,默認(rèn)值是200毫秒),優(yōu)先處理回收價(jià)值收益最大的那些Region,這也就是“Garbage First”名字的由來。這種使用Region劃分內(nèi)存空間,以及具有優(yōu)先級(jí)的區(qū)域回收方式,保證了G1收集器在有限的時(shí)間內(nèi)獲取盡可能高的收集效率。 垃圾處理器總結(jié)目前是新生代老年代垃圾回收器組合方式: |
|