老生常談Java虛擬機垃圾回收機制(必看篇)

2、垃圾收集

垃圾收集主要是針對堆和方法區進行。算法

程序計數器、虛擬機棧和本地方法棧這三個區域屬於線程私有的,只存在於線程的生命週期內,線程結束以後也會消失,所以不須要對這三個區域進行垃圾回收。多線程

 

判斷一個對象是否可被回收併發

1. 引用計數算法

給對象添加一個引用計數器,當對象增長一個引用時計數器加 1,引用失效時計數器減 1。引用計數爲 0 的對象可被回收。框架

兩個對象出現循環引用的狀況下,此時引用計數器永遠不爲 0,致使沒法對它們進行回收。函數

正由於循環引用的存在,所以 Java 虛擬機不使用引用計數算法。性能

public class ReferenceCountingGC {

    public Object instance = null;

    public static void main(String[] args) {
        ReferenceCountingGC objectA = new ReferenceCountingGC();
        ReferenceCountingGC objectB = new ReferenceCountingGC();
        objectA.instance = objectB;
        objectB.instance = objectA;
    }
}

2. 可達性分析算法

經過 GC Roots 做爲起始點進行搜索,可以到達到的對象都是存活的,不可達的對象可被回收。測試

Java 虛擬機使用該算法來判斷對象是否可被回收,在 Java 中 GC Roots 通常包含如下內容:spa

  • 虛擬機棧中局部變量表中引用的對象
  • 本地方法棧中 JNI 中引用的對象
  • 方法區中類靜態屬性引用的對象
  • 方法區中的常量引用的對象

 

3. 方法區的回收

由於方法區主要存放永久代對象,而永久代對象的回收率比新生代低不少,所以在方法區上進行回收性價比不高。線程

主要是對常量池的回收對類的卸載翻譯

在大量使用反射、動態代理、CGLib 等 ByteCode 框架、 動態生成 JSP 以及 OSGi 這類頻繁自定義 ClassLoader 的場景都須要虛擬機具有類卸載功能,以保證不會出現內存溢出。

類的卸載條件不少,須要知足如下三個條件,而且知足了也不必定會被卸載:

  • 該類全部的實例都已經被回收,也就是堆中不存在該類的任何實例
  • 加載該類的 ClassLoader 已經被回收
  • 該類對應的 Class 對象沒有在任何地方被引用,也就沒法在任何地方經過反射訪問該類方法。

能夠經過 -Xnoclassgc 參數來控制是否對類進行卸載。

4. finalize()

finalize() 相似 C++ 的析構函數,用來作關閉外部資源等工做。可是 try-finally 等方式能夠作的更好, 而且該方法運行代價高昂,不肯定性大,沒法保證各個對象的調用順序,所以最好不要使用。

當一個對象可被回收時,若是須要執行該對象的 finalize() 方法, 那麼就有可能在該方法中讓對象從新被引用,從而實現自救。 自救只能進行一次,若是回收的對象以前調用了 finalize() 方法自救,後面回收時不會調用 finalize() 方法。

引用類型

不管是經過引用計算算法判斷對象的引用數量,仍是經過可達性分析算法判斷對象是否可達, 斷定對象是否可被回收都與引用有關。

Java 提供了四種強度不一樣的引用類型。

1. 強引用

被強引用關聯的對象不會被回收

使用 new 一個新對象的方式來建立強引用。

Object obj = new Object();

2. 軟引用

被軟引用關聯的對象只有在內存不夠的狀況下才會被回收

使用 SoftReference 類來建立軟引用。

Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;  // 使對象只被軟引用關聯

3. 弱引用

被弱引用關聯的對象必定會被回收,也就是說它只能存活到下一次垃圾回收發生以前。

使用 WeakReference 類來實現弱引用。

Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;

4. 虛引用

又稱爲幽靈引用或者幻影引用。一個對象是否有虛引用的存在, 徹底不會對其生存時間構成影響,也沒法經過虛引用取得一個對象。

爲一個對象設置虛引用關聯的惟一目的就是能在這個對象被回收時收到一個系統通知

使用 PhantomReference 來實現虛引用。

Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj = null;

垃圾收集算法

1. 標記 - 清除

 

將存活的對象進行標記,而後清理掉未被標記的對象。

不足:

  • 標記和清除過程效率都不高;
  • 會產生大量不連續的內存碎片,致使沒法給大對象分配內存。

2. 標記 - 整理

 

讓全部存活的對象都向一端移動,而後直接清理掉端邊界之外的內存。

3. 複製

 

將內存劃分爲大小相等的兩塊,每次只使用其中一塊,當這一塊內存用完了就將還存活的對象複製到另外一塊上面,而後再把使用過的內存空間進行一次清理。

主要不足是隻使用了內存的一半。

