垃圾回收算法和垃圾收集器

對象是否可被回收

引用計數法

實現原理: 為每個對象配備一個整形的計數器,例如,對象A,只要有任何對象引用了A,則A的引用計數器就加1,當引用失效時,減1;當A的引用計數器為0時,對象A就不可能再被使用,即可以進行回收。

存在問題:

  1. 無法處理循環引用的問題。所以在Java垃圾回收器中,沒有使用這種方法。

循環引用:有對象A和B,A中含有對象B的引用,對象B中含有A的引用;此時,A和B的計數器都不為0,即無法進行回收;但是在整個系統中,沒有其他對象引用了A和B,A和B是應該被回收的對象,但由於垃圾對象之間的互相引用,從而使垃圾回收器無法識別,造成內存洩漏。

  1. 引用計數器要求每次引用產生和消除的時候,都要伴隨一次加減操作,對系統性能有一定影響。

可達性分析法

實現原理: 通過一系列稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜索,搜索走過的路徑稱為引用鏈,當一個對象到 GC Roots 沒有任何引用鏈時(即不可達),則此對象不可用,可以判定為可回收對象。

可以作為 GC Roots 的對象:

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

判斷可觸及性

通過可達性分析法可以判斷哪些對象不可達。一般來說,不可達對象需要被回收。但事實上,該對象有可能在某一條件下“復活自己”,如果這樣,再回收就不合理了。為此需要定義對象的可觸及性狀態,並規定在什麼狀態下,可以安全回收對象。

可觸及性包含三種狀態:

  • 可觸及的:從根節點出發,可以到達這個對象。
  • 可復活的:對象的所有引用都被釋放,但是對象有可能在 finalize() 函數中復活。
  • 不可觸及的:對象的finalize()函數被調用,並且沒有復活,或者 對象的 finalize() 沒有重寫(沒必要執行), 那麼就會進入不可觸及狀態,此時對象不可能被複活,因為 finalize() 只會被調用一次。

對象只有在不可觸及狀態時,才可以被回收。

注意:

  • finalize()函數是一個非常糟糕的模式,不推薦使用它釋放資源。
  • finalize() 有可能發生引用外洩,在無意中復活對象。
  • finalize() 被系統調用,調用時間不明確。釋放資源推薦在 try-catch-finally 中實現。

引用級別

Java 提供四個級別的引用,由強到弱依次是:強引用、軟引用、弱引用、虛引用。除強引用外,其他都可以在 java.lanf.ref 包中找到。

不同引用級別出現的意義: 希望描述這樣一類對象:當內存對象還足夠時,則保留在內存之中;如果內存空間在進行垃圾回收後還是非常緊張,則可以拋棄這些對象。

  • 強引用:類似 Object object = new Object() 這類的引用,只要強引用還存在,垃圾回收器永遠不會回收掉引用的對象。
  • 軟引用:用來描述一些還有用但是非必須的對象。對於軟引用關聯的對象,系統將在發生內存溢出前,將這些對象列進回收範圍進行第二次回收。如果這次回收還沒有足夠的內存,才會拋出內存溢出異常。
  • 弱引用:用來描述非必須對象。它關聯的對象只能拿生存到下一次垃圾收集發生之前,當垃圾回收器工作時,無論內存是否足夠,都會回收被弱引用關聯的對象。
  • 虛引用:一個對象是否有虛引用,完全不會影響其生存週期,也無法通過虛引用取得對象實例。為對象設置虛引用關聯的唯一目的是能在這個對象被回收時得到系統通知。

軟引用,弱引用非常適合保存可有可無的緩存數據,從而加速系統運行。

垃圾回收算法

標記清除算法

顧名思義:此算法分為兩個階段 標記 和 清除。

標記:標記的過程其實就是,遍歷所有的 GC Roots,然後將所有GC Roots可達的對象標記為存活的對象。

清除:清除的過程將遍歷堆中所有的對象,將沒有標記的對象全部清除掉。

不足:

  1. 效率問題:標記和清除過程的效率都不高。
  2. 空間碎片問題:標記清除後會出現大量不連續的內存碎片,空間碎片太多會導致以後程序運行無法分配較大對象時,提前觸發GC。

複製算法

將原有的內存空間分為兩塊,每次只是用其中一塊,在GC時,將正在使用的內存中存活對象複製到未使用的內存塊中,然後,清除正在使用內存的所有對象,交換兩塊內存的角色。

複製算法適合存活對象少、垃圾對象多的情況,所以複製算法很適合新生代(新生代中垃圾對象通常多於存活對象)。

新生代中的 Eden 存活下來的對象,Survivor區不能完全存放,那麼這些對象會通過分配擔保機制進入老年代。

標記整理算法

標記整理算法是一種老年代的回收算法。首先需要從根節點開始,對所有可達對象做一次標記,然後將所有存活對象整理到內存的一端,之後清理邊界外所有的空間。

分代收集算法

將內存區間根據對象的特點分成幾塊,根據每塊內存區間的特點,使用不同的回收算法,以提高垃圾回收的效率。

對於新生代,回收頻率很高,但每次GC耗時很短,而老年代頻率低,但會消耗更長的時間。為了支持高頻率的新生代回收,虛擬機使用一種叫做卡表的數據結構。卡表為一個比特位集合,每一個比特位可以用來表示老年代的某一區域中的所有對象是否持有新生代對象的引用。

這樣在新生代GC時,可以不用耗大量時間掃描所有老年代對象,來確定引用關係,可以先掃描卡表,卡表位為1時,才包含新生代引用,在新生代GC時,只需掃描卡表位為1所在的老年代空間,這樣可大大加快新生代回收速度。

分區算法

分區算法將整個堆空間劃分成連續的小區間。每個小區間都獨立使用,獨立回收。這種做法的好處是可以控制一次回收多少個小區間,從而很好的控制GC產生的停頓時間。

垃圾回收器

Serial收集器

新生代串行回收器:

  1. 它只使用單線程進行垃圾回收
  2. 它是獨佔式的垃圾回收

由於串行收集器只使用單線程回收,所以在垃圾回收時會出現“Stop The World”。這樣會造成很槽糕的用戶體驗,在實時性要求較高的場景不適應。

由於新生代串行收集器使用複製算法,實現相對簡單,邏輯處理特別高效,而且沒有線程切換開銷。在單CPU處理器等硬件平臺不是特別優越的場合,它的性能可以超過並行回收器和併發回收器。

Serial Old 收集器

老年代串行回收器:

  1. 使用的是標記-整理算法
  2. 串行的,它是獨佔式的

可以作為CMS回收器的備用回收器

ParNew收集器

新生代併發回收器

  1. 回收策略,算法,參數和新生代串行回收器一樣
  2. 回收過程進行了多線程化,但是回收過程應用程序依舊會全部暫停

Parallel Scavenge 收集器

同樣也是新生代併發收集器,但是它卻注重系統吞吐量。

Parallel Old 收集器

老年代併發回收器,使用標記-清除算法

  1. 多線程併發收集器,也是一種關注吞吐量的收集器

CMS收集器

老年代併發收集器,主要關注於系統停頓時間。

  1. 初始標記: STW(Stop The World)標記根對象
  2. 併發標記:標記所有對象
  3. 預清理:清理前準備以及控制停頓時間
  4. 重新標記:STW,修正併發標記數據
  5. 併發清理:清理垃圾
  6. 併發重置

初始標記和重新標記是獨佔系統資源的,而預清理、併發標記、併發清除和併發重置是可以和用戶線程一起執行的。

G1收集器


分享到:


相關文章: