1. 程式人生 > >JVM (四)--垃圾收集(二)

JVM (四)--垃圾收集(二)

一、垃圾收集演算法

1、標記-清除 

將存活的物件進行標記,然後清除掉未被標記的物件。

不足:

  • 標記和清除過程中效率多不高;
  • 會產生大量不連續的記憶體碎片,導致無法給大物件分配記憶體。

2、標記-整理

讓所有存活的物件都向一端移動,然後直接清除掉端邊界以外的記憶體。

3、複製

將記憶體劃分為大小相等的兩塊,每次只使用一塊,當這一塊記憶體用完就將還存活的物件複製到另外一塊上面,然後再把使用過的記憶體空間進行一次清理。

主要不足:只使用了記憶體的一半。

現在的商業虛擬機器都採用這種收集演算法來回收新生代,但是並不是將記憶體劃分為大小相等的兩塊,而是分為一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 空間和其中一塊 Survivor。在回收時,將 Eden 和 Survivor 中還存活著的物件一次性複製到另一塊 Survivor 空間上,最後清理 Eden 和使用過的那一塊Survivor。HotSpot 虛擬機器的 Eden 和 Survivor 的大小比例預設為 8:1,保證了記憶體的利用率達到 90%。如果每次回收有多於 10% 的物件存活,那麼一塊 Survivor 空間就不夠用了,此時需要依賴於老年代進行分配擔保,也就是借用老年代的空間儲存放不下的物件。

二、分代回收機制

現在的商業虛擬機器大多采用分代收集機制,它根據物件存活週期將記憶體劃分為幾塊,不同塊採用適當的收集演算法。

一般將Java堆分為新生代和老年代:

  • 新生代:複製演算法;
  • 老年代:標記-清理  或者  標記-整理 演算法

三、垃圾收集器

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

  • 單執行緒與並行(多執行緒):單執行緒指的是垃圾收集器只使用一個執行緒進行收集,二並行使用多個執行緒。
  • 序列與併發:序列指的是垃圾收集器與使用者程式交替執行,這意味著在執行垃圾收集的時候需要停頓使用者程式;併發知道是垃圾收集器和使用者程式同時執行。出來CMS和G1之外,其他垃圾收集器都是以序列的方式執行。

1)Serial收集器

Serial翻譯為序列,也就是說它是以序列的方式執行。

它是單執行緒的收集器,只會使用一個執行緒進行垃圾是收集工作。

它的優點是簡單高效,對於單個CPU環境來說,由於沒有執行緒互動的開銷,因此擁有最高的單執行緒收集效率。

它是Client模式下的預設新生代收集器,因為在使用者的桌面應用場景下,分配給i虛擬機器管理的記憶體一般來說不會很大。 Serial收集器收集幾十兆甚至一兩百兆的新生代停頓時間可以控制在一百多毫秒以內,只要不是太頻繁,這點停頓是可以接受的。

2)ParNew收集器

它是Serial收集器的多執行緒版本。

ParNew收集器是Server模式下的虛擬機器首選的新生代收集器,除了效能的原因外,主要是因為除了Serial收集器,只有它能與CMS收集器配合工作。

預設開啟的執行緒數量與CPU數量相同,可以使用-XX:ParallelThreads引數來設定執行緒數。

3)Paraller Scavenge收集器

與ParNew 一樣是並行的多執行緒收集器。

其他收集器關注點是儘可能縮短垃圾收集器時使用者執行緒停頓時間,而它的目標是達到一個可控制的吞吐量,它被稱為“吞吐量優先”收集器。這裡的吞吐量是指CPU用於執行使用者程式碼的時間佔總時間的比值。停頓時間越短就越適合需要與使用者互動的程式,良好的響應速度能提升使用者體驗。而高吞吐量則可以高效率地利用CPU,儘快完成的執行任務,主要適合在後臺運算而不需要太大互動的任務。

提供了兩個引數用於精確控制吞吐量,分別是控制最大垃圾收集停頓時間 -XX:MaxGCPauseMillis 引數以及直接設定吞吐量大小的 -XX:GCTimeRatio 引數(值為大於 0 且小於 100 的整數) 。縮短停頓時間是以犧牲吞吐量和新生代空間來換取的:新生代空間變小,垃圾回收變得頻繁,導致吞吐量下降。還提供了一個引數 -XX:+UseAdaptiveSizePolicy,這是一個開關引數,開啟引數
後,就不需要手工指定新生代的大小(-Xmn) 、Eden 和 Survivor 區的比例(-XX:SurvivorRatio) 、晉升老年代物件年齡(-XX:PretenureSizeThreshold) 等細節引數了,虛擬機器會根據當前系統的執行情況收集效能監控資訊,動態調整這些引數
以提供最合適的停頓時間或者最大的吞吐量,這種方式稱為 GC 自適應的調節策略(GC Ergonomics) 。

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。

7、G1收集器

G1(Garbage-First) ,它是一款面向服務端應用的垃圾收集器,在多 CPU 和大記憶體的場景下有很好的效能。HotSpot 開發團隊賦予它的使命是未來可以替換掉 CMS收集器。Java 堆被分為新生代、老年代和永久代,其它收集器進行收集的範圍都是整個新生代或者老生代,而 G1 可以直接對新生代和永久代一起回收。

G1 把堆劃分成多個大小相等的獨立區域(Region) ,新生代和永久代不再物理隔離。

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

每個 Region 都有一個 Remembered Set,用來記錄該 Region 物件的引用物件所在的 Region。通過使用 Remembered Set,在做可達性分析的時候就可以避免全堆掃描。