如今的商業虛擬機都採用這種收集算法來回收新生代,可是並非將新生代劃分爲大小相等的兩塊,而是分爲一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 空間和其中一塊 Survivor。在回收時,將 Eden 和 Survivor 中還存活着的對象一次性複製到另外一塊 Survivor 空間上,最後清理 Eden 和使用過的那一塊 Survivor。

HotSpot 虛擬機的 Eden 和 Survivor 的大小比例默認爲 8:1,保證了內存的利用率達到 90%。若是每次回收有多於 10% 的對象存活,那麼一塊 Survivor 空間就不夠用了,此時須要依賴於老年代進行分配擔保,也就是借用老年代的空間存儲放不下的對象。

4. 分代收集

如今的商業虛擬機採用分代收集算法,它根據對象存活週期將內存劃分爲幾塊,不一樣塊採用適當的收集算法。

通常將堆分爲新生代和老年代。

  • 新生代使用:複製算法
  • 老年代使用:標記 - 清除 或者 標記 - 整理 算法

垃圾收集器

 

以上是 HotSpot 虛擬機中的 7 個垃圾收集器,連線表示垃圾收集器能夠配合使用。

  • 單線程與多線程:單線程指的是垃圾收集器只使用一個線程進行收集,而多線程使用多個線程;
  • 串行與並行:串行指的是垃圾收集器與用戶程序交替執行,這意味着在執行垃圾收集的時候須要停頓用戶程序;並行指的是垃圾收集器和用戶程序同時執行。除了CMS 和 G1以外,其它垃圾收集器都是以串行的方式執行。

1. Serial 收集器

 

Serial 翻譯爲串行,也就是說它以串行的方式執行。

它是單線程的收集器,只會使用一個線程進行垃圾收集工做。

它的優勢是簡單高效,對於單個 CPU 環境來講,因爲沒有線程交互的開銷,所以擁有最高的單線程收集效率。

它是 Client 模式下的默認新生代收集器,由於在該應用場景下,分配給虛擬機管理的內存通常來講不會很大。Serial 收集器收集幾十兆甚至一兩百兆的新生代停頓時間能夠控制在一百多毫秒之內,只要不是太頻繁,這點停頓是能夠接受的。

2. ParNew 收集器

 

它是 Serial 收集器的多線程版本。

是 Server 模式下的虛擬機首選新生代收集器,除了性能緣由外,主要是由於除了 Serial 收集器,只有它能與 CMS 收集器配合工做。

默認開啓的線程數量與 CPU 數量相同,可使用 -XX:ParallelGCThreads 參數來設置線程數。

3. Parallel Scavenge 收集器

與 ParNew 同樣是多線程收集器。

其它收集器關注點是儘量縮短垃圾收集時用戶線程的停頓時間,而它的目標是達到一個可控制的吞吐量,它被稱爲「吞吐量優先」收集器。這裏的吞吐量指 CPU 用於運行用戶代碼的時間佔總時間的比值。

停頓時間越短就越適合須要與用戶交互的程序,良好的響應速度能提高用戶體驗。而高吞吐量則能夠高效率地利用 CPU 時間,儘快完成程序的運算任務,適合在後臺運算而不須要太多交互的任務。

縮短停頓時間是以犧牲吞吐量和新生代空間來換取的:新生代空間變小,垃圾回收變得頻繁,致使吞吐量降低。

能夠經過一個開關參數打開 GC 自適應的調節策略(GC Ergonomics), 就不須要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 區的比例、晉升老年代對象年齡等細節參數了。 虛擬機會根據當前系統的運行狀況收集性能監控信息, 動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量。 自適應調節策略是 Parallel Scavenge 收集器和 ParNew 收集器的一個重要區別。

4. Serial Old 收集器

 

是 Serial 收集器的老年代版本,也是給 Client 模式下的虛擬機使用,採用標記-整理算法。若是用在 Server 模式下,它有兩大用途:

  • 在 JDK 1.5 以及以前版本(Parallel Old 誕生之前)中與 Parallel Scavenge 收集器搭配使用。
  • 做爲 CMS 收集器的後備預案,在併發收集發生 Concurrent Mode Failure 時使用。

5. Parallel Old 收集器

 

是 Parallel Scavenge 收集器的老年代版本,採用標記-整理算法。

在注重吞吐量以及 CPU 資源敏感的場合,均可以優先考慮 Parallel Scavenge 加 Parallel Old 收集器。

6. CMS 收集器

 

CMS(Concurrent Mark Sweep),Mark Sweep 指的是標記 - 清除算法

分爲如下四個流程:

  • 初始標記:僅僅只是標記一下 GC Roots 能直接關聯到的對象,速度很快,須要停頓。
  • 併發標記:進行 GC Roots Tracing 的過程,它在整個回收過程當中耗時最長,不須要停頓。
  • 從新標記:爲了修正併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分對象的標記記錄,須要停頓。
  • 併發清除:不須要停頓。

