每日一問:講講 Java 虛擬機器的垃圾回收

nanchen2251發表於2019-06-13

昨天我們用比較精簡的文字講了 Java 虛擬機器結構,沒看過的可以直接從這裡檢視: 每日一問:你瞭解 Java 虛擬機器結構麼?

今天我們必須來看看 Java 虛擬機器的垃圾回收演算法是怎樣的。不過在開始之前,我們一定得確定哪些是活著的物件,又有哪些是可以進行回收的。

判斷物件是否存活方式

引用計數演算法

對應判斷一個物件是否可以回收,我想引用計數一定是最容易被想到的演算法了吧。給每個物件加一個引用計數器,每當有一個地方引用它時,計數器就加 1,引用失效後減 1,當物件的計數器為 0,則說明這個物件可以被回收了。這個演算法非常簡單,但存在一個非常大的弊端:一旦兩個物件相互引用,這個演算法就沒轍了。

根搜尋演算法

Java 就是採用的根搜尋演算法進行判斷物件是否存活。這個演算法的思路是:通過一系列名為 "GC Roots" 的物件作為起始點,從這些結點開始向下搜尋,當一個物件到 "GC Roots" 沒有任何引用鏈相連的話,則證明這個物件是可以被回收的。在 Java 中,可以作為 "GC Roots" 的物件包括:

  • 虛擬機器棧中引用的物件;
  • 方法區中的類靜態屬性和常量引用的物件;
  • 本地方法棧中 JNI 引用的物件;

四種引用

在 JDK 1.2 之後,引用被分為了強引用、軟引用、弱引用和虛引用四種,這四種引用強度依次逐漸減弱。

強引用

強引用在 Android 程式碼中普遍存在,只要強引用還在,垃圾回收器就不會回收掉被引用的物件,這就是為什麼我們用內部類持有 Activity 例項會造成記憶體洩漏的根本原因。

軟引用

軟引用用來描述一些還有用,但非必需的物件,用 SoftReference 實現,**被軟引用關聯的物件,在系統將要發生 OOM 之前,會把這些物件列進回收範圍之中並進行第二次回收。**軟引用在 Android 中主要是用於做快取,比如軟引用快取網路請求的圖片。

弱引用

弱引用也是用來描述非必需物件的,但它的強度比軟引用更弱,用 WeakReference 實現。**被弱引用管理的物件只能生存到下一次垃圾收集發生之前。**弱引用在 Android 中主要用於處理記憶體洩漏。

虛引用

虛引用其實沒啥好說的,一個物件是否有虛引用的存在,完全不會對生存時間構成影響,也無法通過虛引用來取得一個物件例項。就目前為止,我還沒有在 Android 開發中使用過它。

都有些什麼垃圾回收演算法

學習 Java 虛擬機器的垃圾回收演算法之前,我們必須來看看我們常見的幾種垃圾回收演算法的思想,並把它們的優劣進行一定的對比,這樣一定才能讓你理解更加深刻。

標記 - 清除演算法

標記 - 清除演算法應該是最簡單基礎的收集演算法了,只需要標記需要回收的物件,標記完成後統一回收即可。但其有兩個非常明顯的弊端。

  • 標記清除效率都不高;
  • 標記清除後會產生大量不連續的記憶體碎片,導致程式以後需要較大物件時無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。
複製收集演算法

複製演算法主要是將可用記憶體劃分為大小相等的兩塊,每次只使用其中的一塊,當這一塊記憶體用完,就將存活著的物件複製到另一塊記憶體上去,然後把已使用過的記憶體空間一次性清理掉。複製回收演算法能有效地避免記憶體碎片,但是演算法需要把記憶體一分為二,導致記憶體使用率大大降低。

標記 - 整理演算法

複製收集演算法在物件存活率較高時就需要進行較多的複製操作,效率非很低。 效率會很低。標記-整理演算法就解決了這樣的問題,同樣採用的是根搜尋演算法進行存活物件標記,但後續是將所有存活的物件都移動到記憶體的一端,然後清理掉端外界的物件。

分代收集演算法

當前包括 Java 虛擬機器在內的商業虛擬機器都採用的是分代收集演算法。這種演算法其實就是根據物件的存活週期不同將記憶體劃分為幾塊。一般把 Java 堆分為新生代和老年代,然後根據各個年代的特點採用最適合的收集演算法。

Java 虛擬機器的垃圾回收策略

前面說了 Java 虛擬機器採用的是分代回收演算法,該演算法會根據各個年代的特點採用最適合的收集演算法,我們就必須瞭解 Java 堆分的各個年代區域的特點。

JVM 中共分為三個代:新生代、老年代和持久代。其中持久代主要存放的是 Java 類的類資訊,與垃圾收集要收集的 Java 物件關係不大。

  • 新生代:所有新生成的物件首先都是放在新生代的,新生代採用複製回收演算法。新生代的目標就是儘可能快速地收集掉那些生命週期短的物件。新生代按照 8:1 的比例分為一個 Eden 區和兩個 Survivor 區。大部分物件在 Eden 區生成,當 Eden 區滿時,還存活的物件將被複制到其中的一個 Survivor 區,當這個 Survivor 區滿時,此區的存活物件將被複制到另外一個 Survivor 區,當另一個 Survivor 區也滿了的時候,從第一個 Survivor 區複製過來的並且此時還存活的物件,將被複制到了「年老區 」。需要注意,Survivor 的兩個區是對稱的,沒有任何的先後關係,所以同一個區中可能同時存在 Eden 複製過來的物件,和從前一個 Survivor 區複製過來的物件,而複製到年老區的只有從第一個 Survivor 區過來的物件,而且,Survivor 區總有一個是空的。
  • 老年代:在新生代中經歷了 N 次垃圾回收後仍然存活的物件,就會被放到老年代中,老年代採用標記整理回收演算法。因此,可以認為老年代中存放的都是一些生命週期較長的物件。
  • 持久代:用於存放靜態檔案,如 final 常量、static 常量、常量池等。持久代對垃圾回收沒有顯著影響,但有些應用可能動態生成或者呼叫一些 class。在這種時候需要設定一個比較大的持久代空間來存放這些執行過程中新增的類。

談談 Java 垃圾回收的觸發條件

Java 垃圾回收包含兩種型別:Scavenge GC 和 Full GC。

  • Scavenge GC:一般情況下,當新物件生成,並且在 Eden 申請空間失敗的時候,就會觸發 Scavenge GC,對 Eden 區進行 GC,清除非存活的物件,並且把尚且存活的物件移動到 Survivor 區,然後整理 Survivor 的兩個區。這種方式的 GC 是對新生代的 Eden 區進行,不會影響到老年代。因為大部分物件都是從 Eden 區開始的,同時 Eden 區不會分配的很大,所以 Eden 區的 GC 會頻繁進行。
  • Full GC:Full GC 將會對整個堆進行整理,包括新生代、老年代和持久代。Full GC 因為需要對整個堆進行回收,所以比 Scavenge GC 要慢,因此應該儘量減少 Full GC 的次數。在對 JVM 調優的過程中,很大一部分工作就是對 Full GC 的調節,有如下原因可能導致 Full GC:
    1. 老年代被寫滿;
    2. 持久代被寫滿;
    3. System.gc() 被顯式呼叫;

好了,這一篇文字比起前面的文字稍微多了一些,主要是知識關聯性稍微大了一些,又不適合分開講解,所以就只能這樣了。

相關文章