Yige

Yige

Build

JVM系列-JVM之垃圾收集

JVM 系列 - JVM 之垃圾收集#

內容來自:

  1. 垃圾收集策略與算法
  2. HotSpot 垃圾收集器

一、垃圾回收判定#

引用的種類#

  • 強引用(Strong Reference): 只要強引用存在,垃圾收集器永遠不會回收被引用的對象

  • 軟引用(Soft Reference): 當 JVM 認為內存不足時,在拋出 OutOfMemoryError 之前會去回收軟引用指向的對象

  • 弱引用(Weak Reference): 只要當 JVM 進行垃圾回收時,無論內存是否充足,都会回收被軟引用關聯的對象

  • 虛引用(Phantom Reference): 也稱幽靈引用或者幻影引用,它是最弱的一種引用關係。一个对象是否有虚引用的存在,完全不會對其生存時間構成影響。它僅僅是提供了一種確保對象被 finalize 以後,做某些事情的機制,比如,通常用來做所謂的 Post-Mortem 清理機制。

判定對象是否存活#

若一個對象不被任何對象或變量引用,那麼它就是無效對象,需要被回收。

引用計數法#

在對象頭維護著一個 counter 計數器,對象被引用一次則計數器 +1;若引用失效則計數器 -1。當計數器為 0 時,就認為該對象無效了。

引用計數算法的實現簡單,判定效率也很高,在大部分情況下它都是一個不錯的算法。但是主流的 Java 虛擬機裡沒有選用引用計數算法來管理內存,主要是因為它很難解決對象之間循環引用的問題:
比如對象 objA 和 objB 都有字段 instance,令 objA.instance = objB 並且 objB.instance = objA,由於它們互相引用著對方,導致它們的引用計數都不為 0,於是引用計數算法無法通知 GC 收集器回收它們。

可達性分析法#

所有和 GC Roots 直接或間接關聯的對象都是有效對象,和 GC Roots 沒有關聯的對象就是無效對象。

GC Roots 是指:

  • Java 虛擬機棧(棧幀中的本地變量表)中引用的對象
  • 本地方法棧中引用的對象
  • 方法區中常量引用的對象
  • 方法區中類靜態屬性引用的對象

GC Roots 並不包括堆中對象所引用的對象,這樣就不會有循環引用的問題。

回收堆中無效對象#

對於可達性分析中不可達的對象,也並不是沒有存活的可能。

判定 finalize () 是否有必要執行#

JVM 會判斷此對象是否有必要執行 finalize () 方法,如果對象沒有覆蓋 finalize () 方法,或者 finalize () 方法已經被虛擬機調用過,那麼視為 **“沒有必要執行”**。那麼對象基本上就真的被回收了。

如果對象被判定為有必要執行 finalize () 方法,那麼對象會被放入一個 F-Queue 隊列中,虛擬機會以較低的優先級執行這些 finalize () 方法,但不會確保所有的 finalize () 方法都會執行結束。如果 finalize () 方法出現耗時操作,虛擬機就直接停止指向該方法,將對象清除。

對象重生或死亡#

如果在執行 finalize () 方法時,將 this 赋给了某一個引用,那麼該對象就重生了。如果沒有,那麼就會被垃圾收集器清除。

注意:
任何一個對象的 finalize () 方法只會被系統自動調用一次,如果對象面臨下一次回收,它的 finalize () 方法不會被再次執行,想繼續在 finalize () 中自救就失效了。

回收方法區內存#

方法區中存放生命周期較長的類信息、常量、靜態變量,每次垃圾收集只有少量的垃圾被清除。方法區中主要清除兩種垃圾:

  • 廢棄常量
  • 無用的類

判定廢棄常量#

只要常量池中的常量不被任何變量或對象引用,那麼這些常量就會被清除掉。比如,一個字符串 "bingo" 進入了常量池,但是當前系統沒有任何一個 String 對象引用常量池中的 "bingo" 常量,也沒有其它地方引用這個字面量,必要的話,"bingo" 常量會被清理出常量池。

判定無用的類#

判定一個類是否是 “無用的類”,條件較為苛刻。

  • 該類的所有對象都已經被清除
  • 加載該類的 ClassLoader 已經被回收
  • 該類的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

一個類被虛擬機加載進方法區,那麼在堆中就會有一個代表該類的對象。這個對象在類被加載進方法區時創建,在方法區該類被刪除時清除。

內存泄露#

Java 中如果長生命周期的對象持有短生命周期的引用,就很可能會出現內存泄露,比如在單例模式中,很多時候我們可以把這個單例對象的生命周期與整個程序的生命周期看做差不多的,所以是一個長生命周期的對象。如果這個對象持有其他對象的引用,就有可能發生內存泄露

更詳細的內容請參考: JAVA 內存泄露詳解(原因、例子及解決)

二、垃圾收集算法#

學會了如何判定無效對象、無用類、廢棄常量之後,剩余工作就是回收這些垃圾。常見的垃圾收集算法有以下幾個:

標記 - 清除算法#

判斷哪些數據需要清除,並對它們進行標記,然後清除被標記的數據。