在整個過程當中耗時最長的併發標記和併發清除過程當中,收集器線程均可以與用戶線程一塊兒工做,不須要進行停頓,具備併發收集、低停頓的優勢。

具備如下缺點:

  • 吞吐量低:低停頓時間是以犧牲吞吐量爲代價的,致使 CPU 利用率不夠高
  • 沒法處理浮動垃圾,可能出現 Concurrent Mode Failure。浮動垃圾是指併發清除階段因爲用戶線程繼續運行而產生的垃圾,這部分垃圾只能到下一次 GC 時才能進行回收。 因爲浮動垃圾的存在,所以須要預留出一部份內存,意味着 CMS 收集不能像其它收集器那樣等待老年代快滿的時候再回收。若是預留的內存不夠存放浮動垃圾,就會出現 Concurrent Mode Failure,這時虛擬機將臨時啓用 Serial Old 來替代 CMS。
  • 標記 - 清除算法致使的空間碎片,每每出現老年代空間剩餘,但沒法找到足夠大連續空間來分配當前對象,不得不提早觸發一次 Full GC。

CMS 已經在 JDK 9 中被標記爲廢棄( deprecated )。

7. G1 收集器

G1(Garbage-First),它是一款面向服務端應用的垃圾收集器,在多 CPU 和大內存的場景下有很好的性能。HotSpot 開發團隊賦予它的使命是將來能夠替換掉 CMS 收集器。

堆被分爲新生代和老年代,其它收集器進行收集的範圍都是整個新生代或者老年代,而 G1 能夠直接對新生代和老年代一塊兒回收

 

G1 把堆劃分紅多個大小相等的獨立區域(Region),Region的大小是一致的,數值是在1M到32M字節之間的一個2的冪值數,JVM會盡可能劃分2048個左右、同等大小的Region,新生代和老年代再也不物理隔離

 

經過引入 Region 的概念,從而將原來的一整塊內存空間劃分紅多個的小空間,使得每一個小空間能夠單獨進行垃圾回收。這種劃分方法帶來了很大的靈活性,使得可預測的停頓時間模型成爲可能。經過記錄每一個 Region 垃圾回收時間以及回收所得到的空間(這兩個值是經過過去回收的經驗得到),並維護一個優先列表,每次根據容許的收集時間,優先回收價值最大的 Region。

每一個 Region 都有一個 Remembered Set,用來記錄該 Region 對象的引用對象所在的 Region。經過使用 Remembered Set,在作可達性分析的時候就能夠避免全堆掃描。

 

若是不計算維護 Remembered Set 的操做,G1 收集器的運做大體可劃分爲如下幾個步驟:

  • 初始標記
  • 併發標記
  • 最終標記:爲了修正在併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線程的 Remembered Set Logs 裏面,最終標記階段須要把 Remembered Set Logs 的數據合併到 Remembered Set 中。這階段須要停頓線程,可是可並行執行。
  • 篩選回收:首先對各個 Region 中的回收價值和成本進行排序,根據用戶所指望的 GC 停頓時間來制定回收計劃。此階段其實也能夠作到與用戶程序一塊兒併發執行,可是由於只回收一部分 Region,時間是用戶可控制的,並且停頓用戶線程將大幅度提升收集效率。

從 GC 算法的角度, G1 選擇的是複合算法,能夠簡化理解爲:

  • 在新生代,G1採用的仍然是並行的複製算法,因此一樣會發生Stop-The-World的暫停。
  • 在老年代,大部分狀況下都是併發標記,而整理(Compact)則是和新生代GC時捎帶進行,而且不是總體性的整理,而是增量進行的。

具有以下特色:

  • 空間整合:總體來看是基於「標記 - 整理」算法實現的收集器,從局部(兩個 Region 之間)上來看是基於「複製」算法實現的,這意味着運行期間不會產生內存空間碎片。
  • 可預測的停頓:能讓使用者明確指定在一個長度爲 M 毫秒的時間片斷內,消耗在 GC 上的時間不得超過 N 毫秒。

目前尚處於開發中的 JDK 11, JDK 又增長了兩種全新的 GC 方式,分別是:

  • Epsilon GC,簡單說就是個不作垃圾收集的GC,彷佛有點奇怪,有的狀況下,例如在進行性能測試的時候,可能須要明確判斷GC自己產生了多大的開銷,這就是其典型應用場景。
  • ZGC,這是Oracle開源出來的一個超級GC實現,具有使人驚訝的擴展能力,好比支持T bytes級別的堆大小,而且保證絕大部分狀況下,延遲都不會超過10 ms。雖然目前還處於實驗階段,僅支持 Linux 64 位的平臺,但其已經表現出的能力和潛力都很是使人期待。

相關文章
相關標籤/搜索