垃圾收集器

Java與C++之間有一堵由內(nèi)存動態(tài)分配和垃圾收集技術(shù)所圍成的高墻,墻外面的人想進(jìn)去,墻里面的人卻想出來。

概述

GC這項技術(shù)不是Java語言的伴生物,其歷史遠(yuǎn)比Java久遠(yuǎn)。1960年誕生于MIT的Lisp是第一門真正使用動態(tài)內(nèi)存分配和垃圾收集技術(shù)的語言。
Lisp還處于胚胎時期,人們就在思考GC需要完成的三件事情:

  • 哪些內(nèi)存需要回收
  • 什么時候回收
  • 如何回收

Java內(nèi)存運(yùn)行區(qū)域中的程序計數(shù)器、虛擬機(jī)棧、本地方法棧三個區(qū)域隨線程而生,隨線程而死。這幾個區(qū)域的內(nèi)存分配和回收都具備確定性,不需要過度考慮回收的問題,因為隨著方法結(jié)束或者線程結(jié)束時,內(nèi)存自然就跟著回收了。
Java堆和方法區(qū)則不一樣,一個接口中多個實現(xiàn)類需要的內(nèi)存可能不一樣,一個方法中的多個分支需要的內(nèi)存也可能不一樣,只有在運(yùn)行期間才知道會創(chuàng)建哪些對象,這部分內(nèi)存的分配和回收都是動態(tài)。GC所關(guān)注的就是這部分內(nèi)存。

判斷對象存活的算法

引用計數(shù)算法

給對象添加一個引用計數(shù)器,每當(dāng)有一個地方引用它時,計數(shù)器值就加1;當(dāng)引用失效時,計數(shù)器值就減1;任何時刻計數(shù)器都為0的對象就是不可能再被使用的。
但是,Java語言沒有選用引用計數(shù)算法來管理內(nèi)存,其中最主要的原因是它很難解決對象之間的循環(huán)引用問題。

根搜索算法(GC Roots Tracing)

Java和C#,甚至包括之前的Lisp都是使用此算法來判斷對象是否存活。
基本思路:通過一系列名為"GC Roots"的對象作為起始點,從這些節(jié)點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當(dāng)一個對象到GC Roots沒有任何引用鏈相連(GC Roots到這個對象不可達(dá))時,則證明此對象是不可用的。
Java語言里,可作為GC Roots的對象包括下面幾種:

  • 虛擬機(jī)棧(棧幀中的本地變量表)中的引用的對象。
  • 方法區(qū)的類靜態(tài)屬性引用的對象
  • 方法區(qū)中的常量引用的對象
  • 本地方法棧中JNI的引用的對象
引用分類

JDK 1.2之前,Java中對引用的定義很傳統(tǒng):如果reference類型的數(shù)據(jù)中存儲的數(shù)值代表的是另外一塊內(nèi)存的起始地址,就稱這塊內(nèi)存代表著一個引用。 這種定義很狹隘,一個對象在這種定義下只有被引用或者沒有被引用兩種狀態(tài)。對于如何描述一些“食之無味,棄之可惜”的對象就顯得無能為力。我們希望能描述這樣的一些對象:當(dāng)內(nèi)存空間還足夠時,則能保留在內(nèi)存中,如果內(nèi)存在進(jìn)行垃圾收集后還是非常緊張,則可以拋棄這些對象。
JDK 1.2后,Java對引用的概念進(jìn)行了擴(kuò)充,將引用分為強(qiáng)引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)四種,這四種引用強(qiáng)度依次逐漸減弱。

  • 強(qiáng)引用就是指在程序代碼中普遍存在的,類似“Object obj = new Object()”這類的引用,只要強(qiáng)引用還存在,GC永遠(yuǎn)不會回收掉被引用的對象。
  • 軟引用用來描述一些還有用,但并非必須的對象。對于軟引用關(guān)聯(lián)的對象,系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會把這些對象列進(jìn)回收范圍之中并進(jìn)行第二次回收。如果這次回收還是沒有足夠內(nèi)存,才會拋出內(nèi)存溢出異常。
  • 弱引用也是用來描述非必需對象的,但是它的強(qiáng)度比軟引用更弱一些,被弱引用關(guān)聯(lián)的對象只能生存道下一次GC收集發(fā)生之前。當(dāng)GC工作時,無論當(dāng)前內(nèi)存是否足夠,都會回收掉只被弱引用關(guān)聯(lián)的對象
  • 虛引用也被稱為幽靈引用,它是最弱的一種引用關(guān)系。一個對象是否有虛引用的存在,完全不會對其生存時間構(gòu)成影響,也無法通過虛引用來取得一個對象實。為一個對象設(shè)置虛引用關(guān)聯(lián)的唯一目的是希望能在這個對象被回收時收到一個系統(tǒng)通知。
對象什么時候判定為死亡

