java中的垃圾回收機制簡介
記憶體空間是有限的,執行時如果不能獲取到記憶體,會丟擲 OutOfMemory
,一種有效的解決措施是,拋棄那些程式永遠不會不再用到的物件,騰出空間。
如何定義物件不會用到
-
給物件新增一個引用計數器,每當這個物件被引用一次就加1,每當這個物件的引用失效1次,就減1,那麼引用次數為0的就沒有再用了,非0就代表還有用,但是引用計數器很難解決迴圈引用。
新建物件的引用計數為1,如果兩個新建物件互相引用,那麼他們的引用計數為2,此時如果只將原新物件置為null,只會各自使得引用計數減1,這種場景下得到的結果引用結果是1,因而僅靠這種粗略的檢查並不能達到一個好的效果
-
給物件的引用做追蹤。可以定義一組集合,認定從這個集合出發,能夠追溯到的所有物件,都是可用的,其餘的都是不可用的
這種集合也稱作GC Roots,它定義一組根引用,包括當前所有正在被呼叫的方法的引用型別引數、區域性變數、臨時值;方法區中的常量引用物件;本地方法棧中的JNI等等
可引用物件的細分
幹掉沒有引用的物件,沒什麼問題,但是如果記憶體空間仍然不夠,可以幹掉部分雖然可用,但是不那麼重要的物件來“確保大局”,java對此細分了強引用、軟引用、弱引用、虛引用
詳見 reference 引用
常用的垃圾回收演算法思想
- 標記-清除。首先標記出需要清除的物件,然後再統一清除。
- 複製演算法。將一塊記憶體分為兩半,每次只用一半,當對使用的這塊記憶體回收時,將可以用的物件複製到另一塊記憶體上,然後一次性清除所有使用過的記憶體空間
- 標記-整理。思想與標記-清除一致,只是在清除之前,會先把所有存活的物件都移向一端,然後清掉端邊界外的記憶體
- 分代思想。根據物件的存活週期將記憶體劃分成幾塊,對不同的存活時間使用不同的垃圾回收演算法
分代GC帶來的好處
- 大多數情況下,資料都會滿足這麼一個假設:大部分物件的存活時間很短,而其它的物件則有可能存活時間很長。在這一假設前提下劃分為年輕代和老年代。配置年輕代佔據堆中較小的一塊,新建立的物件都在年輕代裡面,GC時,由於大部分物件都會消亡,只會留下較小的部分,這樣適合使用複製演算法的思想來處理,這樣的劃分便降低了單次GC的時間長度(遍歷的空間少),同時提高了GC的效率(回收的多)。hotspot中年輕代被劃分成8:1:1,這裡其實就是認為90%的物件都會被回收,10%用來保留活下來的,這也就意味著複製演算法每次只有10%的空間浪費
- 為了更快的釋放空間,一邊能處理應用記憶體的分配。 併發GC的本質是GC一邊蒐集,應用一邊產生,如果GC的速度跟不上產生的速度,那麼垃圾就會元件堆積,最終應用分配請求只能停下來等GC追趕,因此越快釋放出越多的空間,就能越好應付越高的應用分配記憶體的速率,從而讓GC以完美的併發模式工作。
什麼時候可以回收
要做回收,首先得知道哪些物件是可達的(存活的),而要知道可達性,對於物件引用追蹤這種思想,就得要去遍歷整個GC根集合。而要做到精準的列舉,就需要知道哪些棧的槽位有引用,哪些暫存器有引用,因而需要有一些位置去儲存這些資訊,而能夠儲存這些資訊的地方即安全點或者安全區域。
能夠儲存這些資訊的地方必定也是知道引用情況的地方,這些地方也就可以執行GC
hotspot中的垃圾收集器
無論使用哪種收集器,在收集開始的時候都是從 safepoint開始
serial年輕代收集器
"古老"的收集器,使用單執行緒收集,它工作時必須暫停所有使用者的執行緒,直到收集結束。對於年輕代的收集則使用複製演算法。 可以用於Client模式下的虛擬機器。
ParNew年輕代收集器
serial的多執行緒版本。多執行緒收集,它工作時必須暫停所有使用者的執行緒,直到收集結束。對於年輕代的收集則使用複製演算法。 與CMS收集器配合工作,使用 -XX:UseConcMarkSweepGC
的預設年輕代收集器
Parallel Scavenge年輕代收集器
多執行緒收集器。它的目標是提供一個可控的吞吐量:
與縮短停頓時間的收集器相比,它的目標是高效率的利用CPU的時間,儘快完成運算任務。另外它還支援自適應調節:比如年輕代大小、Eden和Survior的比例、晉升老年代的大小等,來達到最佳的吞吐量。適合後臺運算而不需要太多互動的任務,不能配合CMS工作
縮短停頓時間的關注點則是在於提供良好的響應速度,從而提升使用者體驗
Serial老年代收集器
單執行緒收集。它需要暫停所有使用者執行緒,直到收集結束。年老代使用標記-整理演算法。它同樣適用於Client模式下的虛擬機器
如果是Server模式,在JDK1.5以及之前可以用來配合Parallel Scavenge搭配使用,以及作為CMS收集器的預備方案,在發生Concurrent Mode Failure時使用
Parallel 老年代收集器
多執行緒收集。它需要暫定所有使用者執行緒,直到收集結束。使用標記整理演算法,它也是以吞吐量優先,在JDK1.6中提供,用來配合Parallel Scavenge使用
CMS(Concurrent Mark Sweep) 老年代收集器
併發收集。它分為4個階段:
- 初始標記:需要暫停使用者執行緒。用於標記GC Roots能直接關聯到的物件
- 併發標記:不需暫停使用者執行緒。用於標記所有活著的物件
- 重新標記:需要暫停使用者執行緒。修正因為使用者執行緒執行而導致的標記變動的物件
- 併發清除:不需暫停使用者執行緒。清除消亡的物件
缺點:
- 併發意味著它會佔用CPU資源,吞吐量就低;
- 由於清理的時候使用者執行緒還在執行,那麼使用者執行緒也是需要空間的,如果空間不夠,就產生
Concurrent Mode Failure
,轉而使用Serial Old
,另外使用者執行也會不斷的產生垃圾,這部分無法清除(浮動垃圾),因此有設定CMS觸發的引數 -XX:CMSInitiatingOccupancyFraction
- 標記-清除之後會產生碎片
可以通過 -XX:CMSCompactAtFullCollection設定是否要清理碎片,以及 -XX:CMSFullGCsBeforeCompaction來表示多此次執行不壓縮的Full GC後來一次壓縮
G1(Garbage-First)收集器
新生代和老年代都可以收集。大致步驟如下:
- 初始標記:需要暫停使用者執行緒。用於標記GC Roots能直接關聯到的物件,以及實現G1演算法相關的操作
- 併發標記:不許需要暫停使用者執行緒。用於標記活著的物件
- 最終標記:需要停頓並行執行。用於修正因使用者執行而導致的標記變更
- 篩選回收:不需要暫停,併發執行。根據G1演算法的細節進行回收價值和成本的排序,生成執行計劃
與CMS相比優點:
- 空間整合:G1從整體上來講是基於標記-整理演算法,從區域性來講是基於“複製”演算法實現,這意味著執行期間不會產生記憶體空間碎片
- 可預測的停頓:使用者可以指定在長度為M毫秒的時間片段內,消耗時間不超過N毫秒
JDK 7 引入
ZGC
併發垃圾收集器。幾乎所有的階段都是併發執行
ZGC仍然會壓縮堆,壓縮堆這件事,通常意味著
- 將或者的物件移到堆的一端
- 執行移動過程中需要暫停應用執行緒
壓縮主要會遇到這麼些問題
- 在搬運物件到另一個記憶體地址的時候,另一個執行緒也同時會對物件進行讀和寫
- 搬運成功後,其它有這個物件引用的也必須去跟新他們的引用地址
JDK 11引入
垃圾收集器引數
- UseParNewGC:使用ParNew+Serial Old收集器
- UseConcMarkSweepGC:使用ParNew+CMS+Serial Old收集器
- UseParallelGC:使用Parallel Scavenge+Serial Old
- UseParallelOldGC:使用Parallel Scavenge+Parallel Old
執行調節的引數
- SurvivorRatio:Eden與Survivor的分配比例
- Newratio:年輕代和年老代的比值,比如4表示young:old=1:4
- PretenureSizeThreshold:直接晉升到老年代的物件大小
- MaxTenuringThreshold:晉升到老年代的年齡
- ParallelGCThreads:並行GC記憶體回收的執行緒數
- CMSCompactAtFullCollection:執行CMS後是否需要碎片整理
- CMSFullGCsBeforeCompaction:執行CMS若干次後再進行碎片整理
- CMSInitiatingOccupancyFraction:使用CMS,老年代空間使用多少後觸發GC,預設68%
專業名詞
Partial GC:不收集整個GC堆
- young GC:只收集年輕代
- Old GC:只收集年老代,限CMS的並行收集
- Mixed GC:收集年輕代和年老代,限G1
Full GC:收集整個堆,包括年輕代,年老代,永久帶(如果有的話)
Minor GC一般指的是young GC;Major GC通常和Full GC等價,另外由於名詞混用,也可能指的是Old GC
觸發young gc的時候,如果發現之前young GC的平均大小比目前老年代的剩餘空間大,則觸發Full GC,永久帶如果沒有足夠的空間,也會觸發Full GC
注意: ParallelScavenge 則是在每次觸發Full GC之前會先執行一次young gc,再執行full gc;
GC 檔案
使用jstat -gc pid time_interval count格式能夠檢視Java堆狀況
- gcutil可以用來查詢百分比
- gcnew/gcold分別檢視年輕代和年老代的GC
結果如下
S0CS1CS0US1UECEUOCOUMCMUCCSCCCSUYGCYGCTFGCFGCTGCT 16960.0 16960.0 5116.00.0136064.0 93854.9339724.0271888.9152936.0 149578.7 20444.0 19683.32202.122190.9233.045 複製程式碼
- S0C、S1C、S0U、S1U:Survivor 0/1區容量(Capacity)和使用量(Used)
- EC、EU:Eden區容量和使用量
- OC、OU:年老代容量和使用量
- PC、PU:永久代容量和使用量
- YGC、YGT:年輕代GC次數和GC耗時
- FGC、FGCT:Full GC次數和Full GC耗時
- GCT:GC總耗時
使用-XX:+PrintGCDetails可以顯示GC的情況,形如
[GC[ParNew: 6996K->1202K(78656K), 0.0036460 secs][CMS: 0K->1163K(174784K), 0.0311840 secs] 6996K->1163K(253440K), [CMS Perm : 3060K->3059K(21248K)], 0.0349020 secs] [Times: user=0.03 sys=0.02, real=0.03 secs] 複製程式碼
- [GC 表明停頓的型別,有Full則表明發生了Full GC
- [ParNew 表示垃圾收集的區域與對應的GC收集器 ;其中 [ParNew 表明用的是ParNew收集器; [DefNew 表明使用的是Serial收集器;[PSYoungGen 表明使用的是 Parallel Scavenge收集器;[CMS 這種表示CMS收集器
- 6996K->1202K(78656K) 表示“GC前該區域已使用容量->GC後該區域已使用容量(該記憶體區域的總容量)”
- 0.0036460 secs 表示該記憶體區域GC所用的時間
- 6996K->1163K(253440K) (
方括號外
)的表示"GC前java堆已使用的容量->GC後Java堆使用的容量(Java堆總容量)" - [Times: user=0.03 sys=0.02, real=0.03 secs] 分別表示使用者態消耗的CPU時間、核心態的CPU時間和操作從開始到結束所經過的強鍾時間
牆鍾時間包含各種非運算的等待耗時,例如等待磁碟IO,CPU時間則不包含這些,但是多執行緒會疊加CPU的時間