目錄
垃圾回收機制
引用計數法
可達性分析算法
垃圾回收算法
標記清除算法
復制算法
標記壓縮算法
JVM中一次完整的GC(分代收集算法)
在新生代中
在老年代中
空間分配擔保原則
對象從新生代進入老年代的幾種情況?
Young GC?和 Full GC
垃圾回收器
CMS的回收過程
G1的回收過程
垃圾就是那些不再被程序使用,但仍然占據內存空間沒有被釋放的對象。
垃圾回收機制
引用計數法
- 每個對象有一個引用計數器,記錄有多少個引用指向該對象,有一個引用則+1。當計數器歸零時,該對象被認為是垃圾。
- 這個方法存在循環引用的問題,即使AB兩個對象不再被程序其他地方使用,但如果它們之間存在相互引用,計數器永遠不會歸零,也就永遠不會被定義成垃圾?。
可達性分析算法
- 這是Java中常用的垃圾回收方式。從 Gc Root 出發可以通過引用訪問到的的對象不會被當作垃圾對象,即一個對象被 GcRoot 直接 或 間接持有,那么該對象就不會被當作垃圾對象。那些不可達的對象肯定就是垃圾了,直接清理掉即可。
- GC Root 就是一些特殊的引用。如:
- 棧中局部變量
- 方法區中靜態變量
- 本地方法棧JNI引用的對象
- 被同步鎖持有的對象
- GC Root 就是一些特殊的引用。如:
但一個對象被認為是垃圾后,不會馬上被回收,還需要進行兩次標記:
- 第一次標記:判斷當前對象是否有finalize()方法并且該方法沒有被執行過,若不存在則標記為垃圾對象,等待回收;若有的話,則進行第二次標記;?
- 第二次標記將當前對象放入F-Queue隊列,并生成一個finalize線程去執行該方法,虛擬機不保證該方法一定會被執行,這是因為如果線程執行緩慢或進入了死鎖,會導致回收系統的崩潰;如果執行了finalize方法之后仍然沒有與GC Roots有直接或者間接的引用,則該對象會被回收;
垃圾回收算法
標記清除算法
分標記和清除兩階段來進行垃圾收集工作:
- 標記:通過GC Roots可達的對象進行標記,即堆所有存活(可用)的對象進行標記
- 清除:對整個堆內存空間進行掃描,如果發現某個對象未被標記為可達對象,那么將其回收
優點:實現簡單,執行效率高
缺點:容易產生 內存碎片(可用內存分布比較分散 不連續),如果需要申請大塊連續內存可能會頻繁觸發 GC
復制算法
將內存分為兩塊,每次只是用其中一塊。首先遍歷所有對象,將可用對象復制到另一塊內存中,此時上一塊內存可視為全是垃圾,清理后將新內存塊置為當前可用。如此反復進行。
優點:解決了內存碎片的問題
缺點:需要按順序分配內存,可用內存變為原來的一半。
標記壓縮算法
和標記清除算法類似,分為兩步:?
- 標記:通過GC Roots可達的對象進行標記,即堆所有存活(可用)的對象進行標記
- 壓縮:將所有存活對象往一端空閑空間移動,按照內存地址依次排序,并更新對應引用的指針,然后清理末端內存地址以外的全部內存空間
優點:解決了標記清除的 內存碎片 問題 ,也不需要復制算法中的 內存分塊,彌補了浪費一半內存空間的缺點。
缺點:仍需要將對象進行移動,使用成本更高。執行效率略低。
JVM中一次完整的GC(分代收集算法)
在新生代中
- 一個對象剛被創建時會放到 Eden 區,Eden 區即將存滿時做一次垃圾回收(Minor GC),將當前存活的對象復制到 幸存0區(from區) ,隨后將 Eden 清空
- 當Eden 下一次存滿時,再做一次垃圾回收,先將存活對象復制到 幸存1區(to區) ,再把 Eden 和 幸存0區 所有對象進行回收,
- 當Eden 再一次存滿時,再做一次垃圾回收,將存活對象復制到 幸存0區,再把 Eden 和 幸存1區 對象進行回收。如此反復進行大概 15 次,將最終依舊存活的對象放入到老年代區域。
- 新生代工作流程與 復制算法 應用場景較為吻合,都是以復制為核心,所以會采用復制算法。
在老年代中
- 當一個對象存活時間較久會被存入到 老年代 區域。 老年代 區即將被存滿時會做一次垃圾回收,
- 所以 老年代 區域特點是存活對象多、垃圾對象少,采用標記壓縮算法時移動少、也不會產生內存碎片。所以老年代 區域可以選用 標記清除或標記壓縮算法 進行垃圾收集。
空間分配擔保原則
- JVM有一個老年代空間分配擔保機制來保證對象能夠進入老年代。?
- 如果YougGC時新生代有大量對象存活下來,而 survivor 區放不下了,這時必須轉移到老年代中,但這時發現老年代也放不下這些對象了,于是JVM有一個老年代空間分配擔保機制來保證對象能夠進入老年代。
- 在執行每次 YoungGC 之前,JVM會先檢查老年代最大可用連續空間是否大于新生代所有對象的總大小。因為在極端情況下,可能新生代 YoungGC 后,所有對象都存活下來了,而 survivor 區又放不下,那可能所有對象都要進入老年代了。
- 如果老年代的可用連續空間是大于新生代所有對象的總大小的,那就可以放心進行 YoungGC。
- 如果老年代的內存大小是小于新生代對象總大小的,那就有可能老年代空間不夠放入新生代所有存活對象。
- 這時JVM就會先檢查 -XX:HandlePromotionFailure 參數是否允許擔保失敗。
- 如果允許,就會判斷老年代最大可用連續空間是否大于歷次晉升到老年代對象的平均大小。
- 如果大于,將嘗試進行一次YoungGC,盡快這次YoungGC是有風險的。
- 如果小于,或者 -XX:HandlePromotionFailure 參數不允許擔保失敗,這時就會進行一次 Full GC。
- 如果允許,就會判斷老年代最大可用連續空間是否大于歷次晉升到老年代對象的平均大小。
- 在允許擔保失敗并嘗試進行YoungGC后,可能會出現三種情況:?
- ① YoungGC后,存活對象小于survivor大小,此時存活對象進入survivor區中?
- ② YoungGC后,存活對象大于survivor大小,但是小于老年大可用空間大小,此時直接進入老年代。?
- ③ YoungGC后,存活對象大于survivor大小,也大于老年大可用空間大小,老年代也放不下這些對象了,此時就會發生“Handle Promotion Failure”,就觸發了 Full GC。如果 Full GC后,老年代還是沒有足夠的空間,此時就會發生OOM內存溢出了。
對象從新生代進入老年代的幾種情況?
?對象年齡達到指定閾值?:默認情況下,對象的年齡達到15次GC后會被晉升到老年代。這個閾值可以通過JVM參數-XX:MaxTenuringThreshold進行設置?。
?動態年齡判斷?:如果Survivor區中相同年齡的所有對象大小總和大于Survivor空間的一半,年齡大于或等于該年齡的對象可以直接進入老年代?。
?大對象直接進入老年代?:如果對象的大小超過了設定的閾值(默認是1MB),可以直接在老年代中分配,避免在新生代中頻繁進行垃圾回收?。
?Young GC后存活對象超過Survivor區大小?:在Young GC后,如果存活的對象超過了Survivor區的大小,這些對象會被直接晉升到老年代?。
Young GC?和 Full GC
- Young GC:只收集新生代的GC。?
- Full GC: 收集整個堆,包括 新生代,老年代,永久代(在 JDK 1.8及以后,永久代被移除,換為metaspace 元空間)等所有部分的模式。?
- Young GC觸發條件:當Eden區滿時,觸發Minor GC。?
- Full GC觸發條件:?
- 通過Minor GC后 進入老年代的平均大小大于老年代的可用內存。如果發現統計數據說之前Minor GC的平均晉升大小比目前老年代剩余的空間大,則不會觸發Minor GC而是轉為觸發full GC。
- 老年代空間不夠分配新的內存(或永久代空間不足,但只是JDK1.7有的,這也是用元空間來取代永久代的原因,可以減少Full GC的頻率,減少GC負擔,提升其效率)。?
- 由Eden區、From Space區向To Space區復制時,對象大小大于To Space可用內存,則把該對象轉存到老年代,且老年代的可用內存小于該對象大小。?
- 調用System.gc時,系統建議執行Full GC,但是不必然執行。
垃圾回收器
- 垃圾回收器主要分為以下幾種:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1,主流的垃圾回收器:CMS、G1
- Serial:單線程的收集器,收集垃圾時,必須stop the world,使用復制算法。它的最大特點是在進行垃圾回收時,需要對所有正在執行的線程暫停(stop the world),對于有些應用是難以接受的,但是如果應用的實時性要求不是那么高,只要停頓的時間控制在N毫秒之內,大多數應用還是可以接受的,是client級別的默認GC方式。?
- ParNew:Serial收集器的多線程版本,也需要stop the world,復制算法。?
- Parallel Scavenge:新生代收集器,復制算法的收集器,并發的多線程收集器,目標是達到一個可控的吞吐量,和ParNew的最大區別是GC自動調節策略;虛擬機會根據系統的運行狀態收集性能監控信息,動態設置這些參數,以提供最優停頓時間和最高的吞吐量;?
- Serial Old:Serial收集器的老年代版本,單線程收集器,使用標記整理算法。?
- Parallel Old:是Parallel Scavenge收集器的老年代版本,使用多線程,標記-整理算法。?
- CMS:是一種以獲得最短回收停頓時間為目標的收集器,標記清除算法,運作過程:初始標記,并發標記,重新標記,并發清除,收集結束會產生大量空間碎片;?
- G1:標記整理算法實現,運作流程主要包括以下:初始標記,并發標記,最終標記,篩選回收。不會產生空間碎片,可以精確地控制停頓;G1將整個堆分為大小相等的多個Region(區域),G1跟蹤每個區域的垃圾大小,在后臺維護一個優先級列表,每次根據允許的收集時間,優先回收價值最大的區域,已達到在有限時間內獲取盡可能高的回收效率;
CMS的回收過程
- CMS (Concurrent Mark Sweep,并發標記清除) 收集器是以獲取最短回收停頓時間為目標的收集器(追求低停頓),它在垃圾收集時使得用戶線程和 GC 線程并發執行,因此在垃圾收集過程中用戶也不會感到明顯的卡頓。?
STW "Stop-The-World" ,指的是垃圾收集器在進行垃圾回收時,會暫停應用程序的運行,這個停頓的時間稱為停頓時間(Pause Time)。停頓時間是指垃圾回收器在執行垃圾回收時,導致應用程序無法繼續執行的時間段 - 從名字就可以知道,CMS是基于“標記-清除”算法實現的。CMS 回收過程分為以下四步:?
- a.初始標記 (CMS initial mark):主要是標記 GC Root 開始的下級(注:僅下一級)對象,這個過程會 STW,但是跟 GC Root 直接關聯的下級對象不會很多,因此這個過程其實很快。
STW(stop the world)時間不會很長、確保盡可能數據正確 - b.并發標記 (CMS concurrent mark):根據上一步的結果,繼續向下標識所有關聯的對象,直到這條鏈上的最盡頭。這個過程是多線程的,雖然耗時理論上會比較長,但是其它工作線程并不會阻塞,沒有 STW。
- c.重新標記(CMS remark):就是要再標記一次。因為第 2 步并沒有阻塞其它工作線程,其它線程在標識過程中,很有可能會產生新的垃圾。會STW。(錯標、漏標)
- d.并發清除(CMS concurrent sweep):清除階段是清理刪除掉標記階段判斷的已經死亡的對象,由于不需要移動存活對象,所以這個階段也是可以與用戶線程同時并發進行的。
- a.初始標記 (CMS initial mark):主要是標記 GC Root 開始的下級(注:僅下一級)對象,這個過程會 STW,但是跟 GC Root 直接關聯的下級對象不會很多,因此這個過程其實很快。
- CMS 的問題:?
- 1、并發回收導致CPU資源緊張:
- 在并發階段,它雖然不會導致用戶線程停頓,但卻會因為占用了一部分線程而導致應用程序變慢,降低程序總吞吐量。CMS默認啟動的回收線程數是:(CPU核數 + 3)/ 4,當CPU核數不足四個時,CMS對用戶程序的影響就可能變得很大。?
- 2、無法清理浮動垃圾:
- 在CMS的并發標記和并發清理階段,用戶線程還在繼續運行,就還會伴隨有新的垃圾對象不斷產生,但這一部分垃圾對象是出現在標記過程結束以后,CMS無法在當次收集中處理掉它們,只好留到下一次垃圾收集時再清理掉。這一部分垃圾稱為“浮動垃圾”。?
- 3、并發失敗(Concurrent Mode Failure):
- 由于在垃圾回收階段用戶線程還在并發運行,那就還需要預留足夠的內存空間提供給用戶線程使用,因此CMS不能像其他回收器那樣等到老年代幾乎完全被填滿了再進行回收,必須預留一部分空間供并發回收時的程序運行使用。默認情況下,當老年代使用了 92% 的空間后就會觸發 CMS 垃圾回收,這個值可以通過 -XX: CMSInitiatingOccupancyFraction 參數來設置。?
- 這里會有一個風險:要是CMS運行期間預留的內存無法滿足程序分配新對象的需要,就會出現一次“并發失敗”(Concurrent Mode Failure),這時候虛擬機將不得不啟動后備預案:Stop The World,臨時啟用 Serial Old 來重新進行老年代的垃圾回收,這樣一來停頓時間就很長了。?
- 4、內存碎片問題:?
- CMS是一款基于“標記-清除”算法實現的回收器,這意味著回收結束時會有內存碎片產生。內存碎片過多時,將會給大對象分配帶來麻煩,往往會出現老年代還有很多剩余空間,但就是無法找到足夠大的連續空間來分配當前對象,而不得不提前觸發一次 Full GC 的情況。?
- 為了解決這個問題,CMS收集器提供了一個 -XX:+UseCMSCompactAtFullCollection 開關參數(默認開啟),用于在 Full GC 時開啟內存碎片的合并整理過程,由于這個內存整理必須移動存活對象,是無法并發的,這樣停頓時間就會變長。還有另外一個參數 -XX:CMSFullGCsBeforeCompaction,這個參數的作用是要求CMS在執行過若干次不整理空間的 Full GC 之后,下一次進入 Full GC 前會先進行碎片整理(默認值為0,表示每次進入 Full GC 時都進行碎片整理)。?
- 1、并發回收導致CPU資源緊張:
G1的回收過程
- 整體: 標記-壓縮 算法實現的回收器,局部: 標記-復制 算法實現。
- G1(Garbage First)回收器采用面向局部收集的設計思路和基于Region的內存布局形式,是一款主要面向服務端應用的垃圾回收器。G1設計初衷就是替換 CMS,成為一種全功能收集器。G1 在JDK9 之后成為服務端模式下的默認垃圾回收器,取代了 Parallel Scavenge 加 Parallel Old 的默認組合,而 CMS 被聲明為不推薦使用的垃圾回收器。G1從整體來看是基于 標記-壓縮 算法實現的回收器,但從局部(兩個Region之間)上看又是基于 標記-復制 算法實現的。
- G1 回收過程,G1 回收器的運作過程大致可分為四個步驟:?
- a.初始標記(會STW):僅僅只是標記一下 GC Roots 能直接關聯到的對象,并且修改TAMS指針的值,讓下一階段用戶線程并發運行時,能正確地在可用的Region中分配新對象。這個階段需要停頓線程,但耗時很短,而且是借用進行Minor GC的時候同步完成的,所以G1收集器在這個階段實際并沒有額外的停頓。
- b.并發標記:從 GC Roots 開始對堆中對象進行可達性分析,遞歸掃描整個堆里的對象圖,找出要回收的對象,這階段耗時較長,但可與用戶程序并發執行。當對象圖掃描完成以后,還要重新處理在并發時有引用變動的對象。
- c.最終標記(會STW):對用戶線程做短暫的暫停,處理并發階段結束后仍有引用變動的對象。
- d.清理階段(會STW):更新Region的統計數據,對各個Region的回收價值和成本進行排序,根據用戶所期望的停頓時間來制定回收計劃,可以自由選擇任意多個Region構成回收集,然后把決定回收的那一部分Region的存活對象復制到空的Region中,再清理掉整個舊Region的全部空間。這里的操作涉及存活對象的移動,必須暫停用戶線程,由多條回收器線程并行完成的。