GC
Java中一個接口的多個實現類所需要的內存可能不一樣,一個方法中的多個分支需要的內存也可能不一樣,我們只有在程序處于運行期間時才會知道創建了哪些對象,這部分內存的分配時動態的,而程序計數器、虛擬機棧、本地方法棧這幾個區域內存的分配和回收都是確定的,因此垃圾收集器關注的就是Java堆。
判斷對象是否存活
引用計數法
給對象中添加一個引用計數器,每當有一個地方引用它時計數器就加1;當引用失效時,計數器就減1;任何時刻計數器為0的對象就是不可能再被使用的。
引用計數法的優點是實現簡單,判定效率高,COM(Component Object Model)、Python使用它進行內存管理。
缺點是它很難解決對象之間的相互循環引用問題
相互循環引用
假設對象A、B都有一個成員變量object
A a=new A();
B b=new B();
a.object=B;
b.object=A;
a=null;
b=null;
實際上這兩個對象都不可能再被訪問,但是因為它們互相引用導致引用計數不為0無法被回收。為了解決這個問題,Java采用下面的可達性分析算法
可達性分析算法
可達性分析(Reachability Analysis),基本思想(本質是圖論)是通過一系列被稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈,當GC Roots 到一個對象不可達時,證明此對象不可用。
可作為GC Roots的對象包括
- 虛擬機棧中(棧幀中的本地變量表)引用的對象
- 方法區中類靜態屬性引用的對象
- 方法區中常量引用的對象
- 本地方法棧中JNI(Java Native Interface)引用的對象
引用
如果reference類型的數據中存儲的數值代表的是另外一塊內存的起始地址,就稱這塊內存代表著一個引用
這個定義過于簡單以至于沒有實用價值。
JDK1.2之后對引用的概念進行了擴充
- 強引用:在程序代碼中普遍存在的類似“Object obj = new Object()”這類的引用,只要強引用還在,垃圾回收器永遠不會回收掉被引用的對象。
- 軟引用 :描述還有用但并非必須的對象。僅在系統內存不足時進行清理。對應的類是SoftReference
- 弱引用:描述非必需對象。無論內存是否足夠都會被清理。對應的類時WeakReference
- 虛引用:最弱的引用關系,一個對象是否有虛引用的存在完全不會對其生存時間造成影響,也無法通過虛引用來取得一個對象實例,為對象設置虛引用的唯一目的是能在這個對象被收集器回收時收到一個系統通知。對應的類是PhantomReference
引用強度:強引用》軟引用》弱引用》虛引用
finalize-不可達對象逃脫回收的辦法
一個對象真正被回收,至少需要經過兩次標記
- 對經過可達性分析無法到達的對象進行第一次標記并篩選,如果對象覆蓋了finalize方法且finalize方法沒有被虛擬機調用過,有必要執行finalize方法,否則沒有必要執行finalize方法,直接清理
- 如果對象被判定為有必要執行finalize方法,這個對象會被放置到一個稱為F-Query的隊列中,稍后由虛擬機創建的、低優先級的Finalizer會去執行(為了安全,會觸發這個對象的finalize方法但不承諾會等待它運行結束)它,如果在finalize方法中對象重新與引用鏈上的任何一個對象建立聯系,在第二次標記中就會被移除“即將回收”的集合,否則就真的被回收。
回收方法區
方法區的回收效率低、條件苛刻。因此一般回收較少。回收方法區(或者說永久代)在大量使用反射、動態代理、CGLib等字節碼框架、動態生成JSP及OSGi這類頻繁自定義ClassLoader的場景都需要虛擬機具備類卸載的功能,以保證永久代不會溢出。
回收條件
- 該類的所有實例都已經被回收(Java堆中不存在該類的任何實例)
- 加載該類的ClassLoader已經被回收
- 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
垃圾收集算法
主要有
- 標記-清除算法
- 復制算法
- 標記-整理算法
標記-清除算法
標記需要回收的對象,標記完成后統一清除。
不足是
- 效率不高
- 內存碎片
復制算法
將內存分成大小相同的兩塊,每次使用一塊,清理時將存活的對象復制到另一塊,再將已使用過的內存空間清理掉。解決了標記-清除算法的缺陷,但是內存縮小為一半,代價太高。根據研究,不需要分為1:1,將新生代內存分為一塊較大的Eden和兩塊較小的Survivor,Eden:Survivor=8:1,內存利用率90%。每次使用Eden和其中一塊Servivor,清理時將Eden和Survivor中存活對象轉移到另一塊Survivor中。
標記-整理算法
標記后將存活對象向一端移動,直接清理掉端邊界之外的內存,解決了內存碎片問題。
分代收集算法
根據對象存活周期的不同將內存劃分為幾塊,一般分為新生代和老年代。針對新生代特掉采用復制算法,針對老年代采用標記-清理或標記-整理算法。
HotSpot的實現
Stop The World
可達性分析必須在一個能確保一致性的快照中進行,也就是說至少在枚舉根節點(GC Roots)時,必須停頓所有Java執行線程,因此被稱為“Stop The World”
標記的實現細節
OopMap
虛擬機通過OopMap直接得知哪些地方存存放著對象引用
安全點
OopMap只在安全點生成,程序執行時只有到達安全點才能GC。安全點的選定標準時一程序“是否具有將程序長時間執行的特征”為標準選定。最明顯的特征就是指令序列復用,如方法調用、循環跳轉、異常跳轉等。
發生GC時讓所有線程(不包括執行JNI調用的線程)“跑到”最近的安全點再停頓下來,有兩種方案
- 搶先式中斷
- 主動式中斷
目前的主流時主動式中斷
安全區域
在一段代碼片段中,引用關系不會發生變化,在這個區域中的任意地方開始GC都是安全的。可以看作拓展的安全點
垃圾收集器
到目前為止沒有最好的收集器,更沒有萬能的收集器,我們所選擇的是對具體應用最合適的收集器。
下圖上方是工作在新生代的收集器,下方是工作在老年代的收集器,連線表示兩者可以協同工作。G1中并沒有對新生代和老年代做過多區分,可以獨立工作。
新生代收集器
Serial
復制算法 單線程收集器,最古老,依然是虛擬機運行在Client模式下的默認新生代收集器,工作時Stop The World,停頓時間較長。優點是簡單而高效(沒有線程交互的開銷,專心做垃圾收集)
ParNew
復制算法 Serial的多線程版本,與Serial基本相同,但只有它才能和CMS收集齊協同工作,是運行在Server模式下的蓄奴及首選的新生代收集器
Parallel Scavenge
復制算法 新生代收集器,關注點是達到一個可控制的吞吐量(吞吐量=運行用戶代碼時間/(運行用戶代碼時間+GC時間)),主要適合在后臺運算而不需要太多交互的任務
老年代收集器
Serial Old
標記-整理算法 Serial Old 是Serial收集器的老年代版本。單線程,使用“標記-整理”算法,給Client模式下的虛擬機使用。
兩大用途是
- JDK1.5及之前的版本與 Parallel Scavenge配合使用
- 作為CMS收集器的后備預案
Parallel Old
標記-整理算法 Parallel Old (JDK1.6)是Parallel Scavenge收集器的老年代版本。使用多線程和標記-整理算法,在此之前,如果新生代使用Parallel Scavenge,那么老年代只能使用Serial Old。Parallel Old 無法與CMS配合工作
CMS
CMS(Concurrent Mark Sweep),并發收集,低停頓,以獲取最短回收停頓時間為目標的收集器。適合Java Web應用。
CMS基于“標記-清除”算法,具體步驟為
- 初始標記(STW,標記GC Roots能關聯到的對象,速度快)
- 并發標記(與用戶進程并發,進行GC Roots Tracing)
- 重新標記(GC線程并行,STW,修正并發標記階段變動的對象記錄)
- 并發清除(與用戶進程并發,清理)
耗費時間:初始標記《重新標記《《并發標記~并發清理
缺點:
- CPU資源敏感,默認啟動的線程數為(CPU數量+3)/4,為此有了增量式并發收集器(i-CMS),但是效果并不好,deprecated
- 無法處理浮動垃圾(并發清理階段用戶線程產生的垃圾,只能留到下次清理),為了給用戶線程留出運行空間,CMS當老年代使用了68%就會被激活,JDK1.6中啟動閾值提升到92%。如果CMS運行期預留內存無法滿足程序需要,發生“Concurrent Failure”,虛擬機啟動后備預案-臨時啟用Serial Old。
- 空間碎片,因為CMS式基于“標記-清理”。分配大對象時會觸發一次[^Full GC],可通過設置開啟空間整理選項
[^Full GC]:老年代GC,也稱為 Major GC.對應的新生代GC 稱為Minor GC
G1
G1(Garbage First)收集器技術發展的最前沿成果之一。
特點
- 并行與并發
- 分代收集
- 空間整理
- 可預測的停頓,讓使用者指定在一個長度為M ms的時間片段里,消耗在GC的時間不得超過N ms。這幾乎是實時Java(RTSJ)的收集器的特征。
內存劃分
G1收集器將整個Java堆劃分為多個大小相等的獨立區域(Region),雖然還有新生代和老年代的概念,但不再是物理隔離的,他們都是一部分Region(無需連續)。
算法
G1收集器整體基于“標記-整理”算法,局部(兩個Region)基于復制算法。
可預測的停頓的實現
G1有計劃的避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region中的垃圾堆積的價值大小,并在后臺維護一個優先列表,每次根據允許的時間,優先收集價值最大的Region。
如何避免全堆掃描(適用于各種收集器)
虛擬機使用Remember Set來避免全堆掃描。G1中的每個Region都有一個對應的Remember Set,虛擬機發現程序在對Reference類型的數據進行寫操作時,會產生一個Write Varrier操作,檢查Reference引用的對象是否處于不同的Region中,如果是便通過CardTable把相關引用信息記錄到被引用對象所屬Region的Remember Set中。內存回收時,在GC根節點的枚舉范圍中加入Remember Set即可保證不對全堆掃描也不會有遺漏。
運作步驟
- 初始標記(STW)
- 并發標記(與用戶線程并發執行)
- 最終標記(GC線程并行執行。將并發標記階段記錄在線程Remembered Set Logs中的記錄合并到 Remembered Set中 )
- 篩選標記(GC線程并行執行,(未來)也可以與用戶程序并發執行。根據期望停頓時間制定回收計劃)
GC日志
33.125: [GC [DefNew: 3324k->152k](3712k),0.0025925 secs] 3323k->152k(11904k),0.0031680 secs]
模式為
GC發生時間:[垃圾回收的停頓類型: [GC發生的區域: GC前該內存區域已使用容量->GC后該內存區域已使用容量(該內存區域內存總容量),該內存區域GC所占時間] GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆總容量),Java堆GC所用時間]
垃圾回收的停頓類型
- GC
- Full GC,發生STW
- [Full GC(System)],發生STW
GC發生的區域,與收集器緊密相關
- DefNew:Default New Generation。Serial收集器新生代
- ParNew:Parallel New Generation。ParNew收集器
- PSYoungGen:Parallel Scavenge。Parallel Scavenge收集器
......
內存分配與回收策略
- 對象優先在新生代Eden區分配,空間不足時發起一次Minor GC。
- 大對象直接進入老年代
- 長期存活對象進入老年代(對象每經歷一次Minor GC,年齡加一,達到晉升閾值進入老年代)
- 在Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于等于該年齡的對象就可以直接進入老年代
- 空間分配擔保。發生Minor GC之前,虛擬機會檢查老年代最大可用的連續空間是否大于新生代所有對象總空間,如果成立,Minor GC確保是安全的。否則查看配置書否允許,如果允許查看老年代最大可用連續空間是否大于歷次晉升老年代對象的平均大小,如果是嘗試Minor GC,否則進行一次Full GC。