《深刻理解Java虛擬機》第3章讀書筆記java
本文介紹瞭如何判斷對象是否存活,三種垃圾回收算法,分析比較了幾種垃圾收集器的特色。本文並不是原創,是《深刻理解Java虛擬機》第3章的整理、總結和補充。算法
垃圾收集器在對堆進行回收前,要先判斷哪些對象「存活」,哪些已經「死去」。json
給對象中添加一個引用計數器,每當有一個地方引用它時,計數器就加1;當引用失效時,計數器就減1;任什麼時候刻計數器爲0的對象就是不可能再被使用的。安全
主流的Java虛擬機裏面沒有選用引用計數算法來管理內存。服務器
優勢:實現簡單,效率高。微信
缺點:很難解決對象之間相互循環引用的問題。多線程
循環引用問題,以下代碼所示,併發
/** * 源代碼出自《深刻理解Java虛擬機》P62-63 * 循環引用 **/
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
/** * 這個成員屬性的惟一意義就是佔點內存,以便能在GC日誌中看清楚是否被回收過 */
private byte[] bigSize = new byte[2 * _1MB];
/** * 運行參數 * -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M */
public static void main(String[] args) {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
// 假設在這行發生GC,objA和objB是否能被回收?
System.gc();
}
}
複製代碼
JVM參數設置了新生代爲10MB,運行結果顯示,在第一次觸發GC時,「5120K->576K(9216K)」回收了約4MB內存。意味着虛擬機並無由於兩個對象相互引用就不回收它們,這也側面說明了虛擬機並非經過引用計數算法來判斷對象是否存活網站
經過一系列的稱爲「GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來講,就是從GC Roots到這個對象不可達)時,則證實此對象是不可用的。spa
如圖,object五、object六、object7 爲可回收對象
主流的Java虛擬機使用可達性分析算法
在Java語言中,GC Roots包括如下幾種:
最基礎的收集算法——「標記-清除」(Mark-Sweep)算法。通常用於老年代。
算法分爲「標記」和「清除」兩個階段:
缺點:效率低,空間碎片化。
爲了解決效率問題,出現了「複製」算法(Copying)。它將內存分爲兩塊,每次只使用其中一塊,當這一塊內存用完了,就將還存活的對象複製到另外一塊上面,而後再把已使用過的內存空間一次清理掉。
缺點:內存縮小爲原來的一半。
JVM虛擬機在新生代使用這種收集算法,並非按照 1:1 的比例來劃份內存空間。而是根據新生代中的對象98%是「朝生夕死」這一特色,將內存分爲一塊較大的 Eden(80%) 空間和兩塊較小的 Survivor(10%) 空間。每次只使用 Eden 和其中一塊 Survivor,當回收時,將 Eden 和 Survivor 中還存活的對象,一次性地複製到另一塊 Survivor 空間上,最後清理掉 Eden 和剛纔用過的 Survivor 空間。
HotSpot默認Eden和Survivor的大小比例爲8:1,這樣就只有10%的內存被「浪費」。當出現超過10%的對象存活時,就會使用老年代作分配擔保,把Survivor空間放不下的對象,直接放入老年代。
在Mark-Sweep算法的基礎上作了改良,用於解決空間碎片化問題。標記-整理(Mark-Compact)算法在標記後不是簡單作清除,而是讓全部存活的對象都向一端移動,而後清理掉端邊界之外的內存。通常用於老年代。
在作可達性分析時,須要保持分析期間整個系統不會發生變化,這就致使GC進行時必須停頓全部Java執行線程(Stop The World),即便是在號稱(幾乎)不會發生停頓的CMS收集器中,枚舉根節點時也必需要停頓。
程序執行時並不是在全部地方都能停下來開始GC,只有在到達**安全點(Safepoint)**時才能暫停。Safepoint 的選定既不能太少以至於讓GC等待時間太長,也不能過於頻繁以至於過度增大運行時的負荷。因此,安全點的選定基本上是以程序「是否具備讓程序長時間執行的特徵」爲標準進行選定的,例如方法調用,循環跳轉,異常跳轉等。
如何在GC發生時讓線程都跑到最近的安全點再停頓下來?
安全區域(Safe Region)是指在一段代碼片斷中,引用關係不會發生變化。在這個區域的任何地方開始GC都是安全的。典型的安全區域好比線程處於Sleep狀態或者Blocked狀態。
在線程執行到Safe Region中的代碼時,首先標識本身已經進入了Safe Region。當要發起GC時,就不用管標識爲Safe Region狀態的線程了。當線程要離開Safe Region時,要檢查是否處於GC狀態,若是是,就要繼續等待,直到收到能夠安全離開Safe Region的信號爲止。
吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即
吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間)。
假設虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。
收集器 | 串行、並行、併發 | 新生代、老生代 | 算法 | 目標 | 使用場景 |
---|---|---|---|---|---|
Serial | 單線程,串行 | 新生代 | 複製算法 | 響應速度優先 | 單CPU環境下的Client模式 |
Serial Old | 單線程,串行 | 老年代 | 標記-整理 | 響應速度優先 | 單CPU環境下的Client模式、CMS的後 |
ParNew | 多線程,並行 | 新生代 | 複製算法 | 響應速度優先 | 多CPU環境時在Server模式下與CMS配合 |
Parallel Scanvenge | 多線程,並行 | 新生代 | 複製算法 | 吞吐量優先 | 在後臺運算而不須要太多交互的任務 |
Parallel Old | 多線程,並行 | 老年代 | 標記-整理 | 吞吐量優先 | 在後臺運算而不須要太多交互的任務 |
CMS | 併發 | 老年代 | 標記-清除 | 響應速度優先 | 集中在互聯網站或B/S系統服務端上的Java應用 |
G1 | 併發 | both | 複製算法+標記-整理 | 響應速度優先 | 面向服務端應用,未來替換CMS |
Serial收集器的多線程版本,Service模式下的首選新生代收集器,除了Serial收集器外,目前只有它能與CMS收集器配合工做。
ParNew 收集器是使用 -XX:+UseConcMarkSweepGC
選項後的默認新生代收集器,也可使用 -XX:+UseParNewGC
選項來強制指定它。
運行示意圖以下:
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。基於Mark-Sweep算法。運行過程分爲四個部分:
其中,初始標記和從新標記仍然須要 Stop The World。初始標記只是標記一下 GC Roots 能直接關聯到的對象,速度很快。併發標記就是進行 GC Roots Tracing 的過程,而從新標記階段則是爲了修正併發標記期間用戶程序繼續運做而致使標記產生變更的那一部分對象的標記記錄,這個階段的停頓時間比初始標記稍長一些,但遠比並發標記的時間短。
因爲整個過程當中耗時最長的併發標記和併發清楚過程收集器線程均可以與用戶線程一塊兒工做,因此,從整體上來講,CMS收集器的內存回收過程是與用戶線程一塊兒併發執行的。
運行示意圖以下:
CMS 收集器有以下3個缺點:
對CPU資源很是敏感
由於併發階段須要佔用一個用戶線程,若是CPU小於4個,則會致使用戶程序的執行速度降低大於25%,若是隻有2個CPU,用戶程序執行速度則會降低50%,這是讓人沒法接收的。通常來講使用CMS收集器的服務器配置至少須要4個CPU。
沒法處理浮動垃圾
在併發清理過程當中產生的垃圾稱爲「浮動垃圾」。這些垃圾只能等待下次垃圾回收。所以,CMS 收集器不能像其餘收集器那樣等到老年代幾乎被徹底填滿了再進行收集,須要預留一部分空間提供併發收集時的程序運做使用。
內存空間碎片化
CMS 收集器是基於Mark-Sweep算法,這個算法會產生內存空間碎片。CMS 收集器提供了一個 -XX:+UseCMSCompactAtFullCollection
開關參數(默認爲開啓),用於在CMS收集器頂不住要進行Full GC時開啓內存碎片的合併整理過程。內存整理的過程是沒法併發執行的,空間碎片問題沒有了,但停頓時間不得不變長。
如過同時使用了四個組合配置,這是時候就會報錯
可是好比 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
這兩個配置同時存在就不會報錯。
翻看源碼可知
有些配置項是能夠並存的。
其實,在使用 UseConcMarkSweepGC
配置的時候,虛擬機默認開啓了 UseParNewGC
因此在配置JVM時,咱們儘可能顯式配置。好比要啓用 ParNew + CMS 組合能夠配置爲
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
複製代碼
本文首發於個人我的博客 chaohang.top
做者 張小超
公衆號【超超不會飛】
轉載請註明出處
歡迎關注個人微信公衆號 【超超不會飛】,獲取第一時間的更新。