Java 垃圾回收(GC) 泛讀

Java 垃圾回收(GC) 泛讀

文章地址: http://www.javashuo.com/article/p-rxhvuizw-cm.htmlhtml

0. 序言

帶着問題去看待 垃圾回收(GC) 會比較好,通常來講主要的疑惑在於這麼幾點:java

  • 爲何須要 GC ?算法

  • 虛擬機(JVM) 與 垃圾回收(GC) 的關係?segmentfault

  • GC 的原理有哪些?jvm

  • 哪些 對象容易被 GC ?性能

  • 等等優化

帶着這些問題往下看:spa

1. 爲何須要 GC ?

GC: 是Garbage Collection 的英文縮略,垃圾收集的意思。線程

爲何須要 GC?
主要是隨着應用程序所應對的業務愈來愈龐大、複雜,用戶愈來愈多,沒有GC就不能保證應用程序正常進行。設計

爲何常常討論 GC,沒有完美的解決方案嗎?
完美的解決方法目前尚未。因爲在 GC 時須要STW(Stop The World),這長不能知足實際的需求,容易形成卡頓、延遲等性能問題,因此纔會不斷地嘗試對GC進行優化。社區的需求是儘可能減小對應用程序的正常執行干擾,這也是業界目標。

2. 虛擬機(JVM) 與 GC 的關係 ?

以 HotSpotJVM 爲例描述下 GC 在 JVM 中的位置:

HotSpot-JVM

因爲 不一樣的 JVM 會有不一樣的 GC 實現,不一樣的 GC 實現使用的算法又不盡相同,這才形成了 GC 的多樣性。
在收購SUN以前,Oracle使用的是JRockit JVM,收購以後使用HotSpot JVM。目前Oracle擁有兩種JVM實現而且一段時間後兩個JVM實現會合二爲一。
HotSpot JVM是目前Oracle SE平臺標準核心組件的一部分。
最新的 GC 方案是 Garbage First(通常簡稱爲 G1)。

3. GC 的種類

1. GC 的發展歷程

  1. 1999年隨JDK1.3.1一塊兒來的是串行方式的Serial GC ,它是第一款 GC 。

  2. 2002年2月26日,J2SE1.4發佈,Parallel GC 和Concurrent Mark Sweep (CMS)GC跟隨JDK1.4.2一塊兒發佈,而且Parallel GC在JDK6以後成爲HotSpot默認GC

2. 不一樣 GC 的區別

HotSpot有這麼多的垃圾回收器,那麼若是有人問,Serial GC、Parallel GC、Concurrent Mark Sweep GC這三個GC有什麼不一樣呢?請記住如下口令:

若是你想要最小化地使用內存和並行開銷,請選Serial GC;
若是你想要最大化應用程序的吞吐量,請選Parallel GC;
若是你想要最小化GC的中斷或停頓時間,請選CMS GC。

固然這不包括新推出的 GC 方案----G1。

3. 關於 Java 1.7 以後的 G1

爲何名字叫作Garbage First(G1)呢?

由於G1是一個並行回收器,它把堆內存分割爲不少不相關的區間(Region),每一個區間能夠屬於老年代或者年輕代,而且每一個年齡代區間能夠是物理上不連續的。

老年代區間這個設計理念自己是爲了服務於並行後臺線程,這些線程的主要工做是尋找未被引用的對象。而這樣就會產生一種現象,即某些區間的垃圾(未被引用對象)多於其餘的區間。

垃圾回收時實則都是須要停下應用程序的,否則就沒有辦法防治應用程序的干擾 ,而後G1 GC能夠集中精力在垃圾最多的區間上,而且只會費一點點時間就能夠清空這些區間裏的垃圾,騰出徹底空閒的區間。

繞來繞去終於明白了,因爲這種方式的側重點在於處理垃圾最多的區間,因此咱們給G1一個名字:垃圾優先(Garbage First)

4. GC 的原理

1. 對象存活判斷

判斷對象是否存活通常有兩種方式:

  • 引用計數:每一個對象有一個引用計數屬性,新增一個引用時計數加1,引用釋放時計數減1,計數爲0時能夠回收。此方法簡單,沒法解決對象相互循環引用的問題。

  • 可達性分析(Reachability Analysis):從GC Roots開始向下搜索,搜索所走過的路徑稱爲引用鏈。當一個對象到GC Roots沒有任何引用鏈相連時,則證實此對象是不可用的。不可達對象。

在Java語言中,GC Roots包括:虛擬機棧中引用的對象、方法區中類靜態屬性實體引用的對象、方法區中常量引用的對象、本地方法棧中JNI引用的對象。

