Java GC的那些事(2)
收集演算法
垃圾收集演算法主要有:標記-清除、複製和標記-整理。
1、標記-清除演算法
對待回收的物件進行標記。
演算法缺點:效率問題,標記和清除過程效率都很低;空間問題,收集之後會產生大量的記憶體碎片,不利於大物件的分配。
2、複製演算法
複製演算法將可用記憶體劃分成大小相等的兩塊A和B,每次只使用其中一塊,當A的記憶體用完了,就把存活的物件複製到B,並清空A的記憶體,不僅提高了標記的效率,因為只需要標記存活的物件,同時也避免了記憶體碎片的問題,代價是可用記憶體縮小為原來的一半。
3、標記-整理演算法
在老年代中,物件存活率較高,複製演算法的效率很低。在標記-整理演算法中,標記出所有存活的物件,並移動到一端,然後直接清理邊界以外的記憶體。
物件標記過程
在可達性分析過程中,為了準確找出與GC Roots相關聯的物件,必須要求整個執行引擎看起來像是被凍結在某個時間點上,即暫停所有執行中的執行緒,不可以出現物件的引用關係還在不斷變化的情況。
如何快速列舉GC Roots?
GC Roots主要在全域性性的引用(常量或類靜態屬性)與執行上下文(本地變數表中的引用)中,很多應用僅僅方法區就上百兆,如果進行遍歷查詢,效率會非常低下。
在HotSpot中,使用一組稱為OopMap的資料結構進行實現。類載入完成時,HotSpot把物件內什麼偏移量上是什麼型別的資料計算出來儲存到OopMap中,通過JIT編譯出來的原生程式碼,也會記錄下棧和暫存器中哪些位置是引用。GC發生時,通過掃描OopMap的資料就可以快速標識出存活的物件。
如何安全的GC?
執行緒執行時,只有在到達安全點(Safe Point)才能停頓下來進行GC。
基於OopMap資料結構,HotSpot可以快速完成GC Roots的遍歷,不過HotSpot並不會為每條指令都生成對應的OopMap,只會在Safe Point處記錄這些資訊。
所以Safe Point的選擇很重要,如果太少可能導致GC等待的時間太長,如果太頻繁可能導致執行時的效能問題。大部分指令的執行時間都非常短暫,通常會選擇一些執行時間較長的指令作為Safe Point,如方法呼叫、迴圈跳轉和異常跳轉等。
關於Safe Point更多的資訊,可以看看這篇文章 JVM的Stop The World,安全點,黑暗的地底世界
發生GC時,如何讓所有執行緒跑到最近的Safe Point再暫停?
當發生GC時,不直接對執行緒進行中斷操作,而是簡單的設定一箇中斷標誌,每個執行緒執行到Safe Point的時候,主動去輪詢這個中斷標誌,如果中斷標誌為真,則將自己進行中斷掛起。
這裡忽略了一個問題,當發生GC時,執行中的執行緒可以跑到Safe Point後進行掛起,而那些處於Sleep或Blocked狀態的執行緒在此時無法響應JVM的中斷請求,無法到Safe Point處進行掛起,針對這種情況,可以使用安全區域(Safe Region)進行解決。
Safe Region是指在一段程式碼片段中,物件的引用關係不會發生變化,在這個區域中的任何位置開始GC都是安全的。
1、當執行緒執行到Safe Region的程式碼時,首先標識已經進入了Safe Region,如果這段時間內發生GC,JVM會忽略標識為Safe Region狀態的執行緒;
2、當執行緒即將離開Safe Region時,會檢查JVM是否已經完成GC,如果完成了,則繼續執行,否則執行緒必須等待直到收到可以安全離開Safe Region的訊號為止;
垃圾收集器
Java虛擬機器規範並沒有規定垃圾收集器應該如何實現,使用者可以根據系統特點對各個區域所使用的收集器進行組合使用。
上圖展示了7種不同分代的收集器,如果兩兩之間存在連線,說明可以組合使用。
1、Serial收集器(序列GC)
Serial 是一個採用單個執行緒並基於複製演算法工作在新生代的收集器,進行垃圾收集時,必須暫停其他所有的工作執行緒。對於單CPU環境來說,Serial由於沒有執行緒互動的開銷,可以很高效的進行垃圾收集動作,是Client模式下新生代預設的收集器。
2、ParNew收集器(並行GC)
ParNew其實是serial的多執行緒版本,除了使用多條執行緒進行垃圾收集之外,其餘行為與Serial一樣。
3、Parallel Scavenge收集器(並行回收GC)
Parallel Scavenge是一個採用多執行緒基於複製演算法並工作在新生代的收集器,其關注點在於達到一個可控的吞吐量,經常被稱為“吞吐量優先”的收集器。
吞吐量 = 使用者程式碼執行時間 /(使用者程式碼執行時間 + 垃圾收集時間)
Parallel Scavenge提供了兩個引數用於精確控制吞吐量:
1、-XX:MaxGCPauseMillis 設定垃圾收集的最大停頓時間
2、-XX:GCTimeRatio 設定吞吐量大小
4、Serial Old收集器(序列GC)
Serial Old 是一個採用單執行緒基於標記-整理演算法並工作在老年代的收集器,是Client模式下老年代預設的收集器。
5、Parallel Old收集器(並行GC)
Parallel Old是一個採用多執行緒基於標記-整理演算法並工作在老年代的收集器。在注重吞吐量以及CPU資源敏感的場合,可以優先考慮Parallel Scavenge和Parallel Old的收集器組合。
6、CMS收集器(併發GC)
CMS(Concurrent Mark Sweep)是一種以獲取最短回收停頓時間為目標的收集器,工作在老年代,基於“標記-清除”演算法實現,整個過程分為以下4步:
1、初始標記:這個過程只是標記以下GC Roots能夠直接關聯的物件,但是仍然會Stop The World;
2、併發標記:進行GC Roots Tracing的過程,可以和使用者執行緒一起工作。
3、重新標記:用於修正併發標記期間由於使用者程式繼續執行而導致標記產生變動的那部分記錄,這個過程會暫停所有執行緒,但其停頓時間遠比並發標記的時間短;
4、併發清理:可以和使用者執行緒一起工作。
CMS收集器的缺點:
1、對CPU資源比較敏感,在併發階段,雖然不會導致使用者執行緒停頓,但是會佔用一部分執行緒資源,降低系統的總吞吐量。
2、無法處理浮動垃圾,在併發清理階段,使用者執行緒的執行依然會產生新的垃圾物件,這部分垃圾只能在下一次GC時收集。
3、CMS是基於標記-清除演算法實現的,意味著收集結束後會造成大量的記憶體碎片,可能導致出現老年代剩餘空間很大,卻無法找到足夠大的連續空間分配當前物件,不得不提前觸發一次Full GC。
JDK1.5實現中,當老年代空間使用率達到68%時,就會觸發CMS收集器,如果應用中老年代增長不是太快,可以通過-XX:CMSInitiatingOccupancyFraction引數提高觸發百分比,從而降低記憶體回收次數提高系統效能。
JDK1.6實現中,觸發CMS收集器的閾值已經提升到92%,要是CMS執行期間預留的記憶體無法滿足使用者執行緒需要,會出現一次”Concurrent Mode Failure”失敗,這是虛擬機器會啟動Serial Old收集器對老年代進行垃圾收集,當然,這樣應用的停頓時間就更長了,所以這個閾值也不能設定的太高,如果導致了”Concurrent Mode Failure”失敗,反而會降低效能,至於如何設定這個閾值,還得長時間的對老年代空間的使用情況進行監控。
7、G1收集器
G1(Garbage First)是JDK1.7提供的一個工作在新生代和老年代的收集器,基於“標記-整理”演算法實現,在收集結束後可以避免記憶體碎片問題。
G1優點:
1、並行與併發:充分利用多CPU來縮短Stop The World的停頓時間;
2、分代收集:不需要其他收集配合就可以管理整個Java堆,採用不同的方式處理新建的物件、已經存活一段時間和經歷過多次GC的物件獲取更好的收集效果;
3、空間整合:與CMS的”標記-清除”演算法不同,G1在執行期間不會產生記憶體空間碎片,有利於應用的長時間執行,且分配大物件時,不會導致由於無法申請到足夠大的連續記憶體而提前觸發一次Full GC;
4、停頓預測:G1中可以建立可預測的停頓時間模型,能讓使用者明確指定在M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒。
使用G1收集器時,Java堆的記憶體佈局與其他收集器有很大區別,整個Java堆會被劃分為多個大小相等的獨立區域Region,新生代和老年代不再是物理隔離了,都是一部分Region(不需要連續)的集合。G1會跟蹤各個Region的垃圾收集情況(回收空間大小和回收消耗的時間),維護一個優先列表,根據允許的收集時間,優先回收價值最大的Region,避免在整個Java堆上進行全區域的垃圾回收,確保了G1收集器可以在有限的時間內儘可能收集更多的垃圾。
不過問題來了:使用G1收集器,一個物件分配在某個Region中,可以和Java堆上任意的物件有引用關係,那麼如何判定一個物件是否存活,是否需要掃描整個Java堆?其實這個問題在之前收集器中也存在,如果回收新生代的物件時,不得不同時掃描老年代的話,會大大降低Minor GC的效率。
針對這種情況,虛擬機器提供了一個解決方案:G1收集器中Region之間的物件引用關係和其他收集器中新生代與老年代之間的物件引用關係被儲存在Remenbered Set資料結構中,用來避免全堆掃描。G1中每個Region都有一個對應的Remenbered Set,當虛擬機器發現程式對Reference型別的資料進行寫操作時,會產生一個Write Barrier暫時中斷寫操作,檢查Reference引用的物件是否處於相同的Region中,如果不是,則通過CardTable把相關引用資訊記錄到被引用物件所屬Region的Remenbered Set中。
相關文章
- Java GC 的那些事(2)JavaGC
- Java GC 的那些事(1)JavaGC
- Java 混淆那些事(六):Android 混淆的那些瑣事JavaAndroid
- Java泛型的那些事Java泛型
- JAVA執行緒的那些事?Java執行緒
- 雲原生java的那些事兒Java
- Java字串那些事兒Java字串
- Java中的陣列 - Java那些事兒Java陣列
- 談談 Java 中的那些“瑣”事Java
- Java最困擾你的那些事Java
- Java執行緒池的那些事Java執行緒
- Java反射機制那些事Java反射
- 聊聊java就業那些事Java就業
- Java安全——金鑰那些事Java
- JAVA架構師那些事?Java架構
- 讓人疑惑的Java程式碼 – Java那些事兒Java
- 說說Java裡的equals(中)- Java那些事兒Java
- 讓人疑惑的Java程式碼 - Java那些事兒Java
- GC那些事兒–Android記憶體優化第一彈GCAndroid記憶體優化
- GC那些事兒--Android記憶體優化第一彈GCAndroid記憶體優化
- Oauth2協議那些事OAuth協議
- Java 資料庫連線的那些事Java資料庫
- 談談java入門的那些事兒Java
- 物件導向 - Java那些事兒物件Java
- Java日誌框架那些事兒Java框架
- Java 混淆那些事(五):ProGuard 其他的選項Java
- java高併發之ConcurrentSkipListMap的那些事Java
- 我和Linux那些事(2)-----CentOSLinuxCentOS
- ArrayList初始化 – Java那些事兒Java
- ArrayList初始化 - Java那些事兒Java
- Synchronized的那些事synchronized
- webassembly 的那些事Web
- ViewPager的那些事Viewpager
- Java基礎7:關於Java類和包的那些事Java
- Java long型別和Long型別的那些事Java型別
- Java自動裝箱/拆箱 - Java那些事兒Java
- MVP那些事兒 (2) 初探MVC架構MVPMVC架構
- Java 混淆那些事(一):重新認識 ProGuardJava