本文主要參考《深入理解java虛擬機》進行簡化總結,希望能夠讓讀者快速入門jvm垃圾回收相關機制。
1.為什么了解GC
需要排查內存泄露、內存溢出問題,當垃圾收集成為系統達到高并發的瓶頸時。
2.GC需要完成3件事
- 哪些內存需要回收
- 什么時候回收
- 如何回收
3.哪些內存需要回收
程序計數器、虛擬機棧和本地方法棧三個方法區域隨線程而生,隨線程而滅,這部分不需要關心。java堆和方法區,只有在程序處于運行期間才能知道創建哪些對象,這部分內存和回收都是動態的,是需要關心的。
4.什么時候回收
垃圾收集器對堆進行回收前,需要確定這些對象有哪些存活,哪些死去。常用的判斷對象是否存活的方法:
4.1引用計數法
對象添加一個引用計數器,每個地方引用+1,失效-1。主要問題,對象相互循環引用,所以不被采用。
4.2根搜索算法
通過一系列名為“GCRoots”的對象為起點,從起點開始向下搜索,搜索走過的路徑稱為“引用鏈”,當一個對象到GC Roots沒有任何引用鏈,證明該對象已經死亡。以下可以作為GC Roots
- 虛擬機棧中的引用對象
- 方法區中的類靜態屬性引用對象
- 方法區中常量引用對象
- 本地方法中引用對象
4.3兩次標記
在經歷第一次跟搜索標記以后,將進行一次篩選,條件是對象是否覆蓋finalize()方法(并且從未被調用過),這些需要被執行finalize方法的對象,會被放置到一個名為F-Queue的隊列中,并在稍后由一條虛擬機自動建立的低優先級線程執行,該執行只是觸發,并不承諾等待運行結束,finalize方法是對象逃脫死亡最后機會,可以在該方法中重新引用。這種自救只能使用一次,因為finalize方法只會被虛擬機執行一次。建議不要使用finalize方法,運行代價高昂,不確定性很大,finally可以代替。
4.4方法區回收判斷
HotSpot虛擬機中的永久代,按照jvm規范方法區是可以不回收的,通常回收效率很低。主要收集兩部分內容:廢棄常量和無用類。判斷廢棄常量和堆中對象類似,但是無用類就非常嚴格了:
- 該類所有實例都已經被回收
- 加載該類的ClassLoader已經被回收
- 該類對應的Class對象沒有任何地方在引用,無法用其進行反射方位該類。
滿足上述所有條件,同時HotSpot中配置了相關參數才會回收。在大量使用反射、動態代理以及CGLIb等bytecode框架的場景,以及動態生成JSP和自定義ClassLodaer場景需要卸載功能,來保證永久代不會溢出。
5.如何回收
垃圾收集在不同平臺的虛擬機操作內存的方法各不相同,簡單介紹幾種回收思想:
5.1標記-清除算法
分為標記和清除兩個過程,標記利用前面敘述的根搜索算法標記出所有需要清除的對象。作為基礎算法該算法有以下幾個缺點:
- 效率不高
- 清除之后會產生大量不連續的內存碎片,可能導致后續分配大內存的對象找不到合適空間。
5.2復制算法
將可用容量等分成兩塊,每次只使用其中一塊,當一塊內存用完了。計算該內存中存活對象并將其復制到另一塊內存,將該內存全部清空。商業虛擬機采用該算法回收新生代,新生代中的對象98%朝生夕死(所以不需要分配1:1),將內存分為一塊比較大的Eden空間和兩塊較小的Survivor空間。
每次使用Eden和一塊Survivor空間,回收的時候,將存活的對象一次性復制到另一塊Survivor空間。默認情況下,Eden和Survivor大小是8:1,當然我們沒法保證每次回收只有10%的對象存活,當Survivor空間不夠用時,需要分配擔保機制直接進入老年代。
5.3標記整理算法
復制算法在對象存活率較高的時候執行較多的復制,效率會變得很低,這時提出了標記整理算法。主要針對老年代,整理過程將所有存活對象都向一端移動,然后直接清理掉邊界以外所有內存。
5.4分代收集
實際商業虛擬機會根據對象存活周期不同將內存劃分為幾塊,一般是分為新生代和老年代,根據不同代特性選擇不同的算法進行垃圾收集。
6.垃圾回收器
上述算法只是內存回收的方法論,而垃圾收集器則是具體實現。一般廠商會提供參數供用戶根據自己應用特點和要求組合出各個年代要使用的收集器。首先是3款常見新生代收集器:
6.1Serial收集器
jdk1.3之前新生代收集唯一選擇,單線程收集器在進行垃圾收集的時候需要暫停所有其他工作。(Client模式下默認新生代收集器)
6.2ParNew收集器
Serial收集器的多線程版本,參數配置、收集算法以及策略等都和Serial完全相同。Server模式下選擇它的重要理由是他是除了Serial以外唯一和CMS(老年代收集器)可以配合的收集器。在多核情況下,ParNew才會顯得比Serial高效。特別指出這里的并行是指多條垃圾收集線程并行工作,用戶線程還是要處于等待的。
6.3Parallel Scavenge收集器
作為新生代收集器,使用復制算法的收集器,其關注點是達到一個可控制吞吐量,所謂吞吐量=運行代碼時間/(運行用戶代碼時間+垃圾收集時間),假設虛擬機總共運行100分鐘,垃圾回收用掉1分鐘,則吞吐量就是99%。
一般收集器設計會關注兩個點:停頓時間(如CMS)和吞吐量(如Parallel Scavenge)。前者常用于用戶交互頻繁的程序,較短的停頓帶來良好的用戶體驗;后者則適合后臺運算不需要太多的交互行為,高效利用cpu。Parallel Scavenge收集器也被經常稱為“吞吐量優先”收集器。接下來介紹3款老生代垃圾回收器:
6.4Serial Old收集器
與Serial收集器類似,單線程收集器,使用“標記—整理算法”,Client模式下默認老生代收集器。
6.5Parallel Old收集器
Parallel Scavenge收集器的老年代版本,使用多線程和”標記—整理算法“,jdk1.6開始提供用來配合Parallel Scavenge收集器。在此之前,Parallel Scavenge收集器只能和Serial Old收集器配合使用,性能被后者拖累。
6.6CMS收集器
以獲取最短時間停頓為目標的收集器,目前web服務端大部分都在使用為了給用戶較好的體驗,其是基于“標記——整理”算法實現。目前,很大一部分java應用集中在web的服務端,該類服務重視響應速度,希望停頓時間越短越好,而CMS非常符合該類需求。CMS收集器運作過程分為以下4個步驟:
- 初始化標記(標記GC Roots直接關聯的對象)
- 并發標記(GC Roots的Tracing過程)
- 重新標記(修正并發標記期間,用戶運行程序導致標記的改動)
- 并發清除
其優點主要表現為并發收集和低停頓,當然它自身也有明顯缺點:
- 并發階段會占用CPU資源導致應用程序變慢,特別是負載高的情況下會導致用戶的執行速度突然降低
- 無法處理浮動垃圾,清理階段用戶程序還在運行產生新的垃圾,因此CMS通常不能再老年代幾乎填滿的時候再收集。默認情況下,CMS在老年代使用68%的情況就會被激活,可以通過參數設置。如果在CMS運行期間預留內存不足,就會出現”Concurrent Mode Failure“失敗。
- 使用標記—整理算法,在收集結束會產生大量空間碎片,為了解決該問題,CMS提供了”+XX:UseCMSCompactAtFullCollection“開關參數用于,在完成垃圾回收后進行一次碎片整理過程,同時還提供參數”-XX:CMSFullGCsBeforeCompaction“用來控制多少次FullGC以后啟動該次碎片整理,畢竟碎片整理會延長停頓時間。
6.7G1收集器
Jdk7+版本都可以自主配置G1作為JVM GC選項,作為垃圾收集器理論進一步發展的結果,不同于其他的分代回收算法、G1將堆空間劃分成了互相獨立的區塊。G1將整個java堆包括新生代和老生代劃分為多個固定大小獨立區域,并且跟蹤這些區域里面垃圾堆積程度,在后臺維護一個優先級列表,在規定的允許的時間內,優先收集垃圾最多的區域。與CMS相比其優勢在于:
- 空間劃分為多個區域避免了空間碎片化
- 精確控制停頓時間到毫秒級
參考文檔
《深入理解java虛擬機》——周志明
http://ifeve.com/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3g1%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8/