這種方法有兩個不足

  • 效率問題:標記和清除兩個過程的效率都不高。
  • 空間問題:標記清除之後會產生大量不連續的內存碎片,碎片太多可能導致以後需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。

複製算法(新生代)#

為了解決效率問題,“複製” 收集算法出現了。它將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊內存用完,需要進行垃圾收集時,就將存活者的對象複製到另一塊上面,然後將第一塊內存全部清除。這種算法有優有劣:

  • 優點:不會有內存碎片的問題。
  • 缺點:內存縮小為原來的一半,浪費空間。

為了解決空間利用率問題,可以將內存分為三塊: Eden、From Survivor、To Survivor,比例是 8:1:1,每次使用 Eden 和其中一塊 Survivor。回收時,將 Eden 和 Survivor 中還存活的對象一次性複製到另外一塊 Survivor 空間上,最後清理掉 Eden 和剛才使用的 Survivor 空間。這樣只有 10% 的內存被浪費。

但是我們無法保證每次回收都只有不多於 10% 的對象存活,當 Survivor 空間不夠,需要依賴其他內存(指老年代)進行分配擔保。

分配擔保#

通過清除老年代中廢棄數據來擴大老年代空閒空間,以便給新生代作擔保。

JDK 6 Update 24 之前:
在發生 Minor GC 之前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代所有對象總空間, 如果這個條件成立,Minor GC 可以確保是安全的; 如果不成立,則虛擬機會查看 HandlePromotionFailure 值是否設置為允許擔保失敗, 如果是,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小, 如果大於,將嘗試進行一次 Minor GC, 儘管這次 Minor GC 是有風險的; 如果小於,或者 HandlePromotionFailure 設置不允許冒險,那此時也要改為進行一次 Full GC。

JDK 6 Update 24 之後:
只要老年代的連續空間大於新生代對象總大小或者歷次晉升的平均大小,就會進行 Minor GC,否則將進行 Full GC。

這個過程就是分配擔保

標記 - 整理算法(老年代)#

在回收垃圾前,首先將廢棄對象做上標記,然後將未標記的對象移到一邊,最後清空另一邊區域即可。

這是一種老年代的垃圾收集算法。老年代的對象一般壽命比較長,因此每次垃圾回收會有大量對象存活,如果採用複製算法,每次需要複製大量存活的對象,效率很低。

分代收集算法#

根據對象存活周期的不同,將內存劃分為幾塊。一般是把 Java 堆分為新生代和老年代,針對各個年代的特點採用最適當的收集算法。

  • 新生代:複製算法
  • 老年代:標記 - 清除算法、標記 - 整理算法

三、垃圾收集器#

image.png

新生代垃圾收集器#

Serial 垃圾收集器(單線程)#

只開啟一條 GC 線程進行垃圾回收,並且在垃圾收集過程中停止一切用戶線程 (Stop The World)。

一般客戶端應用所需內存較小,不會創建太多對象,而且堆內存不大,因此垃圾收集器回收時間短,即使在這段時間停止一切用戶線程,也不會感覺明顯卡頓。因此 Serial 垃圾收集器適合客戶端使用

由於 Serial 收集器只使用一條 GC 線程,避免了線程切換的開銷,從而簡單高效

ParNew 垃圾收集器(多線程)#

ParNew 是 Serial 的多線程版本。由多條 GC 線程並行地進行垃圾清理。但清理過程依然需要 Stop The World。

ParNew 追求 **“低停頓時間”**, 與 Serial 唯一区别就是使用了多線程進行垃圾收集,在多 CPU 環境下性能比 Serial 會有一定程度的提升;但線程切換需要額外的開銷,因此在單 CPU 環境中表現不如 Serial

Parallel Scavenge 垃圾收集器(多線程)#

Parallel Scavenge 和 ParNew 一樣,都是多線程、新生代垃圾收集器。但是兩者有巨大的不同點:

  • Parallel Scavenge:追求 CPU 吞吐量,能夠在較短時間內完成指定任務,因此適合沒有交互的後台計算。
  • ParNew:追求降低用戶停頓時間,適合交互式應用。

追求高吞吐量,可以通過減少 GC 執行實際工作的時間,然而,仅仅偶爾運行 GC 意味著每當 GC 運行時將有許多工作要做,因為在此期間積累在堆中的對象數量很高。單個 GC 需要花更多的時間來完成,從而導致更高的暫停時間。而考慮到低暫停時間,最好頻繁運行 GC 以便更快速完成,反過來又導致吞吐量下降。

  • 通過參數 -XX:GCTimeRadio 設置垃圾回收時間占總 CPU 時間的百分比
  • 通過參數 -XX:MaxGCPauseMillis 設置垃圾處理過程最久停頓時間
  • 通過命令 -XX:+UseAdaptiveSizePolicy 開啟自適應策略。我們只要設置好堆的大小和 MaxGCPauseMillis 或 GCTimeRadio,收集器會自動調整新生代的大小、Eden 和 Survivor 的比例、對象進入老年代的年齡,以最大程度上接近我們設置的 MaxGCPauseMillis 或 GCTimeRadio

