Java GC與四種引用

常見的垃圾收集算法

  1. 復制(Copying)算法,我前面講到的新生代GC,基本都是基于復制算法,將活著的對象復制到to區(qū)域,拷貝過程中將對象順序放置,就可以避免內(nèi)存碎片化。這么做的代價是,既然要進行復制,既要提前預留內(nèi)存空間,有一定的浪費;另外,對于G1這種分拆成為大量region的GC,復制而不是移動,意味著GC需要維護region之間對象引用關(guān)系,這個開銷也不小,不管是內(nèi)存占用或者時間開銷。
  2. 標記-清除(Mark-Sweep)算法,首先進行標記工作,標識出所有要回收的對象,然后進行清除。這么做除了標記、清除過程效率有限,另外就是不可避免的出現(xiàn)碎片化問題,這就導致其不適合特別大的堆;否則,一旦出現(xiàn)Full GC,暫停時間可能根本無法接受。
  3. 標記-整理(Mark-Compact),類似于標記-清除,但為避免內(nèi)存碎片化,它會在清理過程中將對象移動,以確保移動后的對象占用連續(xù)的內(nèi)存空間。

GC

Serial GC

最古老的垃圾收集器,“Serial”體現(xiàn)在其收集工作是單線程的,并且在進行垃圾收集過程中,會進入臭名昭著的“Stop-The-World”狀態(tài)(即在收集垃圾的時候會停止整個程序的運行)。當然,其單線程設(shè)計也意味著精簡的GC實現(xiàn),無需維護復雜的數(shù)據(jù)結(jié)構(gòu),初始化也簡單,所以一直是Client模式下JVM的默認選項。
從年代的角度,通常將其老年代實現(xiàn)單獨稱作Serial Old,它采用了標記-整理(Mark-Compact)算法,區(qū)別于新生代的復制算法。
Serial GC的對應(yīng)JVM參數(shù)是:-XX:+UseSerialGC

ParNew GC

新生代GC實現(xiàn),它實際是Serial GC的多線程版本,最常見的應(yīng)用場景是配合老年代的CMS GC工作,下面是對應(yīng)參數(shù)-XX:+UseConcMarkSweepGC -XX:+UseParNewGC

CMS(Concurrent Mark Sweep) GC

基于標記-清除(Mark-Sweep)算法,設(shè)計目標是盡量減少停頓時間,這一點對于Web等反應(yīng)時間敏感的應(yīng)用非常重要,一直到今天,仍然有很多系統(tǒng)使用CMS GC。但是,CMS采用的標記-清除算法,存在著內(nèi)存碎片化問題,所以難以避免在長時間運行等情況下發(fā)生full GC,導致惡劣的停頓。另外,既然強調(diào)了并發(fā)(Concurrent),CMS會占用更多CPU資源,并和用戶線程爭搶。

  • 標記清除算法流程:
    1. 初始標記(CMS-initial-mark) :標記 Roots 能直接引用到的對象
    2. 并發(fā)標記(CMS-concurrent-mark):進行 GC Root Tracing
    3. 重新標記(CMS-remark) :修正并發(fā)標記期間由于用戶程序運行而導致的變動
    4. 并發(fā)清除(CMS-concurrent-sweep):進行清除工作

Parrallel GC

在早期JDK 8等版本中,它是server模式JVM的默認GC選擇,也被稱作是吞吐量優(yōu)先的GC。它的算法和Serial GC比較相似,盡管實現(xiàn)要復雜的多,其特點是新生代和老年代GC都是并行進行的,在常見的服務(wù)器環(huán)境中更加高效。
開啟選項是:-XX:+UseParallelGC

  • 另外,Parallel GC引入了開發(fā)者友好的配置項,我們可以直接設(shè)置暫停時間或吞吐量等目標,JVM會自動進行適應(yīng)性調(diào)整,例如下面參數(shù):
    -XX:MaxGCPauseMillis=value
    這里GC時間和用戶時間比例 = 1 / (N+1)
    -XX:GCTimeRatio=N

G1 GC

