記憶體回收演算法基本思想:
1.枚舉根節點(GC Roots)
在記憶體回收時,我們要想辦法找出哪些對象是存活的,一般會選取一些被稱為GC Root的對象,從這些對象開始枚舉。枚舉時要求所有對象停下來,也就是大家所稱的“Stop the world”。所有的演算法實現都會將虛擬機器停下來的,否則分析結果的準確性將無法保證。當執行系統停頓下來之後,虛擬機器不需要遍曆所有的根節點和上下文去確定GC Roots,而是存在著一個OopMap的資料結構來達到這個目的。在類載入完成的時候,虛擬機器就會把什麼類的什麼位移上是什麼類型的資料計算出來。在JIT編譯的時候也會在特定位置記下在寄存器和棧中哪些位置是引用,GC在掃描時就可直接得到資訊。
2.安全點
因為程式在運行時不是所有的時候停下來都是安全的(比如運算進行到一半,資料值是一個髒資料),安全點是所有線程在”Stop the world”時到達的一個安全的點。由於堆中的對象龐大,若為每個對象都產生OopMap資料結構將佔用大量空間,所以HotSpot只在”安全點“上產生這些資料結構。同時程式並非在所有位置都可以停止,而是只能在安全點才會停止。所以”安全點“還會影響到GC的及時性。”安全點“選取時不能太多,以造成空間上的支出,也不能太少,以讓GC等待較長時間。
對於”安全點“還有另外一個需要考慮的問題是,當GC發生時如何讓所有線程都跑到最近的”安全點“,一般都兩種方案。搶先式中斷,搶先式中斷是虛擬機器將所有線程停下來,然後一一檢查是否已達安全點,若沒有到達安全點則恢複線程讓其到達最近的”安全點”,此種方式幾乎沒有虛擬機器使用。主動式中斷的思路是中斷髮生時,虛擬機器在所有的線程上設定一個標誌,線程自行檢查該標誌,然後進入“安全點”。檢查標誌的地方和安全點是重合的。
3.安全區域
上面沒有解決的問題是當一個線程處於休眠,或未分配CPU時鐘,比如sleep或blocked狀態時,他就無法走到安全點去掛起自己。對於這種情況,就需要通過安全區域來解決,安全區域是一個程式不會更改自己引用的區間,在這個地區的任何地方開始GC都是安全的,可以被認為是擴充了的安全點。當線程執行到SafeRegion的代碼時,他就會標記自己已經進入Safe Region,此時發生GC,將不會管這些線程。快要離開安全區域的時候他就會去檢查當前的狀態,如果是在GC中,那邊他就會掛起等待可以離開Safe Region的訊號,否則他就可以繼續運行。
二、垃圾收集器演算法簡介
JDK1.7版本,包含的垃圾收集處理器如下:
a
上圖是jdk1.7提供了記憶體回收行程圖,兩者有連線說明可以搭配使用,回收器還因為其適用的地區分為年青代,年老代。另外記憶體回收處理器沒有優劣之分,只有適用與不適用之分。
在介紹記憶體回收行程之前,先說明兩個概念。
並行(Parallel):指多條記憶體回收處理器產並行工作,但此時使用者線程是暫停。
並發(Concurrent):指使用者線程和垃圾收集線程同時執行(也有可能是交替執行),可能使用者線程運行在這個CPU上,而記憶體回收線程運行在另一個CPU上。
年青代的記憶體回收行程
1.Serial收集器(年老代單線程收集器)
單線程的收集器,收集時還會暫停所有背景工作執行緒,直到它收集結束。
Serial收集年青時使用複製演算法。暫停所有使用者線程
備忘:是在client模式下預設的新生代收集器,原因是在桌面圖案下管理的記憶體比較小,幾十M,單線程相比多線程回收,少了線程間互動,效率更高。所以單線程模式是一個不錯的選擇。
2.ParNew收集器(年青代多線程收集器)
ParNew是Serial的多線程版本。新生代收集採用複製演算法,收集時會暫停所有使用者線程。他是server模式下年青代的預設記憶體回收處理器,一個重要的原因是只有他能與年老代的CMS處理器配合使用。
ParNew處理器在單CPU環境下絕對不會比Serial有更好的表現。因為有線程互動的成本,但是隨著CPU數量的增加,使用還是很有好處的。
3.Parllel Scavenge收集器
Parllel Scavenge收集器也是一個並行的多線程收集器,看起來似乎同ParNew收集器無本質的區別,但是他的目標是不一樣的。他的目標是達到一個可控制的輸送量。輸送量=運行代碼時間/(運行代碼時間+記憶體回收時間)。其實高輸送量是指較高程度的利用了CPU。但不意味著其使用者體驗一定是最好的,因為單次暫停可能會非常長。他有兩個參數來控制:
MaxGCPauseMillis:期望的最大GC停頓時間,當這個時間就少,他是通過將新生代調小來減少停頓時間(收集300M比收集500M來得快,但這樣會導致垃圾收集發生得更頻率,原來10秒一次,每次停100ms,現在每5秒停一次,每次停70秒),但會降低總體的輸送量。
GCTimeRatio:配置的是使用者時間同GC時間的比值,比如19,則是指GC時間同使用者使用時間的比值是1:19。那麼允許的最大GC時間就是1/(1+19),即5%.
-XX:+UseAdaptiveSizePolicy:這個收集器另外可配置的一個參數,虛擬機器會自動根據運行時的情況來動態調整年青代,年老代的大小,以提供最合適的停頓時間或者最大的輸送量。這個被稱為GC自適應的調節策略。
也就是說只需要給虛擬機器設定輸送量目標和管理的最大堆的大小,然後就讓自適應調節策略去自動最佳化吧,這是和ParNew收集器的一個重要區別。
年老代的記憶體回收行程
1.Serial Old
Serial Old是單線程版本的老年代收集器,收集時使用“標記-整理”演算法,收集時暫停所有使用者線程。他存在的主要意義是供Client模式下的虛擬機器使用,原因同serial收集器是client模式下的預設收集器一樣。
如果是Server模式下,他還有兩個使用者,一個是和Parallel Scavenge收集器搭配使用。另一個是做為CMS收集器的後備預案,用於在CMS在Concurrent Mode Failure時使用。
2.Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程的“標記-整理”演算法。他們倆適合配套使用。由於Parallel Scavenge不能同CMS配合使用,一般就只能同Serial Old配合使用,所以jdk1.6才引入的Parallel Old收集器填補了這塊空白。
3.CMS(Concurrent Mark Sweep)並發標記清掃收集器
CMS是一種以獲得最短停頓時間為目標的收集器。由於大部分的互連網應用都傾向於向使用者提供更好的互動體驗,所以希望較短的停頓時間,以帶來更好的體驗。他的實現相較更複雜,分為如下步驟
初始標記(stop the world)
並發標記
重新標記(stop the world)
並發清除
除1,3步驟是暫停,其它步驟是並發的。初始標記主要是標記的GC Root直接關聯到的對象。並發標記則是走的GC Root Tracing的過程。重新標記是修正在並發標記期間由於使用者線程的並發運行引起的變動,耗時會較長,但遠比並發標記耗時來得短。整個過程中較耗時的並發標記和並發清除階段都是和使用者進程並發的,所以他較小的減少了停頓時間。
總體而言,CMS是並發的,低停頓收集器,但他還遠算不上完美,他有如下一些缺點
CPU敏感,會佔用系統CPU資源,並發回收時一般開啟(CPU數量+3)/4個線程,會導致總輸送量降低。
CMS不能處理浮動垃圾,可能出現”Concurrent Mode Failure”失敗而導致一次Full GC.浮動垃圾是指在回收期間新產生的垃圾,這部分垃圾只有留待下次清掃。另外由於使用者線程並發運行,他還需要記憶體來支援,所以收集器會在一個較低的使用率時就開始工作,以便有空間供記憶體回收期間程式並發時使用。使用-XX:CMSInitiatingOccupancyFraction來修改觸發時的比例,JDK1.6設定的比率是92%。如果CMS在運行期間預留的記憶體無法滿足程式使用,就會出現“Concurrent Mode Failure”, 從而會啟動備選的Serial Old來進行記憶體回收,這樣會耗更長的時間,所以一個較合適的比值會提高輸送量。
使用”標記-清掃”演算法會產生片段,此時需暫停所有使用者線程以進行整理,會消耗更多的時間。
4.G1收集器
G1是一款面向伺服器端的垃圾收集器,被視為是CMS收集器的後繼者,他具有以下特點。
並行與並發。充分利用多CPU來縮短記憶體回收的停頓時間,原本需要停頓的,仍然可以通過並發方式讓JAVA程式繼續運行
分代收集。G1不需要同其它收集器配合使用。其內部會根據對象的存活長短使用不同的回收演算法。
空間整合。整體來看是基於“標記-整理”演算法,從局部上來看是基於“複製”演算法。這種方式會減少片段的產生。
可預測的停頓。除了完成盡量減少停頓的目標以外,還建立了可預測停頓時間的模型。
後面我們來看看G1是如何做到這一切的。
G1收集器的內部記憶體而已與之前收集器有很大不同。他將整個堆劃分成多個大小相等的獨立地區(Region)。雖然還保留有新生代和老年代的概念,但是他們之間不再物理隔離,他們都是一部分Region(不需要連續)的集合。
為什麼G1能做到對垃圾停頓時間的預測,是因為他有計劃的避免進行全地區的記憶體回收。他會掃描各地區,分析記憶體回收的價值,在後台維護一個優先列表,只有有較高回收價值的地區才啟動回收,這也是Garbage-First名字的由來。
整體上來說,G1的思路就是化整為零。但是其實現具有較大的複雜性,因為Region間是互相引用的,回收一個Region需要遍曆到其它Region的內容。
在G1收集器中Region中使用Remebered Set來避免全堆掃描的。當虛擬機器發現對引用型資料進行寫操作時,會產生一個Write Barrier暫時中斷寫操作,檢查是否引用到不同的Region,如果是則使用CardTable把相關的資訊記錄被引用對象所屬的Region的Rember Set中。當進行記憶體回收時,GC Root的枚舉範圍增加Rember Set就不會有遺漏。如果不記對Remembered Set的維護操作,收集步驟大概分為如下幾步:
初始標記
並發標記
最終標記
篩選回收
初始標記
此階段只是標記GC Roots關聯到的對象,並且修改TAMS(Next Top at Mark Start)的值 。讓下一階段並發的使用者能在正確的Region上建立新對象,此過程需要停線程,但是耗時很短。
並發標記
遍曆GC Roots關聯對象,判斷可達性,找出存活對象,可並發。
最終標記
在並發標記階段發生的更改會記錄到Remembered Set Log中。需將這些Log資料合併到Remembered Set中。
篩選回收
此階段是評估回收價值的階段。回收時會停頓使用者線程,因為是對單個子Region回收,由於較小時間可控,停止線程會提高收集效率。後續可能可以做到並發。