1. 程式人生 > 程式設計 >Java GC與四種引用

Java GC與四種引用

常見的垃圾收集演演算法

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

GC

Serial GC

最古老的垃圾收集器,“Serial”體現在其收集工作是單執行緒的,並且在進行垃圾收集過程中,會進入臭名昭著的“Stop-The-World”狀態(即在收集垃圾的時候會停止整個程式的執行)。當然,其單執行緒設計也意味著精簡的GC實現,無需維護複雜的資料結構,初始化也簡單,所以一直是Client模式下JVM的預設選項。 從年代的角度,通常將其老年代實現單獨稱作Serial Old,它採用了標記-整理(Mark-Compact)演演算法,區別於新生代的複製演演算法。 Serial GC的對應JVM引數是:-XX:+UseSerialGC

ParNew GC

新生代GC實現,它實際是Serial GC的多執行緒版本,最常見的應用場景是配合老年代的CMS GC工作,下面是對應引數-XX:+UseConcMarkSweepGC -XX:+UseParNewGC

CMS(Concurrent Mark Sweep) GC

基於標記-清除(Mark-Sweep)演演算法,設計目標是儘量減少停頓時間,這一點對於Web等反應時間敏感的應用非常重要,一直到今天,仍然有很多系統使用CMS GC。但是,CMS採用的標記-清除演演算法,存在著記憶體碎片化問題,所以難以避免在長時間執行等情況下發生full GC,導致惡劣的停頓。另外,既然強調了併發(Concurrent),CMS會佔用更多CPU資源,並和使用者執行緒爭搶。

  • 標記清除演演算法流程:
    1. 初始標記(CMS-initial-mark) :標記 Roots 能直接引用到的物件
    2. 併發標記(CMS-concurrent-mark):進行 GC Root Tracing
    3. 重新標記(CMS-remark) :修正併發標記期間由於使用者程式執行而導致的變動
    4. 併發清除(CMS-concurrent-sweep):進行清除工作

Parrallel GC

在早期JDK 8等版本中,它是server模式JVM的預設GC選擇,也被稱作是吞吐量優先的GC。它的演演算法和Serial GC比較相似,儘管實現要複雜的多,其特點是新生代和老年代GC都是並行進行的,在常見的伺服器環境中更加高效。 開啟選項是:-XX:+UseParallelGC

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

G1 GC

這是一種兼顧吞吐量和停頓時間的GC實現,是Oracle JDK 9以後的預設GC選項。G1可以直觀的設定停頓時間的目標,相比於CMS GC,G1未必能做到CMS在最好情況下的延時停頓,但是最差情況要好很多。

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

G1能去建立可預測的停頓時間模型是因為它可以有計劃地避免在整個Java堆中進行全區域的垃圾收集。 G1跟蹤各個 Region 裡面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region)。 這種使用Region劃分記憶體空間以及有優先順序的區域回收方式,保證了 G1 收集器在有限的時間內可以獲取儘可能高的收集效率。

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

最後做一個簡要整理

  1. Serial收集器:序列執行;作用於新生代;複製演演算法;響應速度優先;適用於單CPU環境下的client模式。
  2. ParNew收集器:並行執行;作用於新生代;複製演演算法;響應速度優先;多CPU環境Server模式下與CMS配合使用。
  3. Parallel Scavenge收集器:並行執行;作用於新生代;複製演演算法;吞吐量優先;適用於後臺運算而不需要太多互動的場景。
  4. Serial Old收集器:序列執行;作用於老年代;標記-整理演演算法;響應速度優先;單CPU環境下的Client模式。
  5. Parallel Old收集器:並行執行;作用於老年代;標記-整理演演算法;吞吐量優先;適用於後臺運算而不需要太多互動的場景。
  6. CMS收集器:併發執行;作用於老年代;標記-清除演演算法;響應速度優先;適用於網際網路或B/S業務。
  7. G1收集器:併發執行;可作用於新生代或老年代;標記-整理演演算法+複製演演算法;響應速度優先;面向服務端應用。