老年代垃圾收集器#

Serial Old 垃圾收集器(單線程)#

Serial Old 收集器是 Serial 的老年代版本,都是單線程收集器,只啟用一條 GC 線程,都適合客戶端應用。它們唯一的區別就是:Serial Old 工作在老年代,使用“標記-整理”算法;Serial 工作在新生代,使用“複製”算法

Parallel Old 垃圾收集器(多線程)#

Parallel Old 收集器是 Parallel Scavenge 的老年代版本,追求 CPU 吞吐量

CMS 垃圾收集器#

CMS (Concurrent Mark Sweep,並發標記清除) 收集器是以獲取最短回收停頓時間為目標的收集器(追求低停頓),它在垃圾收集時使得用戶線程和 GC 線程並發執行,因此在垃圾收集過程中用戶也不會感到明顯的卡頓。

CMS 收集器是一種 “標記-清除”算法實現的,整個過程分為四個步驟:

  • 初始標記:Stop The World,仅使用一条初始标记线程对所有与 GC Roots 直接关联的对象进行标记。
  • 並發標記:使用多條標記線程,與用戶線程並發執行。此過程進行可達性分析,標記出所有廢棄對象。速度很慢。
  • 重新標記:Stop The World,使用多條標記線程並發執行,將剛才並發標記過程中新出現的廢棄對象標記出來。
  • 並發清除:只使用一條 GC 線程,與用戶線程並發執行,清除剛才標記的對象。這個過程非常耗時。

並發標記與並發清除過程耗時最長,且可以與用戶線程一起工作,因此,總體上說,CMS 收集器的內存回收過程是與用戶線程一起並發執行的

缺點

  • 對 CPU 資源敏感,吞吐量低;
  • 無法處理浮動垃圾,導致頻繁 Full GC;
  • 它使用的回收算法 -“標記 - 清除” 算法會導致收集結束時會有大量空間碎片產生

G1 通用垃圾收集器#

G1 (Garbage-First) 是一款面向服務端應用的垃圾收集器,它沒有新生代和老年代的概念,而是將堆劃分為一塊塊獨立的 Region。G1 收集器在後台維護了一個優先列表,進行垃圾回收時,首先估計每個 Region 中垃圾的數量,然後根據允許的收集時間,優先選擇回收價值最大的 Region 當要進行垃圾收集。這種使用 Region 劃分內存空間以及有優先級的區域回收方式,保證了 G1 收集器在有限時間內可以盡可能高的收集效率(把內存化整為零)

從整體上看, G1 是基於 “標記 - 整理” 算法實現的收集器,從局部(兩個 Region 之間)上看是基於 “複製” 算法實現的,這意味著運行期間不會產生內存空間碎片。

每個 Region 都有一個 Remembered Set,用於記錄本區域中所有對象引用的對象所在的區域,進行可達性分析時,只要在 GC Roots 中再加上 Remembered Set 即可防止對整個堆內存進行遍歷。因此即使一個對象和它內部所引用的對象可能不在同一個 Region 中,那麼當垃圾回收時,也不需要掃描整個堆內存才能完整地進行一次可達性分析

如果不計算維護 Remembered Set 的操作,G1 收集器的工作過程分為以下幾個步驟:

  • 初始標記:Stop The World,仅使用一条初始标记线程对所有与 GC Roots 直接关联的对象进行标记。
  • 並發標記:使用一條標記線程與用戶線程並發執行。此過程進行可達性分析,速度很慢。
  • 最終標記:Stop The World,使用多條標記線程並發執行。
  • 篩選回收:回收廢棄對象,此時也要 Stop The World,並使用多條篩選回收線程並發執行

補充拓展#

觸發 JVM 進行 Full GC 的情況#

System.gc () 方法的調用#

此方法的調用是建議 JVM 進行 Full GC,注意這只是建議而非一定,但在很多情況下它會觸發 Full GC,從而增加 Full GC 的頻率。通常情況下我們只需要讓虛擬機自己去管理內存即可,我們可以通過 -XX:+ DisableExplicitGC 來禁止調用 System.gc ()。

老年代空間不足#

老年代空間不足會觸發 Full GC 操作,若進行該操作後空間依然不足,則會拋出如下錯誤:java.lang.OutOfMemoryError: Java heap space

永久代空間不足#

JVM 規範中運行時數據區域中的方法區,在 HotSpot 虛擬機中也稱為永久代(Permanet Generation),存放一些類信息、常量、靜態變量等數據,當系統要加載的類、反射的類和調用的方法較多時,永久代可能會被佔滿,會觸發 Full GC。如果經過 Full GC 仍然回收不了,那麼 JVM 會拋出如下錯誤信息:java.lang.OutOfMemoryError: PermGen space

其他#

  • CMS GC 時出現 promotion failed 和 concurrent mode failure
    promotion failed,就是上文所說的分配擔保失敗,而 concurrent mode failure 是在執行 CMS GC 的過程中同時有對象要放入老年代,而此時老年代空間不足造成的。

  • 統計得到的 Minor GC 晉升到舊生代的平均大小大於老年代的剩余空間

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。