根搜索算法中不可達(dá)的對象,會被第一次標(biāo)記并且進(jìn)行一次篩選,篩選條件是此對象是否有必要執(zhí)行finalize()方法,當(dāng)對象沒有覆蓋finalize()方法,或者finalize()方法已經(jīng)被虛擬機(jī)調(diào)用過,虛擬機(jī)將這兩種情況都視為“沒有必要執(zhí)行”。
如果對象被判定為有必要執(zhí)行finalize()方法,則會被放置到一個名為F-Queue隊列中,并在稍后由虛擬機(jī)自動建立的、低優(yōu)先級的Finalizer線程去執(zhí)行。這里“執(zhí)行”是指虛擬機(jī)會觸發(fā)這個方法,但是不保證會等到它運(yùn)行結(jié)束。finalize()方法是對象逃脫死亡命運(yùn)的最后一次機(jī)會,稍后GC將對F-Queue中的對象進(jìn)行第二次小規(guī)模的標(biāo)記。

回收方法區(qū)

方法區(qū)(HotSpot虛擬機(jī)中的永久代)進(jìn)行垃圾收集的性價比比較低,主要回收兩部分內(nèi)容:廢棄常量無用的類。
判定一個常量是否是“廢棄常量”比較簡單,而要判定一個類是否是“無用的類”的條件則相對苛刻許多,類需要同時滿足下面3個條件才能算是“無用的類”:

  • 該類所有的實例都已經(jīng)被回收,也就是Java堆中不存在該類的任何實例
  • 加載該類的ClassLoader已經(jīng)被回收
  • 該類對應(yīng)的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
    虛擬機(jī)可以對滿足上述3個條件的無用類進(jìn)行回收,僅僅是“可以”,而不是和對象一樣,不使用了就必然會回收。

垃圾收集算法

標(biāo)記-清除算法

最基礎(chǔ)的收集算法是“標(biāo)記-清除”(Mark-Sweep)算法。分為“標(biāo)記”和“清除”兩個階段:首先標(biāo)記出所有需要回收的對象,在標(biāo)記完成后統(tǒng)一回收掉所有被標(biāo)記的對象。之所以說是最基礎(chǔ)的收集算法,是因為后續(xù)的收集算法都是基于這種思路并對其缺點進(jìn)行改進(jìn)而得到的
主要有兩個缺點:一個是效率問題,標(biāo)記和清除過程的效率都不高;二是空間問題,標(biāo)記清除后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,碎片太多后會導(dǎo)致,當(dāng)程序在以后運(yùn)行過程中需要分配較大對象時無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動作。

復(fù)制算法

為了解決效率問題,一種被稱為“復(fù)制”的收集算法出現(xiàn)了。它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中一塊,當(dāng)這塊內(nèi)存用完了,就將還存活的對象依次復(fù)制到另一塊上面,然后把已使用的內(nèi)存空間一次清理掉。
這種算法缺點是內(nèi)存使用率不高,只有原來的一半。
現(xiàn)代商業(yè)虛擬機(jī)都采用這種收集算法來回收新生代,但并不按照1:1來劃分空間,而是將內(nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中的一塊Survivor。當(dāng)回收時,將Eden和Survivor中還存活著的對象一次性地拷貝到另外一塊Survivor空間,最后清理掉Eden和剛才用過的Survivor的空間。HotSpot虛擬機(jī)默認(rèn)Eden和Survivor的大小比例是8:1。

標(biāo)記-整理算法

復(fù)制收集算法在對象存活率較高時就要執(zhí)行較多的復(fù)制操作,效率將會變低。在老年代一般不能直接選用這種方法。
根據(jù)年老代的特點,有人提出了“標(biāo)記-整理”(Mark-Compact)算法,標(biāo)記后,不是直接對可回收的對象進(jìn)行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內(nèi)存。

分代收集算法

當(dāng)前商業(yè)虛擬機(jī)都采用“分代收集”(GenerationalCollection)算法,根據(jù)對象的存活周期的不同將內(nèi)存劃分為幾塊。一般Java堆分為新生代和老年代,根據(jù)各個年代的特點采用最適當(dāng)?shù)氖占惴ā?br> 新生代中,存活率較低,采用復(fù)制算法,而老年代因為對象存活率高、沒有額外空間對它進(jìn)行分配擔(dān)保,就必須使用“標(biāo)記-清理”或“標(biāo)記-整理”算法來進(jìn)行回收。

垃圾收集器

垃圾收集器是內(nèi)存回收的具體實現(xiàn)。不同廠商、不同版本的虛擬機(jī)所提供的垃圾收集器可能會有很大的差別,并且一般會提供參數(shù)供用戶根據(jù)自己的應(yīng)用特點和要求組合出各個年代所使用的收集器。

HotSpot JVM1.6的垃圾收集器

展示了7種作用于不同分代的收集器,如果兩個收集器之間存在連線,就說明它們可以搭配使用。

Serial 收集器

最基本、歷史最悠久的收集器,曾經(jīng)(JDK 1.3.1之前)是虛擬機(jī)新生代唯一的選擇。
這個收集器是一個單線程的收集器,“單線程”的意義并不僅僅是說明它只會使用一個CPU或一條收集線程去完成垃圾收集工作,更重要的是在它進(jìn)行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結(jié)束

