JVM系列3:JVM垃圾回收
1.JVM記憶體分配和回收
1.1 物件分配原則
在JVM系列1:記憶體區域中我們談到,JVM堆中的記憶體劃分如下:
從中可以看出堆記憶體分為新生代和老年代以及永久代(在JDK1.8中已經被MetaSpace元空間替代),其中新生代又分為Eden區和Survior1區和Survior2區;
堆中分配記憶體常見的策略:
- 物件優先分配在Eden區
- 大物件直接進入老年代
- 長期存活的物件進入老年代
1.2 物件回收
因為主流的垃圾回收器使用的分代回收策略,因此堆記憶體需要分為新生代和老年代。方便在不同的區域採用合適的記憶體回收策略。
回收方式有Minor Gc和Full GC(Major Gc)兩種,兩者區別如下:
- 大多數情況下,物件在Eden區分配地址,當Eden區記憶體不足時,會發生一次Minor Gc,也就是新生代GC
- 老年代發生的GC,通常一次Full Gc至少伴隨著一次Minor Gc,且Full Gc的速度較Minor Gc慢10倍以上。也就是老年代GC
以下是測試Minor Gc的例項:
在Run Configurations中的Arguments中新增列印GC資訊的引數:
-XX:+PrintGCDetails
由上面的GC列印結果可知,新建立的物件part1優先分配了Eden區
此時Eden區使用率已經接近100%,此時我們繼續建立一個物件part2
由上面的GC列印結果可知,由於Eden區記憶體空間已經不足以容納物件part2,剛才Minor Gc中講到:Eden區沒有足夠空間進行分配時,虛擬機器將發起一次Minor GC,在GC過程中,虛擬機發現part1物件無法存入Survior區,此時只好通過 分配擔保機制<文章底部有介紹> 把新生代的物件提前轉移到老年代中去,因為老年代上的空間足夠存放part1,所以不會出現Full GC。
2.判斷物件死亡方法
堆中幾乎放著所有的物件例項,因此在對堆垃圾回收的的第一步:就是要判斷哪些物件已經死亡,即哪些物件是此次GC可以進行回收的。
判斷物件是否死亡有兩種方法:引用計數法和可達性分析演算法。
2.1 引用計數法
給物件新增一個引用計算器,每當一個地方引用它時,則該引用計數器加1,當引用失效時,計數器減1,任意時刻如果物件的引用計數器為0,那麼虛擬機器就認為沒有任何地方需要引用該物件,即物件"死了",可以對其進行記憶體回收。
優點:實現簡單、高效。
缺點:無法應對物件的迴圈引用。基於此目前主流的虛擬機器中沒有采用該方法。
public class ReferenceCountingGc { Object instance = null; public static void main(String[] args) { ReferenceCountingGc objA = new ReferenceCountingGc(); ReferenceCountingGc objB = new ReferenceCountingGc(); objA.instance = objB; objB.instance = objA; objA = null; objB = null; } } /*在上述例項中,objA和objB相互引用,即便後面對objA和objB都設定為null *時,但是它們的引用計數器都不為0,導致採用引用計數法的GC也無法對 *objA和objB進行垃圾回收*/
2.2 可達性分析演算法
該演算法的思想是:通過一系列被稱為"GC Roots"的物件作為起點,從這些起點開始出發,到達每個物件所走過的路徑稱為"引用鏈",當一個物件到GC Roots之前沒有引用鏈連線的話,說明該物件是不可及的狀態,也就是不可用,可以進行GC。
目前主流虛擬機器中大多采用該演算法判斷物件是否存活。
2.3 強引用、軟引用、弱引用和虛引用
⑴強引用(StrongReference)
強引用是使用最普遍的引用。如果一個物件具有強引用,那垃圾回收器絕不會回收它。當記憶體空間不足,Java虛擬機器寧願丟擲OutOfMemoryError錯誤,使程式異常終止,也不會靠隨意回收具有強引用的物件來解決記憶體不足的問題。 ps:強引用其實也就是我們平時A a = new A()這個意思。如果你不需要使用某個物件了,可以將相應的引用設定為null,消除強引用來幫助垃圾回收器進行回收。因為過多的強引用也是導致OOM的罪魁禍首。
總結下來,強引用有以下特點:
- 強引用就是最普通的引用
- 可以使用強引用直接訪問目標物件
- 強引用指向的物件在任何時候都不會被系統回收
- 強引用可能會導致記憶體洩漏
- 過多的強引用會導致OOM
因為持有強引用的物件不會被垃圾回收,所以可能導致記憶體洩漏:
ObjectA a = new ObjectA(); ObjectB b = new ObjectB(a); a = null; /*在上述程式碼中,物件a和b都持有一個物件的強引用。當執行a =null後,本來物件ObjectA的強引用a釋放了。但是此時因為ObjectB持有ObjectA的強引 用,導致無法對ObjectA進行垃圾回收,此時就會造成記憶體洩漏*/
(2)軟引用(SoftReference)
軟引用是使用SoftReference建立的引用,強度弱於強引用,被其引用的物件在記憶體不足的時候會被回收,不會產生記憶體溢位。
在垃圾回收器沒有回收時,軟可達物件<文章底部有介紹> 就像強可達物件一樣,可以被程式正常訪問和使用,但是需要通過軟引用物件間接訪問,需要的話也能重新使用強引用將其關聯。所以軟引用適合用來做記憶體敏感的快取記憶體。
String s = new String("Frank"); // 建立強引用與String物件關聯,現在該String物件為強可達狀態 SoftReference<String> softRef = new SoftReference<String>(s); // 再建立一個軟引用關聯該物件 s = null; // 消除強引用,現在只剩下軟引用與其關聯,該String物件為軟可達狀態 s = softRef.get(); // 重新關聯上強引用
軟引用的總結如下:
- 軟引用弱於強引用
- 軟引用指向的物件會在記憶體不足時被垃圾回收清理掉
- JVM會優先回收長時間閒置不用的軟引用物件,對那些剛剛構建的或剛剛使用過的軟引用物件會盡可能保留
- 軟引用可以有效的解決OOM問題
- 軟引用適合用作非必須大物件的快取
(3)弱引用(WeakReference)
弱引用是使用WeakReference建立的引用,弱引用也是用來描述非必需物件的,它是比軟引用更弱的引用型別。在發生GC時,只要發現弱引用,不管系統堆空間是否足夠,都會將物件進行回收。(如果弱引用物件較大,直接進到了老年代,那麼就可以苟且偷生到Full GC觸發前)
String s = new String("Frank"); WeakReference<String> weakRef = new WeakReference<String>(s); s = null; //把s設定為null後,字串物件便只有弱引用指向它。
弱引用的特點:
- 只具有弱引用的物件擁有更短暫的生命週期。
- 被垃圾回收器回收的時機不一樣,在垃圾回收器執行緒掃描它所管轄的記憶體區域的過程中,一旦發現了只具有弱引用的物件,不管當前記憶體空間足夠與否,都會回收它的記憶體。而被軟引用關聯的物件只有在記憶體不足時才會被回收。
- 弱引用不會影響GC,而軟引用會一定程度上對GC造成影響
(4)虛引用(PhantomReference)
虛引用是使用PhantomReference建立的引用,虛引用也稱為幽靈引用或者幻影引用,是所有引用型別中最弱的一個。一個物件是否有虛引用的存在,完全不會對其生命週期構成影響,也無法通過虛引用獲得一個物件例項。
- 虛引用是最弱的引用
- 虛引用對物件而言是無感知的,物件有虛引用跟沒有是完全一樣的
- 虛引用不會影響物件的生命週期
- 虛引用可以用來做為物件是否存活的監控(可用來做堆外記憶體的回收)
虛引用與軟引用和弱引用的一個區別在於: 虛引用必須和引用佇列(ReferenceQueue)聯合使用。當垃圾回收器準備回收一個物件時,如果發現它還有虛引用,就會在回收物件的記憶體之前,把這個虛引用加入到與之關聯的引用佇列中。程式可以通過判斷引用佇列中是 否已經加入了虛引用,來了解被引用的物件是否將要被垃圾回收。程式如果發現某個虛引用已經被加入到引用佇列,那麼就可以在所引用的物件的記憶體被回收之前採取必要的行動。
特別注意,在程式設計中一般很少使用弱引用與虛引用,使用軟引用的情況較多,這是因為軟引用可以加速JVM對垃圾記憶體的回收速度,可以維護系統的執行安全,防止記憶體溢位(OutOfMemory)等問題的產生。
3.幾種垃圾回收演算法
3.1 標記-清除演算法
標記-清除演算法分為:標記階段和清除階段,標記階段:從根節點開始,標記所有從根節點開始的物件,未被標記的物件就是未被引用的垃圾物件,然後時清除階段:清除所有未被標記的物件。
標記-清除演算法存在問題:
- 造成可用記憶體不連續,存在大量記憶體碎片,大物件進行記憶體分配時可能會提前出發Full Gc;
- 效率較低
3.2 複製演算法
將現有的記憶體空間分為兩快,每次只使用其中一塊,在垃圾回收時將正在使用的記憶體中的存活物件複製到未被使用的記憶體塊中,之後,清除正在使用的記憶體塊中的所有物件,交換兩個記憶體的角色,完成垃圾回收。
複製演算法存在的問題是可用記憶體空間只有原來的一半,為了解決這個問題:
IBM研究表明新生代中的物件98%是朝夕生死的,所以並不需要按照1:1的比例劃分記憶體空間,而是將記憶體分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中的一塊Survivor。當回收時,將Eden和Survivor中還存活著的物件一次性地拷貝到另外一個Survivor空間上,最後清理掉Eden和剛才用過的Survivor的空間。HotSpot虛擬機器預設Eden和Survivor的大小比例是8:1(可以通過-SurvivorRattio來配置),也就是每次新生代中可用記憶體空間為整個新生代容量的90%,只有10%的記憶體會被“浪費”。當然,98%的物件可回收只是一般場景下的資料,我們沒有辦法保證回收都只有不多於10%的物件存活,當Survivor空間不夠用時,需要依賴其他記憶體(這裡指老年代)進行分配擔保。
複製演算法的應用場景:存活物件少,垃圾物件多的記憶體區域,這種情形在新生代較為常見,因此現在的商業虛擬機器都採用這種收集演算法來回收新生代,而老年代的存活物件較多,如果依然採用複製演算法,複製開銷過大顯然不合適,此時我們就採用下面一種垃圾回收演算法。
3.3 標記-整理演算法
標記-整理演算法和標記-清除演算法類似,也分為兩個階段:標記過程仍然與“標記-清除”演算法一樣,但後續步驟不是直接對可回收物件回收,而是讓所有存活的物件向一段移動,然後直接清理掉端邊界以外的記憶體。
*標記-壓縮演算法是一種老年代的回收演算法,它在標記-清除演算法的基礎上做了一些優化。首先也需要從根節點開始對所有可達物件做一次標記,但之後,它並不簡單地清理未標記的物件,而是將所有的存活物件壓縮到記憶體的一端。之後,清理邊界外所有的空間。這種方法既避免了碎片的產生,又不需要兩塊相同的記憶體空間,因此,其價效比比較高。
3.4 增量演算法
如果一次GC過程將所有垃圾都進行回收,會導致耗時較長,使程式進入一個長時間停頓的狀態,為了解決這種問題,我們想到了讓垃圾回收的執行緒和程式的主執行緒交替執行,每次垃圾回收執行緒只回收一小部分記憶體區域,然後馬上切換到主程式執行,然後主程式執行一段時間後由切換回垃圾回收執行緒,如此迴圈直至記憶體區域全部回收完畢,使用這種方式時:由於在垃圾回收過程中,間斷性地還執行了應用程式程式碼,所以能減少系統的停頓時間。但是,因為執行緒切換和上下文轉換的消耗,會使得垃圾回收的總體成本上升,造成系統吞吐量的下降。
4.幾種垃圾收集器
第3節中講到的垃圾回收演算法是記憶體回收的方法或是思想,而本節中的垃圾收集器就是記憶體回收的具體實現。
此節我們將對各個收集器進行比較,但並非了挑選出一個最好的收集器。因為知道到目前為止,還沒有最好的垃圾收集器來滿足我們在不同記憶體區域的垃圾回收需求,我們能做的就是根據具體應用場景選擇適合自己的垃圾收集器
4.1 Serial收集器
Serial(序列)收集器是最基本也是最悠久的垃圾收集器,從其命名來看,"序列",即在同一時間內只能做一個任務,所以它是一個"單執行緒"的垃圾收集器,這個單執行緒不僅僅是指只有一條垃圾回收程序來進行回收工作,而是更具重量級的單執行緒,當它在工作時,所有其他工作執行緒都必須停止下來(Stop the world),直至回收結束。
在Serial收集器中:新生代採用複製演算法,老年代採用標記-整理演算法。
優點:由於沒有執行緒切換和上下文切換的開銷,因此它簡單而高效(與其他收集器的單執行緒相比),通常可應用在Client端的垃圾回收中。
缺點:由於"Stop The World",導致使用者體驗差。
4.2 ParNew收集器
ParNew收集器其實就是Serial收集器的多執行緒版本,除了使用多執行緒進行垃圾收集外,其餘行為(控制引數、收集演算法、回收策略等等)和Serial收集器完全一樣。
在ParNew收集器中:新生代採用複製演算法,老年代採用標記-整理演算法。
優點: 1.能夠併發,因此是大多數Server環境下的垃圾回收的選擇 2.除Serial之外是唯一可以和CMS收集器(真正意義上的併發收集器)配合工作。
4.3 Parallel Scavenge收集器
Parallel是採用複製演算法的多執行緒新生代垃圾回收器,它和ParNew收集器有很多的相似的地方,但Parallel更加註重吞吐量(文章下方有註釋)
Parallel Scavenge收集器提供了很多引數供使用者找到最合適的停頓時間或最大吞吐量,如果不熟悉,可以把記憶體管理優化的工作交由虛擬機器來完成。
Parallel Scavenge收集器:新生代採用複製演算法,老年代採用標記-整理演算法。
4.4 Serial Old收集器
Serial收集器的老年代版本,它同樣是一個單執行緒收集器。它主要有兩大用途:一種用途是在JDK1.5以及以前的版本中與Parallel Scavenge收集器搭配使用,另一種用途是作為CMS收集器的後備方案。
4.5 Parallel Old收集器
Parallel Scavenge收集器的老年代版本。使用多執行緒和“標記-整理”演算法。在注重吞吐量以及CPU資源的場合,都可以優先考慮 Parallel Scavenge收集器和Parallel Old收集器。
4.6 CMS收集器
CMS(Concurrent Mark Swep)收集器是一個比較重要的回收器,現在應用非常廣泛,CMS一種獲取最短回收停頓時間為目標的收集器,它是HotSpot虛擬機器第一款真正意義上的併發收集器,它第一次實現了讓垃圾收集執行緒與使用者執行緒(基本上)同時工作。這使得它很適合用於和使用者互動的業務。從名字(Mark Swep)就可以看出,CMS收集器是基於標記清除演算法實現的。它的收集過程分為四個步驟:
- 初始標記(initial mark) :暫停所有的其他執行緒,並記錄下直接與root相連的物件,速度很快 ;
- 併發標記(concurrent mark):同時開啟GC和使用者執行緒,用一個閉包結構去記錄可達物件。但在這個階段結束,這個閉包結構並不能保證包含當前所有的可達物件。因為使用者執行緒可能會不斷的更新引用域,所以GC執行緒無法保證可達性分析的實時性。所以這個演算法裡會跟蹤記錄這些發生引用更新的地方。
- 重新標記(remark):重新標記階段就是為了修正併發標記期間因為使用者程式繼續執行而導致標記產生變動的那一部分物件的標記記錄,這個階段的停頓時間一般會比初始標記階段的時間稍長,遠遠比並發標記階段時間短
- 併發清除(concurrent sweep):開啟使用者執行緒,同時GC執行緒開始對未標記的區域做清掃。
優點:
- 真正實現併發
- 低停頓
缺點:
- 對CPU資源敏感
- 無法處理浮動垃圾
- 使用的回收演算法-“標記-清除”演算法會導致收集結束時會有大量空間碎片產生。
4.7 G1收集器
G1 (Garbage-First)是一款面向伺服器的垃圾收集器,主要針對配備多顆處理器及大容量記憶體的機器. 以極高概率滿足GC停頓時間要求的同時,還具備高吞吐量效能特徵。
G1收集器的工作過程:
- 初始標記
- 併發標記
- 最終標記
- 篩選回收
它具有如下特點:
- 併發與並行:充分利用硬體資源,減少"Stop The World"時間,減少使用者執行緒停頓時間。
- 分代收集:G1可以不需要其他收集器配合就能獨立管理整個GC堆,但是還是保留了分代的概念。
- 空間整合:與CMS的“標記--清理”演算法不同,G1從整體來看是基於“標記整理”演算法實現的收集器;從區域性上來看是基於“複製”演算法實現的,因此不會出現大量空間碎片。
- 可預測的停頓:G1和CMS一樣, 都追求低停頓,但G1還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內。
G1收集器在後臺維護了一個優先列表,每次根據允許的收集時間,優先選擇回收價值最大的Region(這也就是它的名字Garbage-First的由來)。這種使用Region劃分記憶體空間以及有優先順序的區域回收方式,保證了GF收集器在有限時間內可以儘可能高的收集效率(把記憶體化整為零)。
參考:
- 《深入理解Java虛擬機器:JVM高階特性與最佳實踐(第二版》
- https://my.oschina.net/hosee/blog/644618
分配擔保機制:在發生Minor GC之前,虛擬機器會先檢查老年代最大可用的連續空間是否大於新生代所有物件總空間,如果這個條件成立,那麼Minor GC可以確保是安全的。如果不成立,則虛擬機器會檢視HandlePromotionFailure設定值是否允許擔保失敗。如果允許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代物件的平均大小,如果大於,將嘗試著進行一次Minor GC,儘管這次Minor GC是有風險的;如果小於,或者HandlePromotionFailure設定不允許冒險,那這時也要改為進行一次Full GC。
下面解釋一下“冒險”是冒了什麼風險,前面提到過,新生代使用複製收集演算法,但為了記憶體利用率,只使用其中一個Survivor空間來作為輪換備份,因此當出現大量物件在Minor GC後仍然存活的情況(最極端的情況就是記憶體回收後新生代中所有物件都存活),就需要老年代進行分配擔保,把Survivor無法容納的物件直接進入老年代。與生活中的貸款擔保類似,老年代要進行這樣的擔保,前提是老年代本身還有容納這些物件的剩餘空間,一共有多少物件會活下來在實際完成記憶體回收之前是無法明確知道的,所以只好取之前每一次回收晉升到老年代物件容量的平均大小值作為經驗值,與老年代的剩餘空間進行比較,決定是否進行Full GC來讓老年代騰出更多空間。
取平均值進行比較其實仍然是一種動態概率的手段,也就是說,如果某次Minor GC存活後的物件突增,遠遠高於平均值的話,依然會導致擔保失敗(Handle Promotion Failure)。如果出現了HandlePromotionFailure失敗,那就只好在失敗後重新發起一次Full GC。雖然擔保失敗時繞的圈子是最大的,但大部分情況下都還是會將HandlePromotionFailure開關開啟,避免Full GC過於頻繁
軟可達:如果一個物件與GC Roots之間不存在強引用,但是存在軟引用,則稱這個物件為軟可達(soft reachable)
物件。
記憶體洩漏:是指無用物件(不再使用的物件)持續佔有記憶體或無用物件的記憶體得不到及時釋放,從而造成記憶體空間的浪費。
吞吐量:所謂吞吐量就是CPU用於執行使用者程式碼的時間與CPU總消耗時間的比值,即吞吐量=執行使用者程式碼時間 / (執行使用者程式碼時間 + 垃圾收集時間)
來源:https://www.cnblogs.com/LearnAndGet/p/9778004.html