2. GC 經常使用的算法及原理

引用計數法 (Reference Counting)

引用計數器在微軟的 COM 組件技術中、Adobe 的 ActionScript3 種都有使用。
引用計數器的實現很簡單,對於一個對象 A,只要有任何一個對象引用了 A,則 A 的引用計數器就加 1,當引用失效時,引用計數器就減 1。只要對象 A 的引用計數器的值爲 0,則對象 A 就不可能再被使用。
引用計數器的實現也很是簡單,只須要爲每一個對象配置一個整形的計數器便可。可是引用計數器有一個嚴重的問題,即沒法處理循環引用(即兩個對象相互引用)的狀況。所以,在 Java 的垃圾回收器中沒有使用這種算法
一個簡單的循環引用問題描述以下:有對象 A 和對象 B,對象 A 中含有對象 B 的引用,對象 B 中含有對象 A 的引用。此時,對象 A 和對象 B 的引用計數器都不爲 0。可是在系統中卻不存在任何第 3 個對象引用了 A 或 B。也就是說,A 和 B 是應該被回收的垃圾對象,但因爲垃圾對象間相互引用,從而使垃圾回收器沒法識別,引發內存泄漏。

標記-清除算法 (Mark-Sweep)

標記-清除算法將垃圾回收分爲兩個階段:標記階段和清除階段。一種可行的實現是,在標記階段首先經過根節點,標記全部從根節點開始的較大對象。所以,未被標記的對象就是未被引用的垃圾對象。而後,在清除階段,清除全部未被標記的對象。該算法最大的問題是存在大量的空間碎片,由於回收後的空間是不連續的。在對象的堆空間分配過程當中,尤爲是大對象的內存分配,不連續的內存空間的工做效率要低於連續的空間。

複製算法 (Copying)

將現有的內存空間分爲兩快,每次只使用其中一塊,在垃圾回收時將正在使用的內存中的存活對象複製到未被使用的內存塊中,以後,清除正在使用的內存塊中的全部對象,交換兩個內存的角色,完成垃圾回收。
若是系統中的垃圾對象不少,複製算法須要複製的存活對象數量並不會太大。所以在真正須要垃圾回收的時刻,複製算法的效率是很高的。又因爲對象在垃圾回收過程當中統一被複制到新的內存空間中,所以,可確保回收後的內存空間是沒有碎片的。該算法的缺點是將系統內存摺半。
Java 的新生代串行垃圾回收器中使用了複製算法的思想。新生代分爲 eden 空間、from 空間、to 空間 3 個部分。其中 from 空間和 to 空間能夠視爲用於複製的兩塊大小相同、地位相等,且可進行角色互換的空間塊。from 和 to 空間也稱爲 survivor 空間,即倖存者空間,用於存放未被回收的對象。
在垃圾回收時,eden 空間中的存活對象會被複制到未使用的 survivor 空間中 (假設是 to),正在使用的 survivor 空間 (假設是 from) 中的年輕對象也會被複制到 to 空間中 (大對象,或者老年對象會直接進入老年帶,若是 to 空間已滿,則對象也會直接進入老年代)。此時,eden 空間和 from 空間中的剩餘對象就是垃圾對象,能夠直接清空,to 空間則存放這次回收後的存活對象。這種改進的複製算法既保證了空間的連續性,又避免了大量的內存空間浪費。

標記-壓縮算法 (Mark-Compact)

複製算法的高效性是創建在存活對象少、垃圾對象多的前提下的。這種狀況在年輕代常常發生,可是在老年代更常見的狀況是大部分對象都是存活對象。若是依然使用複製算法,因爲存活的對象較多,複製的成本也將很高。
標記-壓縮算法是一種老年代的回收算法,它在標記-清除算法的基礎上作了一些優化。也首先須要從根節點開始對全部可達對象作一次標記,但以後,它並不簡單地清理未標記的對象,而是將全部的存活對象壓縮到內存的一端。以後,清理邊界外全部的空間。這種方法既避免了碎片的產生,又不須要兩塊相同的內存空間,所以,其性價比比較高。

增量算法 (Incremental Collecting)