引用

前面講到了垃圾收集過程中需要GC去找到Roots,然後順藤摸瓜找到與Root有各自關聯的物件,然後篩選回收垃圾,那麼GC是如何找到這些還能存活下來的物件的呢?

首先在java中,可作為GC Roots的物件有:

  1. 虛擬機器器棧(棧幀中的本地變量表)中引用的物件;
  2. 方法區中的類靜態屬性引用的物件;
  3. 方法區中常量引用的物件;
  4. 本地方法棧中JNI(即一般說的Native方法)中引用的物件

不同的引用型別,主要體現的是物件不同的可達性狀態和對垃圾收集的影響。

  1. 所謂強引用,就是我們最常見的普通物件引用,只要還有強引用指向一個物件,就能表明物件還“活著”,垃圾收集器不會碰這種物件。對於一個普通的物件,如果沒有其他的引用關係,只要超過了引用的作用域或者顯式地將相應(強)引用賦值為null,就是可以被垃圾收集的了,當然具體回收時機還是要看垃圾收集策略。
  2. 軟引用,是一種相對強引用弱化一些的引用,可以讓物件豁免一些垃圾收集,只有當JVM認為記憶體不足時,才會去試圖回收軟引用指向的物件。JVM會確保在丟擲OutOfMemoryError之前,清理軟引用指向的物件。軟引用通常用來實現記憶體敏感的快取,如果還有空閒記憶體,就可以暫時保留快取,當記憶體不足時清理掉,這樣就保證了使用快取的同時,不會耗盡記憶體。
  3. 弱引用並不能使物件豁免垃圾收集,僅僅是提供一種訪問在弱引用狀態下物件的途徑。這就可以用來構建一種沒有特定約束的關係,比如,維護一種非強制性的對映關係,如果試圖獲取時物件還在,就使用它,否則重現例項化。它同樣是很多快取實現的選擇。
  4. 對於幻象引用,有時候也翻譯成虛引用,你不能通過它訪問物件。幻象引用僅僅是提供了一種確保物件被finalize以後,做某些事情的機制,比如,通常用來做所謂的Post-Mortem清理機制,我在專欄上一講中介紹的Java平臺自身Cleaner機制等,也有人利用幻象引用監控物件的建立和銷燬。

可達狀態 -- Reachable

  1. 強可達,就是當一個物件可以有一個或多個執行緒可以不通過各種引用訪問到的情況。比如,我們新建立一個物件,那麼建立它的執行緒對它就是強可達。
  2. 軟可達,就是當我們只能通過軟引用才能訪問到物件的狀態。
  3. 弱可達,類似前面提到的,就是無法通過強引用或者軟引用訪問,只能通過弱引用訪問時的狀態。這是十分臨近finalize狀態的時機,當弱引用被清除的時候,就符合finalize的條件了。
  4. 幻象可達,上面流程圖已經很直觀了,就是沒有強、軟、弱引用關聯,並且finalize過了,只有幻象引用指向這個物件的時候。
  5. 還有一個最後的狀態,就是不可達,意味著物件可以被清除了。

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

  1. 除了幻象引用(因為get永遠返回null),如果物件還沒有被銷燬,都可以通過get方法獲取原有物件。這意味著,利用軟引用弱引用,我們可以將訪問到的物件,重新指向強引用,也就是人為的改變了物件的可達性狀態!
  2. 所以,對於軟引用弱引用之類,垃圾收集器可能會存在二次確認的問題,以保證處於弱引用狀態的物件,沒有改變為強引用
  3. 如果我們錯誤的保持了強引用(比如,賦值給了static變數),那麼物件可能就沒有機會變回類似弱引用的可達性狀態了,就會產生記憶體洩漏。所以,檢查弱引用指向物件是否被垃圾收集,也是診斷是否有特定記憶體洩漏的一個思路,如果我們的框架使用到弱引用又懷疑有記憶體洩漏,就可以從這個角度檢查。
參考:Java核心技術36問