這是一種兼顧吞吐量和停頓時間的GC實現(xiàn),是Oracle JDK 9以后的默認GC選項。G1可以直觀的設(shè)定停頓時間的目標,相比于CMS GC,G1未必能做到CMS在最好情況下的延時停頓,但是最差情況要好很多。

  • G1 GC仍然存在著年代的概念,但是其內(nèi)存結(jié)構(gòu)并不是簡單的條帶式劃分,而是類似棋盤的一個個region。Region之間是復制算法,但整體上實際可看作是標記-整理(Mark-Compact)算法,可以有效地避免內(nèi)存碎片,尤其是當Java堆非常大的時候,G1的優(yōu)勢更加明顯。
可預測的停頓時間模型

G1能去建立可預測的停頓時間模型是因為它可以有計劃地避免在整個Java堆中進行全區(qū)域的垃圾收集。 G1跟蹤各個 Region 里面的垃圾堆積的價值大?。ɑ厥账@得的空間大小以及回收所需時間的經(jīng)驗值),在后臺維護一個優(yōu)先列表,每次根據(jù)允許的收集時間,優(yōu)先回收價值最大的Region)。 這種使用Region劃分內(nèi)存空間以及有優(yōu)先級的區(qū)域回收方式,保證了 G1 收集器在有限的時間內(nèi)可以獲取盡可能高的收集效率。

收集流程
  1. 初始標記(Initial Marking),初始標記階段僅僅只是標記一下GC Roots能直接關(guān)聯(lián)到的對象,這階段需要停頓線程,但耗時很短。
  2. 并發(fā)標記(Concurrent Marking) ,并發(fā)標記階段是從GC Root開始對堆中對象進行可達性分析,找出存活的對象,這階段耗時較長,但可與用戶程序并發(fā)執(zhí)行。
  3. 最終標記(Final Marking)最終標記階段則是為了修正在并發(fā)標記期間因用戶程序繼續(xù)運作而導致標記產(chǎn)生變動的那一部分標記記錄,這階段需要停頓線程,但是可并行執(zhí)行。
  4. 篩選回收(Live Data Counting and Evacuation)篩選回收階段首先對各個Region的回收價值和成本進行排序,根據(jù)用戶所期望的GC停頓時間來制定回收計劃,這樣既能保證垃圾回收,又能保證停頓時間,而且也不會降低太多的吞吐量。
  • G1吞吐量和停頓表現(xiàn)都非常不錯,并且仍然在不斷地完善,而且CMS已經(jīng)在JDK 9中被標記為廢棄。

最后做一個簡要整理

  1. Serial收集器:串行運行;作用于新生代;復制算法;響應(yīng)速度優(yōu)先;適用于單CPU環(huán)境下的client模式。
  2. ParNew收集器:并行運行;作用于新生代;復制算法;響應(yīng)速度優(yōu)先;多CPU環(huán)境Server模式下與CMS配合使用。
  3. Parallel Scavenge收集器:并行運行;作用于新生代;復制算法;吞吐量優(yōu)先;適用于后臺運算而不需要太多交互的場景。
  4. Serial Old收集器:串行運行;作用于老年代;標記-整理算法;響應(yīng)速度優(yōu)先;單CPU環(huán)境下的Client模式。
  5. Parallel Old收集器:并行運行;作用于老年代;標記-整理算法;吞吐量優(yōu)先;適用于后臺運算而不需要太多交互的場景。
  6. CMS收集器:并發(fā)運行;作用于老年代;標記-清除算法;響應(yīng)速度優(yōu)先;適用于互聯(lián)網(wǎng)或B/S業(yè)務(wù)。
  7. G1收集器:并發(fā)運行;可作用于新生代或老年代;標記-整理算法+復制算法;響應(yīng)速度優(yōu)先;面向服務(wù)端應(yīng)用。

引用

前面講到了垃圾收集過程中需要GC去找到Roots,然后順藤摸瓜找到與Root有各自關(guān)聯(lián)的對象,然后篩選回收垃圾,那么GC是如何找到這些還能存活下來的對象的呢?