在垃圾回收過程當中,應用軟件將處於一種 CPU 消耗很高的狀態。在這種 CPU 消耗很高的狀態下,應用程序全部的線程都會掛起,暫停一切正常的工做,等待垃圾回收的完成。若是垃圾回收時間過長,應用程序會被掛起好久,將嚴重影響用戶體驗或者系統的穩定性。
增量算法的基本思想是,若是一次性將全部的垃圾進行處理,須要形成系統長時間的停頓,那麼就可讓垃圾收集線程和應用程序線程交替執行。每次,垃圾收集線程只收集一小片區域的內存空間,接着切換到應用程序線程。依次反覆,直到垃圾收集完成。使用這種方式,因爲在垃圾回收過程當中,間斷性地還執行了應用程序代碼,因此能減小系統的停頓時間。可是,由於線程切換和上下文轉換的消耗,會使得垃圾回收的整體成本上升,形成系統吞吐量的降低。

分代 (Generational Collecting)

根據垃圾回收對象的特性,不一樣階段最優的方式是使用合適的算法用於本階段的垃圾回收,分代算法便是基於這種思想,它將內存區間根據對象的特色分紅幾塊,根據每塊內存區間的特色,使用不一樣的回收算法,以提升垃圾回收的效率。以 Hot Spot 虛擬機爲例,它將全部的新建對象都放入稱爲年輕代的內存區域,年輕代的特色是對象會很快回收,所以,在年輕代就選擇效率較高的複製算法。當一個對象通過幾回回收後依然存活,對象就會被放入稱爲老生代的內存空間。在老生代中,幾乎全部的對象都是通過幾回垃圾回收後依然得以倖存的。所以,能夠認爲這些對象在一段時期內,甚至在應用程序的整個生命週期中,將是常駐內存的。若是依然使用複製算法回收老生代,將須要複製大量對象。再加上老生代的回收性價比也要低於新生代,所以這種作法也是不可取的。根據分代的思想,能夠對老年代的回收使用與新生代不一樣的標記-壓縮算法,以提升垃圾回收效率。

5. 以 分代(Generational Collecting) 算法爲例,說明 GC 機制

詞彙彙總:

Young generation :新生代
Eden : 伊甸園 (每一個新 New 出來的對象最開始存放的位置)
Survivor : 倖存區(圖中S0與S1)
Tenured / Old Generation :老年代
Permanent Generation :永久代

分代算法圖解

注意: S0 與 S1 的內存區域是同樣大的

下面講述其 GC 過程:

Step 1:
新建立的對象通常放在新生代的Eden區。
在 Eden 中有 「存活對象」 與 「待回收對象」,當Eden空間被使用完的時候,就會發生新生代GC,也就是Minor GC。

Step 2:
GC 會作如何操做:

  1. 把 「存活對象」 複製到S0中。

  2. 清空 Eden 區。

  3. 將 S0 中的 「存活對象」 年齡(Age)設置爲 1。

這樣第一次GC就完成了。
Step 3:
當Eden區再次被使用完的時候,就會再次進行GC操做。
GC 的操做以下:

  1. 將 Eden 區和 S0 中的「存活對象」 複製 到S1中。

  2. 清空 Eden 和 S0 區。

  3. 而後將 Eden 中複製到 S1 中的對象年齡設置爲 1,將 S0 中複製到 S1 中的對象年齡加 1。

這樣新生代第二次GC就完成了。

Step 4:

當Eden再一次被使用完的時候,就會發生第三次GC操做了。
以後基本重複上面的思路了,
GC 操做以下:

  1. 首先將 Eden 和 S1 中的 「存活對象」 複製到 S0 中。

  2. 而後將 Eden 和 S1 進行清空。

  3. 最後將 Eden 中複製到 S0 中的對象年齡設置爲1,將 S1 中複製到 S0 中的對象年齡加1。

以後就這樣循環了~~~

那 老年代 呢? 什麼時候纔會進入 老年代 ?
若是對象在 GC 過程當中沒有被回收,那麼它的對象年齡(Age)會不斷的增長,對象在Survivor區每熬過一個Minor GC,年齡就增長1歲,當它的年齡到達必定的程度(默認爲15歲),就會被移動到老年代,這個年齡閥值能夠經過-XX:MaxTenuringThreshold設置。

6. 參考文章

這些文章或者視頻資料都很不錯,建議有興趣能夠看看。

JVM 垃圾回收器工做原理及使用實例介紹
Java GC系列(2):Java垃圾回收是如何工做的?
YouTube 視頻:Garbage collection in Java, with Animation and discussion of G1 GC
Java GC系列(1):Java垃圾回收簡介
JVM內存回收理論與實現
JVM爲何須要GC

7. 結束

這些是整理的筆記,但願對你有幫助。

沒有GC機制的JVM是不能想象的,咱們只能經過不斷優化它的使用、不斷調整本身的應用程序,避免出現大量垃圾,而不是一味認爲GC形成了應用程序問題

相關文章
相關標籤/搜索