Serial/Serial Old收集器運(yùn)行示意圖
ParNew 收集器

ParNew收集器其實就是Serial收集器的多線程版本(并行收集器),除了使用多線程進(jìn)行垃圾收集之外,其余行為都與Serial收集器完全一樣。

ParNew / Serial Old收集器運(yùn)行示意圖

是許多運(yùn)行在Server模式下的虛擬機(jī)中的首選的新生代收集器,其中有一個與性能無關(guān)但很重要的原因是,除了Serial收集器外,目前只有它能與CMS收集器配合工作。
ParNew收集器在單CPU環(huán)境中絕對不會有比Serial收集器更好的效果,甚至由于存在線程交互的開銷,該收集器在通過超線程技術(shù)實現(xiàn)的兩個CPU的環(huán)境下中都不能百分之百地保證能超越Serial收集器。

注意并行收集器和并發(fā)收集器。

  • 并行(Parallel):指多條垃圾收集線程并行工作,但此時用于線程仍然處于等待狀態(tài)
  • 并發(fā)(Concurrent):指用于線程與垃圾收集線程同時執(zhí)行(但不一定是并行的,可能會交替執(zhí)行),用戶程序繼續(xù)運(yùn)行,而垃圾收集程序運(yùn)行于另一個CPU上。
Parallel Scavenge 收集器

Parallel Scavenge收集器也是一個新生代收集器,也是使用復(fù)制算法的收集器,又是并行的多線程收集器,看上去和ParNew都一樣。
Parallel Scavenge收集器的特點是它的關(guān)注點和其他收集器不同,CMS等收集器的關(guān)注點是盡可能地縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標(biāo)則是達(dá)到一個可控制的吞吐量。
吞吐量 = 運(yùn)行用戶代碼時間 / (運(yùn)行用戶代碼時間 + 垃圾收集時間)。
停頓時間越短就越適合需要與用戶交互的程序,良好的響應(yīng)速度能提升用戶的體驗;而高吞吐量則可以最高效率地利用CPU時間,盡快地完成程序的運(yùn)算任務(wù),主要適合在后臺運(yùn)算而不需要太多交互的任務(wù)
由于與吞吐量關(guān)系密切,Parallel Scavenge收集器也經(jīng)常被稱為“吞吐量優(yōu)先”收集器。

Serial Old 收集器

是Serial收集器的老年代版本,同樣是一個單線程收集器,使用“標(biāo)記-整理”算法。主要意義也是被Client模式下的虛擬機(jī)使用。在Servier模式下可以作為CMS收集器的后備預(yù)案,在并發(fā)收集發(fā)生Concurrent Mode Failure時使用。

Parallel Old 收集器

是Parallel Scavenge收集器的老年代版本,使用多線程和“標(biāo)記-整理”算法。在注重吞吐量及CPU資源敏感的場合,都可以有效考慮Parallel Scavenge加Parallel Old收集器

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標(biāo)的收集器。從名字上就可以看出其是基于“標(biāo)記-清除”算法實現(xiàn)的,運(yùn)作過程相對于其他收集器來說要更復(fù)雜一些,整個過程分為4個步驟:

  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)記階段就是進(jìn)行GC Roots Tracing的過程,而重新標(biāo)記階段則是為了修正并發(fā)標(biāo)記期間,因用戶程序繼續(xù)運(yùn)行而導(dǎo)致標(biāo)記產(chǎn)生變動的那部分對象的標(biāo)記記錄,這個階段的停頓時間一般會比初始標(biāo)記階段稍長一些,但遠(yuǎn)比并發(fā)標(biāo)記的時間短。
CMS收集器運(yùn)行示意圖

CMS最主要有點是:并發(fā)收集、低停頓。但是還存在三個顯著的缺點:

  • CMS收集器對CPU資源非常敏感。
  • CMS收集器無法處理浮動垃圾(Floating Garbage),可能出現(xiàn)“Concurrent Mode Failure”失敗后而導(dǎo)致另一次Full GC的產(chǎn)生。浮動垃圾是由于CMS并發(fā)清理過程用戶線程同時運(yùn)行而出現(xiàn)在標(biāo)記過程之后,CMS無法在本次收集中處理掉的部分。
  • CMS是基于“標(biāo)記-清除”算法,在收集結(jié)束后可能產(chǎn)生大量空間碎片。
G1收集器

G1(Garbage First)收集器
G1收集器是垃圾收集器理論進(jìn)一步發(fā)展的產(chǎn)物,與CMS收集器相比有連個顯著的改進(jìn):一是G1收集器是基于“標(biāo)記-整理”算法實現(xiàn)的收集器,也就是說它不會產(chǎn)生空間碎片。二是她可以非常精確地控制停頓,既能讓使用者明確指定在一個長度為M毫秒的時間片段內(nèi),消耗在垃圾收集上的時間不得超過N毫秒。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容