1. 程式人生 > >Java 垃圾回收(GC) 泛讀

Java 垃圾回收(GC) 泛讀

其中 中斷 bsp 之前 後臺 轉換 actions 一次 需要

Java 垃圾回收(GC) 泛讀

文章地址:https://segmentfault.com/a/1190000008922319

0. 序言

帶著問題去看待 垃圾回收(GC) 會比較好,一般來說主要的疑惑在於這麽幾點:

  • 為什麽需要 GC ?
  • 虛擬機(JVM) 與 垃圾回收(GC) 的關系?
  • GC 的原理有哪些?
  • 哪些 對象容易被 GC ?
  • 等等

帶著這些問題往下看:

1. 為什麽需要 GC ?

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

為什麽需要 GC?
主要是隨著應用程序所應對的業務越來越龐大、復雜,用戶越來越多,沒有GC就不能保證應用程序正常進行。

為什麽經常討論 GC,沒有完美的解決方案嗎?


完美的解決方法目前還沒有。由於在 GC 時需要STW(Stop The World),這長不能滿足實際的需求,容易造成卡頓、延遲等性能問題,所以才會不斷地嘗試對GC進行優化。社區的需求是盡量減少對應用程序的正常執行幹擾,這也是業界目標。

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

以 HotSpotJVM 為例描述下 GC 在 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造成了應用程序問題

Java 垃圾回收(GC) 泛讀