垃圾收集器與回收策略
JAVA 虛擬機器收集垃圾的區域:
【垃圾回收主要是指方法區和堆記憶體的回收,這些區域的記憶體是變化的。其它區域的記憶體跟隨方法的結束或者執行緒的結束而自動回收】
程式計數器、虛擬機器棧、本地方法棧3個區域隨執行緒而生,隨執行緒而滅;棧中的棧幀隨著方法的進入和退出而有條不紊地執行著出棧和入棧操作。因此這幾個區域的記憶體分配和回收都具備確定性,在這幾個區域內就不需要過多考慮回收的問題,因為方法結束或者執行緒結束時,記憶體自然就跟隨著回收了。而Java堆和方法區則不一樣,一個介面中的多個實現類需要的記憶體可能不一樣,一個方法中的多個分支需要的記憶體也可能不一樣,我們只有在程式處於執行期間時才能知道會建立哪些物件,這部分記憶體的分配和回收都是動態的,垃圾收集器所關注的是這部分記憶體,“記憶體”分配與回收也僅指這一部分記憶體。
堆裡面存放著Java世界中幾乎所有的物件例項,垃圾收集器在對堆進行回收前,第一件事情就是要確定這些物件之中哪些還“存活”著,哪些已經“死去”(即不可能再被任何途徑使用的物件)。判斷物件是否已經死去的方法主要有引用計數法和可達性分析演算法。 引用計數法就是通過計數器來計算,當物件被引用時計數器加1,引用失效時計數器就減去1。但是這對於有迴圈引用的物件,通過該演算法管理記憶體就不太可靠。 可達性分析演算法的思路是是通過一系列的稱為“GC Roots”的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈(Reference Chain),當一個物件到GC Roots沒有任何引用鏈相連(用圖論的話來說,就是從GC Roots到這個物件不可達)時,則證明此物件是不可用的。如圖所示,物件object 5、object 6、object 7雖然互相有關聯,但是它們到GC Roots是不可達的,所以它們將會被判定為是可回收的物件。
在Java語言中GC Roots的物件包括:
虛擬機器棧(棧幀中的本地變量表)中引用的物件
方法區中類靜態屬性引用的物件、方法區中常量引用的物件
本地方法棧中JNI(即一般說的Native方法)引用的物件
方法區的回收:
Java虛擬機器規範中確實說過可以不要求虛擬機器在方法區實現垃圾收集,而且在方法區中進行垃圾收集的“價效比”一般比較低:在堆中,尤其是在新生代中,常規應用進行一次垃圾收集一般可以回收70%~95%的空間,而永久代的垃圾收集效率遠低於此。永久代的垃圾收集主要回收兩部分內容:廢棄常量和無用的類。回收廢棄常量與回收Java堆中的物件非常類似。沒有任何物件引用常量池中的常量可被回收,如字串“abc", 如果沒有任何String物件引用常量池中的“abc"常量,也沒有其它地方引用這個字面量,就可以被清理出常量池。常量池中的其他類(介面)、方法、欄位的符號引用也與此類似。
判斷常量池比較容易,但是判斷類是否無用就需要同時滿足如下三種情況才能被認為是無用的常量類:
該類所有的例項都已經被回收,也就是Java堆中不存在該類的任何例項。
載入該類的ClassLoader已經被回收。
該類對應的java.lang.Class 物件沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
垃圾回收演算法:
標記清除演算法(Mark-Sweep):
主要分為“標記”和“清除”兩個階段:首先標記出所有需要回收的物件,在標記完成後統一回收所有被標記的物件。
它的主要不足有兩個:一個是效率問題,標記和清除兩個過程的效率都不高;另一個是空間問題,標記清除之後會產生大量不連續的記憶體碎片,空間碎片太多可能會導致以後在程式執行過程中需要分配較大物件時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。
複製演算法(Copying):
它將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的物件複製到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。這樣使得每次都是對整個半區進行記憶體回收,記憶體分配時也就不用考慮記憶體碎片等複雜情況,只要移動堆頂指標,按順序分配記憶體即可,實現簡單,執行高效。只是這種演算法的代價是將記憶體縮小為了原來的一半,代價有些高。
【一個Eden,兩個Survivor,比例一般為8:1:1,可用空間為90%,10%的空間用來垃圾回收時複製物件所用,當然如果這10%不夠用,可以找老年代進行分配擔保】
現在的商業虛擬機器都採用這種收集演算法來回收新生代,IBM公司的專門研究表明,新生代中的物件98%是“朝生夕死”的,所以並不需要按照1∶1的比例來劃分記憶體空間,而是將記憶體分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor[插圖]。當回收時,將Eden和Survivor中還存活著的物件一次性地複製到另外一塊Survivor空間上,最後清理掉Eden和剛才用過的Survivor空間。HotSpot虛擬機器預設Eden和Survivor的大小比例是8∶1,也就是每次新生代中可用記憶體空間為整個新生代容量的90% (80%+10%),只有10%的記憶體會被“浪費”。當然,98%的物件可回收只是一般場景下的資料,我們沒有辦法保證每次回收都只有不多於10%的物件存活,當Survivor空間不夠用時,需要依賴其他記憶體(這裡指老年代)進行分配擔保(Handle Promotion)。
標記-整理演算法:
【老年代的物件存活率很高,如果採取一半一半的複製演算法,那麼就近乎於來回複製。為了避免複製,同時又想消除碎片記憶體的影響,可以採取適用於老年代的標記整理演算法】
標記過程仍然與“標記-清除”演算法一樣,但後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體。
分代收集演算法:
【新生代採用複製演算法,老年代採取標記—清理或者標記整理】
當前商業虛擬機器的垃圾收集都採用“分代收集”(Generational Collection)演算法,這種演算法並沒有什麼新的思想,只是根據物件存活週期的不同將記憶體劃分為幾塊。一般是把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集演算法。在新生代中,每次垃圾收集時都發現有大批物件死去,只有少量存活,那就選用複製演算法,只需要付出少量存活物件的複製成本就可以完成收集。而老年代中因為物件存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記—清理”或者“標記—整理”演算法來進行回收。
列舉根節點:
【列舉根節點的過程非常耗時,主要體現在查詢物件的引用,為了減少遍地查詢的時間,通過OopMap的資料結構來儲存物件引用的資訊】
可達性分析從GC Roots節點找引用鏈這個操作為例,可作為GC Roots的節點主要在全域性性的引用(例如常量或類靜態屬性)與執行上下文(例如棧幀中的本地變量表)中,現在很多應用僅僅方法區就有數百兆,如果要逐個檢查這裡面的引用,那麼必然會消耗很多時間。
另外,可達性分析對執行時間的敏感還體現在GC停頓上,因為這項分析工作必須在一個能確保一致性的快照中進行——這裡“一致性”的意思是指在整個分析期間整個執行系統看起來就像被凍結在某個時間點上,不可以出現分析過程中物件引用關係還在不斷變化的情況,該點不滿足的話分析結果準確性就無法得到保證。這點是導致GC進行時必須停頓所有Java執行執行緒(Sun將這件事情稱為“Stop TheWorld”)的其中一個重要原因,即使是在號稱(幾乎)不會發生停頓的CMS收集器中,列舉根節點時也是必須要停頓的。
目前的主流Java虛擬機器使用的都是準確式GC,所以當執行系統停頓下來後,並不需要一個不漏地檢查完所有執行上下文和全域性的引用位置,虛擬機器應當是有辦法直接得知哪些地方存放著物件引用。在HotSpot的實現中,是使用一組稱為OopMap的資料結構來達到這個目的的,在類載入完成的時候,HotSpot就把物件內什麼偏移量上是什麼型別的資料計算出來,在JIT編譯過程中,也會在特定的位置記錄下棧和暫存器中哪些位置是引用。
垃圾收集器:
新生代收集器:都採用複製演算法實現。
老年代收集器:CMS採用標記—清除演算法;Serial Old採用標記—整理演算法; Parallel Old採用標記—整理演算法。
Serial收集器:
新生代收集器,需要“Stop The World”。對於限定單個CPU的環境來說,Serial收集器由於沒有執行緒互動的開銷,專心做垃圾收集自然可以獲得最高的單執行緒收集效率。
Serial/Serial Old 收集器
ParNew收集器:
Server模式下的虛擬機器中首選的新生代收集器。除了Serial收集器外,目前新生代收集器只有它能與CMS收集器配合工作。
ParNew/Serial Old收集器
Parallel Scavenge收集器:
Parallel Scavenge收集器是一個新生代收集器,它也是使用複製演算法的收集器,又是並行的多執行緒收集器。
Parallel Scavenge收集器的特點是它的關注點與其他收集器不同,CMS等收集器的關注點是儘可能地縮短垃圾收集時使用者執行緒的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量(Throughput)。(謂吞吐量就是CPU用於執行使用者程式碼的時間與CPU總消耗時間的比值,即吞吐量 = 執行使用者程式碼時間 /(執行使用者程式碼時間 +垃圾收集時間),虛擬機器總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%)
停頓時間越短就越適合需要與使用者互動的程式,良好的響應速度能提升使用者體驗,而高吞吐量則可以高效率地利用CPU時間,儘快完成程式的運算任務,主要適合在後臺運算而不需要太多互動的任務。
Parallel Scavenge收集器提供了兩個引數用於精確控制吞吐量,分別是控制最大垃圾收集停頓時間的-XX:MaxGCPauseMillis引數以及直接設定吞吐量大小的-XX:GCTimeRatio引數。GCTimeRatio引數的值應當是一個大於0且小於100的整數,也就是垃圾收集時間佔總時間的比率,相當於是吞吐量的倒數。如果把此引數設定為19,那允許的最大GC時間就佔總時間的5%(即1 /(1+19)),預設值為99,就是允許最大1%(即1 /(1+99))的垃圾收集時間。
自適應調整策略:Parallel Scavenge收集器有一個引數-XX:+UseAdaptiveSizePolicy值得關注。這是一個開關引數,當這個引數開啟之後,就不需要手工指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRatio)、晉升老年代物件年齡(-XX:PretenureSizeThreshold)等細節引數了,虛擬機器會根據當前系統的執行情況收集效能監控資訊,動態調整這些引數以提供最合適的停頓時間或者最大的吞吐量,這種調節方式稱為GC自適應的調節策略(GC Ergonomics)。
Serial Old收集器:
Serial Old是Serial收集器的老年代版本,主要用於JDK 1.5以及之前的版本中與Parallel Scavenge收集器搭配使,另一種用途就是作為CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用。
Parallel Old收集器:
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多執行緒和“標記-整理”演算法。
Parallel Scavenge/Parallel Old
CMS收集器:
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分的Java應用集中在網際網路站或者B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給使用者帶來較好的體驗。CMS收集器就非常符合這類應用的需求。
過程:
初始標記(CMS initial mark):只是標記一下GC Roots能直接關聯到的物件,速度很快
併發標記(CMS concurrent mark):進行GC Roots Tracing的過程
重新標記(CMS remark):修正併發標記期間因使用者程式繼續運作而導致標記產生變動的那一部分物件的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短
併發清除(CMS concurrent sweep)
由於整個過程中耗時最長的併發標記和併發清除過程收集器執行緒都可以與使用者執行緒一起工作,所以,從總體上來說,CMS收集器的記憶體回收過程是與使用者執行緒一起併發執行的。
優點:併發收集、低停頓
缺點:1 在併發階段,它雖然不會導致使用者執行緒停頓,但是會因為佔用了一部分執行緒(或者說CPU資源)而導致應用程式變慢,總吞吐量會降低。
2 CMS收集器無法處理浮動垃圾(Floating Garbage),可能出現“Concurrent Mode Failure”失敗而導致另一次Full GC的產生。(浮動垃圾:由於CMS併發清理階段使用者執行緒還在執行著,伴隨程式執行自然就還會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之後,CMS無法在當次收集中處理掉它們,只好留待下一次GC時再清理掉。這一部分垃圾就稱為“浮動垃圾”。)
3 CMS是一款基於“標記—清除”演算法實現的收集器,收集結束時會有大量空間碎片產生。空間碎片過多時,將會給大物件分配帶來很大麻煩,往往會出現老年代還有很大空間剩餘,但是無法找到足夠大的連續空間來分配當前物件,不得不提前觸發一次Full GC。
G1收集器
G1具備如下特點:
並行與併發:G1能充分利用多CPU、多核環境下的硬體優勢,使用多個CPU(CPU或者CPU核心)來縮短Stop-The-World停頓的時間,部分其他收集器原本需要停頓Java執行緒執行的GC動作,G1收集器仍然可以通過併發的方式讓Java程式繼續執行。
分代收集:與其他收集器一樣,分代概念在G1中依然得以保留。雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但它能夠採用不同的方式去處理新建立的物件和已經存活了一段時間、熬過多次GC的舊物件以獲取更好的收集效果。
空間整合:與CMS的“標記—清理”演算法不同,G1從整體來看是基於“標記—整理”演算法實現的收集器,從區域性(兩個Region之間)上來看是基於“複製”演算法實現的,但無論如何,這兩種演算法都意味著G1運作期間不會產生記憶體空間碎片,收集後能提供規整的可用記憶體。這種特性有利於程式長時間執行,分配大物件時不會因為無法找到連續記憶體空間而提前觸發下一次GC。
可預測的停頓:這是G1相對於CMS的另一大優勢,降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已經是實時Java(RTSJ)的垃圾收集器的特徵了。
操作過程:
初始標記(Initial Marking)
併發標記(Concurrent Marking)
最終標記(Final Marking)
篩選回收(Live Data Counting and Evacuation)
G1收集器和CMS收集器的區別:
CMS特點:併發,低停頓
缺點:對CPU非常敏感,無法處理浮動垃圾,記憶體碎片過多時,會產生full gc
G1特點: 是一款面向服務端應用的垃圾收集器,並行於併發,分代收集,空間整合,可預測的停頓。
空間整合:由於G1使用了獨立區域(Region)概念,G1從整體來看是基於“標記-整理”演算法實現收集,從區域性(兩個Region)上來看是基於“複製”演算法實現的,但無論如何,這兩種演算法都意味著G1運作期間不會產生記憶體空間碎片。
可預測的停頓:這是G1相對於CMS的另一大優勢,降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用這明確指定一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒)
記憶體回收策略:
新生代GC(Minor GC):指發生在新生代的垃圾收集動作,因為Java物件大多都具備朝生夕滅的特性,所以Minor GC非常頻繁,一般回收速度也比較快。
老年代GC(Major GC / Full GC):指發生在老年代的GC,出現了MajorGC,經常會伴隨至少一次的Minor GC(但非絕對的,在Parallel Scavenge收集器的收集策略裡就有直接進行Major GC的策略選擇過程)。Major GC的速度一般會比Minor GC慢10倍以上。
大物件直接進入老年代:虛擬機器提供了一個-XX:PretenureSizeThreshold引數,令大於這個設定值的物件直接在老年代分配。這樣做的目的是避免在Eden區及兩個Survivor區之間發生大量的記憶體複製。
長期存活的物件將進入老年代:虛擬機器給每個物件定義了一個物件年齡(Age)計數器。如果物件在Eden出生並經過第一次MinorGC後仍然存活,並且能被Survivor容納的話,將被移動到Survivor空間中,並且物件年齡設為1。物件在Survivor區中每“熬過”一次Minor GC,年齡就增加1歲,當它的年齡增加到一定程度(預設為15歲),就將會被晉升到老年代中。物件晉升老年代的年齡閾值,可以通過引數-XX:MaxTenuringThreshold設定。