如果不計算維護 Remembered Set 的操作,G1 收集器的運作大致可劃分為以下幾
個步驟:

  • 初始標記
  • 併發標記
  • 最終標記:為了修正在併發標記期間因使用者程式繼續運作而導致標記產生變動的那一部分標記記錄,虛擬機器將這段時間物件變化記錄線上程的 RememberedSet Logs 裡面,最終標記階段需要把 Remembered Set Logs 的資料合併到Remembered Set 中。這階段需要停頓執行緒,但是可並行執行。篩選回收:首先對各個 Region 中的回收價值和成本進行排序,根據使用者所期望的 GC 停頓時間來制定回收計劃。此階段其實也可以做到與使用者程式一起併發執行,但是因為只回收一部分 Region,時間是使用者可控制的,而且停頓使用者執行緒將大幅度提高收集效率。

具備如下特點:

  • 空間整合:整體來看是基於“標記 - 整理”演算法實現的收集器,從區域性(兩個Region 之間) 上來看是基於“複製”演算法實現的,這意味著執行期間不會產生記憶體空間碎片。
  • 可預測的停頓:能讓使用者明確指定在一個長度為 M 毫秒的時間片段內,消耗在 GC 上的時間不得超過 N 毫秒。

7種垃圾收集器的比較:

記憶體分配與回收策略

1. Minor GC 和 Full GC

  • Minor GC:發生在新生代上,因為新生代物件存活時間很短,因此 Minor GC會頻繁執行,執行的速度一般也會比較快。
  • Full GC:發生在老年代上,老年代物件其存活時間長,因此 Full GC 很少執行,執行速度會比 Minor GC 慢很多。

2. 記憶體分配策略
(一) 物件優先在 Eden 分配
大多數情況下,物件在新生代 Eden 區分配,當 Eden 區空間不夠時,發起 Minor GC。
(二) 大物件直接進入老年代
大物件是指需要連續記憶體空間的物件,最典型的大物件是那種很長的字串以及陣列。
經常出現大物件會提前觸發垃圾收集以獲取足夠的連續空間分配給大物件。
-XX:PretenureSizeThreshold,大於此值的物件直接在老年代分配,避免在 Eden區和 Survivor 區之間的大量記憶體複製。
(三) 長期存活的物件進入老年代
為物件定義年齡計數器,物件在 Eden 出生並經過 Minor GC 依然存活,將移動到Survivor 中,年齡就增加 1 歲,增加到一定年齡則移動到老年代中。
-XX:MaxTenuringThreshold 用來定義年齡的閾值。
(四) 動態物件年齡判定
虛擬機器並不是永遠地要求物件的年齡必須達到 MaxTenuringThreshold 才能晉升老年代,如果在 Survivor 區中相同年齡所有物件大小的總和大於 Survivor 空間的一半,則年齡大於或等於該年齡的物件可以直接進入老年代,無需等到
MaxTenuringThreshold 中要求的年齡。
(五) 空間分配擔保
在發生 Minor GC 之前,虛擬機器先檢查老年代最大可用的連續空間是否大於新生代所有物件總空間,如果條件成立的話,那麼 Minor GC 可以確認是安全的;如果不成立的話虛擬機器會檢視 HandlePromotionFailure 設定值是否允許擔保失敗,如果允許那麼就會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代物件的平均大小,如果大於,將嘗試著進行一次 Minor GC,儘管這次 Minor GC 是有風險的;如果小於,或者 HandlePromotionFailure 設定不允許冒險,那這時也要改為進行一次 Full GC。
3. Full GC 的觸發條件
對於 Minor GC,其觸發條件非常簡單,當 Eden 區空間滿時,就將觸發一次 Minor GC。而 Full GC 則相對複雜,有以下條件:
(一) 呼叫 System.gc()
只是建議虛擬機器執行 Full GC,但是虛擬機器不一定真正去執行。不建議使用這種方式,而是讓虛擬機器管理記憶體。
(二) 老年代空間不足
老年代空間不足的常見場景為前文所講的大物件直接進入老年代、長期存活的物件進入老年代等。為了避免以上原因引起的 Full GC,應當儘量不要建立過大的物件以及陣列。除此之外,可以通過 -Xmn 虛擬機器引數調大新生代的大小,讓物件儘量在新生代被回收掉,不進入老年代。還可以通過 -XX:MaxTenuringThreshold 調大物件進入老年代的年齡,讓物件在新生代多存活一段時間。
(三) 空間分配擔保失敗
使用複製演算法的 Minor GC 需要老年代的記憶體空間作擔保,如果擔保失敗會執行一次 Full GC。具體內容請參考上面的第五小節。
(四) JDK 1.7 及以前的永久代空間不足
在 JDK 1.7 及以前,HotSpot 虛擬機器中的方法區是用永久代實現的,永久代中存放的為一些 Class 的資訊、常量、靜態變數等資料,當系統中要載入的類、反射的類和呼叫的方法較多時,永久代可能會被佔滿,在未配置為採用 CMS GC 的情況下也會執行 Full GC。如果經過 Full GC 仍然回收不了,那麼虛擬機器會丟擲java.lang.OutOfMemoryError。為避免以上原因引起的 Full GC,可採用的方法為增大永久代空間或轉為使用 CMS GC。
(五) Concurrent Mode Failure
執行 CMS GC 的過程中同時有物件要放入老年代,而此時老年代空間不足(有時候“空間不足”是指 CMS GC 當前的浮動垃圾過多導致暫時性的空間不足) ,便會報Concurrent Mode Failure 錯誤,並觸發 Full GC。