首先在java中,可作為GC Roots的對象有:

  1. 虛擬機棧(棧幀中的本地變量表)中引用的對象;
  2. 方法區(qū)中的類靜態(tài)屬性引用的對象;
  3. 方法區(qū)中常量引用的對象;
  4. 本地方法棧中JNI(即一般說的Native方法)中引用的對象

不同的引用類型,主要體現(xiàn)的是對象不同的可達性狀態(tài)和對垃圾收集的影響。

  1. 所謂強引用,就是我們最常見的普通對象引用,只要還有強引用指向一個對象,就能表明對象還“活著”,垃圾收集器不會碰這種對象。對于一個普通的對象,如果沒有其他的引用關(guān)系,只要超過了引用的作用域或者顯式地將相應(yīng)(強)引用賦值為null,就是可以被垃圾收集的了,當然具體回收時機還是要看垃圾收集策略。
  2. 軟引用,是一種相對強引用弱化一些的引用,可以讓對象豁免一些垃圾收集,只有當JVM認為內(nèi)存不足時,才會去試圖回收軟引用指向的對象。JVM會確保在拋出OutOfMemoryError之前,清理軟引用指向的對象。軟引用通常用來實現(xiàn)內(nèi)存敏感的緩存,如果還有空閑內(nèi)存,就可以暫時保留緩存,當內(nèi)存不足時清理掉,這樣就保證了使用緩存的同時,不會耗盡內(nèi)存。
  3. 弱引用并不能使對象豁免垃圾收集,僅僅是提供一種訪問在弱引用狀態(tài)下對象的途徑。這就可以用來構(gòu)建一種沒有特定約束的關(guān)系,比如,維護一種非強制性的映射關(guān)系,如果試圖獲取時對象還在,就使用它,否則重現(xiàn)實例化。它同樣是很多緩存實現(xiàn)的選擇。
  4. 對于幻象引用,有時候也翻譯成虛引用,你不能通過它訪問對象?;孟笠脙H僅是提供了一種確保對象被finalize以后,做某些事情的機制,比如,通常用來做所謂的Post-Mortem清理機制,我在專欄上一講中介紹的Java平臺自身Cleaner機制等,也有人利用幻象引用監(jiān)控對象的創(chuàng)建和銷毀。

可達狀態(tài) -- Reachable

  1. 強可達,就是當一個對象可以有一個或多個線程可以不通過各種引用訪問到的情況。比如,我們新創(chuàng)建一個對象,那么創(chuàng)建它的線程對它就是強可達。
  2. 軟可達,就是當我們只能通過軟引用才能訪問到對象的狀態(tài)。
  3. 弱可達,類似前面提到的,就是無法通過強引用或者軟引用訪問,只能通過弱引用訪問時的狀態(tài)。這是十分臨近finalize狀態(tài)的時機,當弱引用被清除的時候,就符合finalize的條件了。
  4. 幻象可達,上面流程圖已經(jīng)很直觀了,就是沒有強、軟、弱引用關(guān)聯(lián),并且finalize過了,只有幻象引用指向這個對象的時候。
  5. 還有一個最后的狀態(tài),就是不可達,意味著對象可以被清除了。

垃圾收集機制為什么要在回收垃圾之前再次進行一次 最終標記

  1. 除了幻象引用(因為get永遠返回null),如果對象還沒有被銷毀,都可以通過get方法獲取原有對象。這意味著,利用軟引用弱引用,我們可以將訪問到的對象,重新指向強引用,也就是人為的改變了對象的可達性狀態(tài)!
  2. 所以,對于軟引用、弱引用之類,垃圾收集器可能會存在二次確認的問題,以保證處于弱引用狀態(tài)的對象,沒有改變?yōu)?code>強引用。
  3. 如果我們錯誤的保持了強引用(比如,賦值給了static變量),那么對象可能就沒有機會變回類似弱引用的可達性狀態(tài)了,就會產(chǎn)生內(nèi)存泄漏。所以,檢查弱引用指向?qū)ο笫欠癖焕占彩窃\斷是否有特定內(nèi)存泄漏的一個思路,如果我們的框架使用到弱引用又懷疑有內(nèi)存泄漏,就可以從這個角度檢查。
參考:Java核心技術(shù)36問
?著作權(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)容