為什麼 JVM 需要 GC

麥克周發表於2017-02-28

社群內有人發起了一個討論,關於JVM是否一定需要GC?他們認為應用程式的回收目標是構建一個僅用來處理記憶體分配,而不執行任何真正的記憶體回收操作的 GC。即僅當可用的 Java 堆耗盡的時候,才進行順序的 JVM 停頓操作。

首先需要理解為什麼需要GC。隨著應用程式所應對的業務越來越龐大、複雜,使用者越來越多,沒有GC就不能保證應用程式正常進行。而經常造成STW的GC又跟不上實際的需求,所以才會不斷地嘗試對GC進行優化。

社群的需求是儘量減少對應用程式的正常執行干擾,這也是業界目標。Oracle在JDK7時釋出G1 GC的目的是為了減少應用程式停頓發生的可能性,讓我們通過本文來了解G1 GC所做的工作。

JVM發展歷史簡介

還記得機器貓嗎?他和康夫有一張書桌,書桌的抽屜其實是一個時空穿梭通道,讓我們操作機器貓的時空機器,回到1998年。那年的12月8日,第二代Java平臺的企業版J2EE正式對外發布。為了配合企業級應用落地,1999年4月27日,Java程式的舞臺—Java HotSpot Virtual Machine(以下簡稱HotSpot )正式對外發布,並從這之後釋出的JDK1.3版本開始,HotSpot成為Sun JDK的預設虛擬機器。

GC發展歷史簡介

1999年隨JDK1.3.1一起來的是序列方式的Serial GC ,它是第一款GC,並且這只是起點。此後,JDK1.4和J2SE1.3相繼釋出。2002年2月26日,J2SE1.4釋出,Parallel GC 和Concurrent Mark Sweep (CMS)GC跟隨JDK1.4.2一起釋出,並且Parallel GC在JDK6之後成為HotSpot預設GC。

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

  • 如果你想要最小化地使用記憶體和並行開銷,請選Serial GC;
  • 如果你想要最大化應用程式的吞吐量,請選Parallel GC;
  • 如果你想要最小化GC的中斷或停頓時間,請選CMS GC。

那麼問題來了,既然我們已經有了上面三個強大的GC,為什麼還要釋出Garbage First(G1)GC?原因就在於應用程式所應對的業務越來越龐大、複雜,使用者越來越多,沒有GC就不能保證應用程式正常進行,而經常造成STW的GC又跟不上實際的需求,所以才會不斷地嘗試對GC進行優化。

為什麼名字叫做Garbage First(G1)呢?

因為G1是一個並行回收器,它把堆記憶體分割為很多不相關的區間(Region),每個區間可以屬於老年代或者年輕代,並且每個年齡代區間可以是物理上不連續的。

老年代區間這個設計理念本身是為了服務於並行後臺執行緒,這些執行緒的主要工作是尋找未被引用的物件。而這樣就會產生一種現象,即某些區間的垃圾(未被引用物件)多於其他的區間。

垃圾回收時實則都是需要停下應用程式的,不然就沒有辦法防治應用程式的干擾 ,然後G1 GC可以集中精力在垃圾最多的區間上,並且只會費一點點時間就可以清空這些區間裡的垃圾,騰出完全空閒的區間。

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

G1 GC基本思想

G1 GC是一個壓縮收集器,它基於回收最大量的垃圾原理進行設計。G1 GC利用遞增、並行、獨佔暫停這些屬性,通過拷貝方式完成壓縮目標。此外,它也藉助並行、多階段並行標記這些方式來幫助減少標記、重標記、清除暫停的停頓時間,讓停頓時間最小化是它的設計目標之一。

G1回收器是在JDK1.7中正式投入使用的全新的垃圾回收器,從長期目標來看,它是為了取代CMS 回收器。G1回收器擁有獨特的垃圾回收策略,這和之前提到的回收器截然不同。從分代上看,G1依然屬於分代型垃圾回收器,它會區分年輕代和老年代,年輕代依然有Eden區和Survivor區,但從堆的結構上看,它並不要求整個Eden區、年輕代或者老年代在物理上都是連續。

綜合來說,G1使用了全新的分割槽演算法,其特點如下所示:

  1. 並行性:G1在回收期間,可以有多個GC執行緒同時工作,有效利用多核計算能力;
  2. 併發性:G1擁有與應用程式交替執行的能力,部分工作可以和應用程式同時執行,因此,一般來說,不會在整個回收階段發生完全阻塞應用程式的情況;
  3. 分代GC:G1依然是一個分代收集器,但是和之前的各類回收器不同,它同時兼顧年輕代和老年代。對比其他回收器,或者工作在年輕代,或者工作在老年代;
  4. 空間整理:G1在回收過程中,會進行適當的物件移動,不像CMS只是簡單地標記清理物件。在若干次GC後,CMS必須進行一次碎片整理。而G1不同,它每次回收都會有效地複製物件,減少空間碎片,進而提升內部迴圈速度。
  5. 可預見性:由於分割槽的原因,G1可以只選取部分割槽域進行記憶體回收,這樣縮小了回收的範圍,因此對於全域性停頓情況的發生也能得到較好的控制。

隨著G1 GC的出現,GC從傳統的連續堆記憶體佈局設計,逐漸走向不連續記憶體塊,這是通過引入Region概念實現,也就是說,由一堆不連續的Region組成了堆記憶體。其實也不能說是不連續的,只是它從傳統的物理連續逐漸改變為邏輯上的連續,這是通過Region的動態分配方式實現的,我們可以把一個Region分配給Eden、Survivor、老年代、大物件區間、空閒區間等的任意一個,而不是固定它的作用,因為越是固定,越是呆板。

G1 GC垃圾回收機制

通過市場的力量,不斷淘汰舊的行業,把有限的資源讓給那些競爭力更強、利潤率更高的企業。類似地,矽谷也在不斷淘汰過時的人員,從全世界吸收新鮮血液。經過半個多世紀的發展,在矽谷地區便形成只有卓越才能生存的文化。本著這樣的理念,GC承擔了淘汰垃圾、儲存優良資產的任務。

G1 GC在回收暫停階段會回收最大量的堆內區間(Region),這是它的設計目標,通過回收區間達到回收垃圾的目的。這裡只有一個例外情況,這個例外發生在並行標記階段的清除(Cleanup)步驟,如果G1 GC在清除步驟發現所有的區間都是由可回收垃圾組成的,那麼它會立即回收這些區間,並且將這些區間插入到一個基於LinkedList實現的空閒區間佇列裡,以待後用。因此,釋放這些區間並不需要等待下一個垃圾回收中斷,它是實時執行的,即清除階段起到了最後一道把控作用。這是G1 GC和之前的幾代GC的一大差別。

G1 GC的垃圾回收迴圈由三個主要型別組成:

  • 年輕代迴圈
  • 多步驟並行標記迴圈
  • 混合收集迴圈
  • Full GC

在年輕代回收期,G1 GC暫停應用程式執行緒,然後從年輕代區間移動存活物件到Survivor區間或者老年區間,也有可能是兩個區間都會涉及。對於一個混合回收期,G1 GC從老年區間移動存活物件到空閒區間,這些空閒區間也就成為了老年代的一部分。

G1的區間設計靈感

為了加快GC的回收速度,HotSpot的歷代GC都有自己的不同的設計方案,區間概念在軟體設計、架構領域並不是一個新名詞,關係型資料庫、列式資料庫最先使用這個概念提升資料存、取速度,軟體架構設計時也廣泛使用這樣的分割槽概念加快資料交換、計算。

為什麼會有區間這個設計想法?大家一定看過電視劇《大宅門》吧?大宅門所描述的北京知名醫術世家白家是這本電視劇的主角。白家有三兄弟,沒有分家之前,由老爺子一手掌管全家,老爺子看似是個精明人,實質是個糊塗的人,否則也不會弄得後來白家家破人散。白家的三兄弟在沒有分家之前,老大一家很老實,老二很懦弱,性格像女人,雖然肚子裡明白道理,但是不敢出來做主。老三年輕時混蛋一個,每次出外採購藥材都要私吞家裡的銀兩,造成賬目混亂。老大為了家庭和睦,一直在私下倒貼銀兩,讓老爺子能夠看到一本正常的賬目。這樣的一家子聚在一起,遲早家庭內部會出現問題,倒不如分家,你也不用算計家裡的錢了,分給你,分給你的錢有本事守住,沒本事就一直拮据下去吧。這就是最原始的分割槽(Region)概念。

我們回到技術,看看HBase的RegionServer設計方式。在HBase內部,所有的使用者資料以及後設資料的請求,在經過Region的定位,最終會落在RegionServer上,並由RegionServer實現資料的讀寫操作。RegionServer是HBase叢集執行在每個工作節點上的服務。它是整個HBase系統的關鍵所在,一方面它維護了Region的狀態,提供了對於Region的管理和服務;另一方面,它與Master互動,上傳Region的負載資訊上傳,參與Master的分散式協調管理。

HRegionServer與HMaster以及Client之間採用RPC協議進行通訊。HRegionServer向HMaster定期彙報節點的負載狀況,包括RS記憶體使用狀態、線上狀態的Region等資訊。在該過程中HRegionServer扮演了RPC客戶端的角色,而HMaster扮演了RPC伺服器端的角色。HRegionServer內建的RpcServer實現了資料更新、讀取、刪除的操作,以及Region涉及到Flush、Compaction、Open、Close、Load檔案等功能性操作。

Region是HBase資料儲存和管理的基本單位。HBase使用RowKey將表水平切割成多個HRegion,從HMaster的角度,每個HRegion都紀錄了它的StartKey和EndKey(第一個HRegion的StartKey為空,最後一個HRegion的EndKey為空),由於RowKey是排序的,因而Client可以通過HMaster快速的定位每個RowKey在哪個HRegion中。HRegion由HMaster分配到相應的HRegionServer中,然後由HRegionServer負責HRegion的啟動和管理,和Client的通訊,負責資料的讀(使用HDFS)。每個HRegionServer可以同時管理1000個左右的HRegion。

再來看看軟體系統架構方面的分割槽設計。以任務排程為例,假設我們有一箇中心排程服務,那麼當資料量不斷增多,這個中心排程服務一定會遇到效能瓶頸,因為所有的請求都會最終指向它。為了解決這個效能瓶頸,我們可以將任務排程拆分為多個服務,即這多個服務都可以處理任務排程工作,那麼問題來了,每個任務排程服務處理的源資料是否需要完全一致?

根據華為公司釋出的專利發明,顯示他們對於每一個任務排程服務有資料來源區分的操作,即按照任務排程數量對源資料進行劃分,比如3個任務排程服務,那麼源資料按照行號對3取餘的方式劃分,如果執行了一段時間之後,任務排程服務出現了數量上的增減,那麼這個取餘劃分需要重新進行,要按照那個時候的任務排程數量重新劃分割槽間。

回到G1。在G1中,堆被平均分成若干個大小相等的區域(Region)。每個Region都有一個關聯的Remembered Set(簡稱RS),RS的資料結構是Hash表,裡面的資料是Card Table (堆中每512byte對映在card table 1byte)。

簡單的說RS裡面存在的是Region中存活物件的指標。當Region中資料發生變化時,首先反映到Card Table中的一個或多個Card上,RS通過掃描內部的Card Table得知Region中記憶體使用情況和存活物件。在使用Region過程中,如果Region被填滿了,分配記憶體的執行緒會重新選擇一個新的Region,空閒Region被組織到一個基於連結串列的資料結構(LinkedList)裡面,這樣可以快速找到新的Region。

總結

沒有GC機制的JVM是不能想象的,我們只能通過不斷優化它的使用、不斷調整自己的應用程式,避免出現大量垃圾,而不是一味認為GC造成了應用程式問題。

G1GC優化參考文件

作者介紹

周明耀,2004年畢業於浙江大學,工學碩士,國外投資銀行12年工作經驗,4年分散式系統、物聯網工作經驗,10年技術團隊管理經驗。IBM開發者論壇專家作者、InfoQ專欄作者,著有《大話Java效能優化》、《深入理解JVM&G1 GC》,提交分散式計算領域發明專利超過15項。

相關文章