Java語言出來之前,大家都在拼命的寫C或者C++的程序,而此時存在一個很大的矛盾,C++等語言創建對象要不斷的去開辟空間,不用的時候有需要不斷的去釋放控件,既要寫構造函數,又要寫析構函數,很多時候都在重復的allocated,然后不停的~析構。於是,有人就提出,能不能寫一段程序在實現這塊功能,每次創建,釋放控件的時候復用這段代碼,而無需重復的書寫呢?
1960年 基於MIT的Lisp首先提出了垃圾回收的概念,用於處理C語言等不停的析構操作,而這時Java還沒有出世呢!所以實際上GC並不是Java的專利,GC的歷史遠遠大於Java的歷史!
那究竟GC為我們做了什么操作呢?
1、 哪些內存需要回收? 2、 什么時候回收? 3、 如何回收? |
這時候有人就會疑惑了,既然GC已經為我們解決了這個矛盾,我們還需要學習GC么?當然當然是肯定的,那究竟什么時候我們還需要用到的呢?
1、 排查內存溢出 2、 排查內存泄漏 3、 性能調優,排查並發瓶頸 |
我們知道,GC主要處理的是對象的回收操作,那么什么時候會觸發一個對象的回收的呢?
1、 對象沒有引用
2、 作用域發生未捕獲異常
3、 程序在作用域正常執行完畢
4、 程序執行了System.exit()
5、 程序發生意外終止(被殺進程等)
其實,我們最容易想到的就是當對象沒有引用的時候會將這個對象標記為可回收對象,那么現在就有一個問題,是不是這個對象被賦值為null以后就一定被標記為可回收對象了呢?我們來看一個例子:
package com.yhj.jvm.gc.objEscape.finalizeEscape;
import com.yhj.jvm.gc.objEscape.pojo.FinalizedEscapeTestCase;
/** * @Described:逃逸分析測試 * @author YHJ create at 2011-12-24 下午05:08:09 * @FileNmae com.yhj.jvm.gc.finalizeEscape.FinalizedEscape.java */ public class FinalizedEscape { public static void main(String[] args) throwsInterruptedException { System.out.println(FinalizedEscapeTestCase.caseForEscape); FinalizedEscapeTestCase.caseForEscape = newFinalizedEscapeTestCase(); System.out.println(FinalizedEscapeTestCase.caseForEscape); FinalizedEscapeTestCase.caseForEscape=null; System.gc(); Thread.sleep(100); System.out.println(FinalizedEscapeTestCase.caseForEscape); } } package com.yhj.jvm.gc.objEscape.pojo; /** * @Described:逃逸分析測試用例 * @author YHJ create at 2011-12-24 下午05:07:05 * @FileNmae com.yhj.jvm.gc.pojo.TestCaseForEscape.java */ public class FinalizedEscapeTestCase {
public static FinalizedEscapeTestCase caseForEscape = null; @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("哈哈,我已逃逸!"); caseForEscape = this; } } |
程序的運行結果回事什么樣子的呢?
我們來看這段代碼
1、 System.out.println(FinalizedEscapeTestCase.caseForEscape); 2、 FinalizedEscapeTestCase.caseForEscape = newFinalizedEscapeTestCase(); 3、 System.out.println(FinalizedEscapeTestCase.caseForEscape); 4、 FinalizedEscapeTestCase.caseForEscape=null; 5、 System.gc(); 6、 Thread.sleep(100); 7、 System.out.println(FinalizedEscapeTestCase.caseForEscape); |
1、 當程序執行第一行是,因為這個對象沒有值,結果肯定是null
2、 程序第二行給該對象賦值為新開辟的一個對象
3、 第三行打印的時候,肯定是第二行對象的hash代碼
4、 第四行將該對象重新置為null
5、 第五行觸發GC
6、 為了保證GC能夠順利執行完畢,第六行等待100毫秒
7、 第七行打印對應的值,回事null么?一定會是null么?
我們來看一下對應的運行結果
本例中打印了
GC的日志,讓我們看的更清晰一點,我們很清晰的看出,最后一句打印的不是null,並且子啊之前,還出現了逃逸的字樣。說明這個對象逃逸了,在垃圾回收之前逃逸了,我們再來看這個pojo的寫法,就會發現,我們重寫了方法finalize,而這個方法就相當於C++中的析構方法,在GC回收之前,會先調用一次這個方法,而這個方法又將this指針指向他自己,因此得以成功逃逸!可見,並不是這個對象被賦值為null之后就一定被標記為可回收,有可能會發生逃逸!
下面我們來看一下幾種垃圾收集算法
1、 在JDK1.2之前,使用的是引用計數器算法,即當這個類被加載到內存以后,就會產生方法區,堆棧、程序計數器等一系列信息,當創建對象的時候,為這個對象在堆棧空間中分配對象,同時會產生一個引用計數器,同時引用計數器+1,當有新的引用的時候,引用計數器繼續+1,而當其中一個引用銷毀的時候,引用計數器-1,當引用計數器被減為零的時候,標志着這個對象已經沒有引用了,可以回收了!這種算法在JDK1.2之前的版本被廣泛使用,但是隨着業務的發展,很快出現了一個問題
當我們的代碼出現下面的情形時,該算法將無法適應
a) ObjA.obj = ObjB
b) ObjB.obj - ObjA
這樣的代碼會產生如下引用情形 objA指向objB,而objB又指向objA,這樣當其他所有的引用都消失了之后,objA和objB還有一個相互的引用,也就是說兩個對象的引用計數器各為1,而實際上這兩個對象都已經沒有額外的引用,已經是垃圾了。
2、 根搜索算法
根搜索算法是從離散數學中的圖論引入的,程序把所有的引用關系看作一張圖,從一個節點GC ROOT開始,尋找對應的引用節點,找到這個節點以后,繼續尋找這個節點的引用節點,當所有的引用節點尋找完畢之后,剩余的節點則被認為是沒有被引用到的節點,即無用的節點。

目前java中可作為GC Root的對象有
1、 虛擬機棧中引用的對象(本地變量表)
2、 方法區中靜態屬性引用的對象
3、 方法區中常量引用的對象
4、 本地方法棧中引用的對象(Native對象)
說了這么多,其實我們可以看到,所有的垃圾回收機制都是和引用相關的,那我們來具體的來看一下引用的分類,到底有哪些類型的引用?每種引用都是做什么的呢?
Java中存在四種引用,每種引用如下:
1、 強引用
只要引用存在,垃圾回收器永遠不會回收
Object obj = new Object();
//可直接通過obj取得對應的對象 如obj.equels(new Object());
而這樣 obj對象對后面new Object的一個強引用,只有當obj這個引用被釋放之后,對象才會被釋放掉,這也是我們經常所用到的編碼形式。
2、 軟引用
非必須引用,內存溢出之前進行回收,可以通過以下代碼實現
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;
sf.get();//有時候會返回null
這時候sf是對obj的一個軟引用,通過sf.get()方法可以取到這個對象,當然,當這個對象被標記為需要回收的對象時,則返回null;
軟引用主要用戶實現類似緩存的功能,在內存足夠的情況下直接通過軟引用取值,無需從繁忙的真實來源查詢數據,提升速度;當內存不足時,自動刪除這部分緩存數據,從真正的來源查詢這些數據。
3、 弱引用
第二次垃圾回收時回收,可以通過如下代碼實現
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
wf.get();//有時候會返回null
wf.isEnQueued();//返回是否被垃圾回收器標記為即將回收的垃圾
弱引用是在第二次垃圾回收時回收,短時間內通過弱引用取對應的數據,可以取到,當執行過第二次垃圾回收時,將返回null。
弱引用主要用於監控對象是否已經被垃圾回收器標記為即將回收的垃圾,可以通過弱引用的isEnQueued方法返回對象是否被垃圾回收器
4、 虛引用(幽靈/幻影引用)
垃圾回收時回收,無法通過引用取到對象值,可以通過如下代碼實現
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj=null;
pf.get();//永遠返回null
pf.isEnQueued();//返回從內存中已經刪除
虛引用是每次垃圾回收的時候都會被回收,通過虛引用的get方法永遠獲取到的數據為null,因此也被成為幽靈引用。
虛引用主要用於檢測對象是否已經從內存中刪除。
在上文中已經提到了,我們的對象在內存中會被划分為5塊區域,而每塊數據的回收比例是不同的,根據IBM的統計,數據如下圖所示:

我們知道,方法區主要存放類與類之間關系的數據,而這部分數據被加載到內存之后,基本上是不會發生變更的,
Java堆中的數據基本上是朝生夕死的,我們用完之后要馬上回收的,而Java棧和本地方法棧中的數據,因為有后進先出的原則,當我取下面的數據之前,必須要把棧頂的元素出棧,因此回收率可認為是100%;而程序計數器我們前面也已經提到,主要用戶記錄線程執行的行號等一些信息,這塊區域也是被認為是唯一一塊不會內存溢出的區域。在SunHostSpot的虛擬機中,對於程序計數器是不回收的,而方法區的數據因為回收率非常小,而成本又比較高,一般認為是“性價比”非常差的,所以Sun自己的虛擬機HotSpot中是不回收的!但是在現在高性能分布式J2EE的系統中,我們大量用到了反射、動態代理、CGLIB、JSP和OSGI等,這些類頻繁的調用自定義類加載器,都需要動態的加載和卸載了,以保證永久帶不會溢出,他們通過自定義的類加載器進行了各項操作,因此在實際的應用開發中,類也是被經常加載和卸載的,方法區也是會被回收的!但是方法區的回收條件非常苛刻,只有同時滿足以下三個條件才會被回收!
1、所有實例被回收
2、加載該類的ClassLoader被回收
3、Class對象無法通過任何途徑訪問(包括反射)
好了,我們現在切入正題,Java1.2之前主要通過引用計數器來標記是否需要垃圾回收,而1.2之后都使用根搜索算法來收集垃圾,而收集后的垃圾是通過什么算法來回收的呢?
1、 標記-清除算法
2、 復制算法
3、 標記-整理算法
我們來逐一過一下
1、 標記-清除算法

標記-清除算法采用從根集合進行掃描,對存活的對象對象標記,標記完畢后,再掃描整個空間中未被標記的對象,進行回收,如上圖所示。
標記-清除算法不需要進行對象的移動,並且僅對不存活的對象進行處理,在存活對象比較多的情況下極為高效,但由於標記-清除算法直接回收不存活的對象,因此會造成內存碎片!
2、 復制算法

復制算法采用從根集合掃描,並將存活對象復制到一塊新的,沒有使用過的空間中,這種算法當控件存活的對象比較少時,極為高效,但是帶來的成本是需要一塊內存交換空間用於進行對象的移動。也就是我們前面提到的
s0 s1等空間。
3、 標記-整理算法

標記
-整理算法采用標記-清除算法一樣的方式進行對象的標記,但在清除時不同,在回收不存活的對象占用的空間后,會將所有的存活對象往左端空閑空間移動,並更新對應的指針。標記-整理算法是在標記-清除算法的基礎上,又進行了對象的移動,因此成本更高,但是卻解決了內存碎片的問題。
我們知道,JVM為了優化內存的回收,進行了分代回收的方式,對於新生代內存的回收(minor GC)主要采用復制算法,下圖展示了minor GC的執行過程。

對於新生代和舊生代,
JVM可使用很多種垃圾回收器進行垃圾回收,下圖展示了不同生代不通垃圾回收器,其中兩個回收器之間有連線表示這兩個回收器可以同時使用。

而這些垃圾回收器又分為串行回收方式、並行回收方式合並發回收方式執行,分別運用於不同的場景。如下圖所示

下面我們來逐一介紹一下每個垃圾回收器。
1、 Serial收集器
看名字我們都可以看的出來,這個屬於串行收集器。其運行示意圖如下

Serial
收集器是歷史最悠久的一個回收器,JDK1.3之前廣泛使用這個收集器,目前也是ClientVM下 ServerVM 4核4GB以下機器的默認垃圾回收器。串行收集器並不是只能使用一個CPU進行收集,而是當JVM需要進行垃圾回收的時候,需要中斷所有的用戶線程,知道它回收結束為止,因此又號稱“Stop The World” 的垃圾回收器。注意,JVM中文名稱為java虛擬機,因此它就像一台虛擬的電腦一樣在工作,而其中的每一個線程就被認為是JVM的一個處理器,因此大家看到圖中的CPU0、CPU1實際為用戶的線程,而不是真正機器的CPU,大家不要誤解哦。
串行回收方式適合低端機器,是Client模式下的默認收集器,對CPU和內存的消耗不高,適合用戶交互比較少,后台任務較多的系統。
Serial收集器默認新舊生代的回收器搭配為Serial+ SerialOld
2、 ParNew收集器
ParNew收集器其實就是多線程版本的Serial收集器,其運行示意圖如下

同樣有
Stop The World的問題,他是多CPU模式下的首選回收器(該回收器在單CPU的環境下回收效率遠遠低於Serial收集器,所以一定要注意場景哦),也是Server模式下的默認收集器。
3、 ParallelScavenge
ParallelScavenge又被稱為是吞吐量優先的收集器,器運行示意圖如下

ParallelScavenge
所提到的吞吐量=程序運行時間/(JVM執行回收的時間+程序運行時間),假設程序運行了100分鍾,JVM的垃圾回收占用1分鍾,那么吞吐量就是99%。在當今網絡告訴發達的今天,良好的響應速度是提升用戶體驗的一個重要指標,多核並行雲計算的發展要求程序盡可能的使用CPU和內存資源,盡快的計算出最終結果,因此在交互不多的雲端,比較適合使用該回收器。
4、 ParallelOld
ParallelOld是老生代並行收集器的一種,使用標記整理算法、是老生代吞吐量優先的一個收集器。這個收集器是JDK1.6之后剛引入的一款收集器,我們看之前那個圖之間的關聯關系可以看到,早期沒有ParallelOld之前,吞吐量優先的收集器老生代只能使用串行回收收集器,大大的拖累了吞吐量優先的性能,自從JDK1.6之后,才能真正做到較高效率的吞吐量優先。其運行示意圖如下

5、
SerialOld
SerialOld是舊生代Client模式下的默認收集器,單線程執行;在JDK1.6之前也是ParallelScvenge回收新生代模式下舊生代的默認收集器,同時也是並發收集器CMS回收失敗后的備用收集器。其運行示意圖如下

6、
CMS
CMS又稱響應時間優先(最短回收停頓)的回收器,使用並發模式回收垃圾,使用標記-清除算法,CMS對CPU是非常敏感的,它的回收線程數=(CPU+3)/4,因此當CPU是2核的實惠,回收線程將占用的CPU資源的50%,而當CPU核心數為4時僅占用25%。他的運行示意圖如下

CMS
模式主要分為4個過程
在初始標記的時候,需要中斷所有用戶線程,在並發標記階段,用戶線程和標記線程
並發執行,而在這個過程中,隨着內存引用關系的變化,可能會發生原來標記的對象被釋放,進而引發新的垃圾,因此可能會產生一系列的浮動垃圾,不能被回收。
CMS 為了確保能夠掃描到所有的對象,避免在Initial Marking 中還有未標識到的對象,采用的方法為找到標記了的對象,並將這些對象放入Stack 中,掃描時尋找此對象依賴的對象,如果依賴的對象的地址在其之前,則將此對象進行標記,並同時放入Stack 中,如依賴的對象地址在其之后,則僅標記該對象。
在進行Concurrent Marking 時minor GC 也可能會同時進行,這個時候很容易造成舊生代對象引用關系改變,CMS 為了應對這樣的並發現象,提供了一個Mod Union Table 來進行記錄,在這個Mod Union Table中記錄每次minor GC 后修改了的Card 的信息。這也是ParallelScavenge不能和CMS一起使用的原因。
CMS產生浮動垃圾的情況請見如下示意圖

在運行回收過后,c就變成了浮動垃圾。
由於CMS會產生浮動垃圾,當回收過后,浮動垃圾如果產生過多,同時因為使用標記-清除算法會產生碎片,可能會導致回收過后的連續空間仍然不能容納新生代移動過來或者新創建的大資源,因此會導致CMS回收失敗,進而觸發另外一次FULL GC,而這時候則采用SerialOld進行二次回收。
同時CMS因為可能產生浮動垃圾,而CMS在執行回收的同時新生代也有可能在進行回收操作,為了保證舊生代能夠存放新生代轉移過來的數據,CMS在舊生代內存到達全部容量的68%就觸發了CMS的回收!
7、 GarbageFirst(G1 )
我們再來看垃圾回收器的總圖,剛才我們可以看到,我在圖上標記了一個?,其實這是一個新的垃圾回收器,既可以回收新生代也可以回收舊生代,SunHotSpot 1.6u14以上EarlyAccess版本加入了這個回收器,sun公司預期SunHotSpot1.7發布正式版,他是商用高性能垃圾回收器,通過重新划分內存區域,整合優化CMS,同時注重吞吐量和響應時間,但是杯具的是被oracle收購之后這個收集器屬於商用收費收集器,因此目前基本上沒有人使用,我們在這里也就不多介紹,更多信息可以參考oracle新版本JDK說明。
下面我們再來看下JVM的一些內存分配與回收策略
1、 優先在Edon上分配對象
代碼示例 package com.yhj.jvm.gc.edenFirst; /** * @Described:Edon優先划分對象測試 * VM params : -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -verbose:gc * Edon s0 s1 old * 8 1 1 10 * @author YHJ create at 2012-1-3 下午04:44:43 * @FileNmae com.yhj.jvm.gc.edenFirst.EdonFirst.java */ public class EdonFirst {
private final static int ONE_MB = 1024*1024;
/** * @param args * @Author YHJ create at 2012-1-3 下午04:44:38 */ public static void main(String[] args) { @SuppressWarnings("unused") byte[] testCase1,testCase2,testCase3,testCase4; testCase1 = new byte[2*ONE_MB]; testCase2 = new byte[2*ONE_MB]; testCase3 = new byte[2*ONE_MB]; // testCase1 = null; // testCase2 = null; // testCase3 = null; testCase4 = new byte[2*ONE_MB]; }
} 運行結果
![]() 結果分析 |
從運行結果我們可以很清晰的看到,eden有8MB的存儲控件(通過參數配置),前6MB的數據優先分配到eden區域,當下一個2MB存放時,因空間已滿,觸發一次GC,但是這部分數據因為沒有回收(引用還在,當賦值為null后則不會轉移),數據會被復制到s0區域,但是s0區域不夠存儲,因此直接放入老生代區域,新的2MB數據存放在eden區域
2、 大對象直接進入老生代
代碼示例 package com.yhj.jvm.gc.bigObjIntoOld; /** * @Described:大對象直接進入老生代測試 * VM params : -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -verbose:gc * Edon s0 s1 old * 8 1 1 10 * @author YHJ create at 2012-1-3 下午05:28:47 * @FileNmae com.yhj.jvm.gc.bigObjIntoOld.BigObjIntoOld.java */ public class BigObjIntoOld {
private final static int ONE_MB = 1024*1024;
/** * @param args * @Author YHJ create at 2012-1-3 下午04:44:38 */ public static void main(String[] args) { @SuppressWarnings("unused") byte[] testCase1,testCase2,testCase3,testCase4; testCase1 = new byte[8*ONE_MB]; // testCase2 = new byte[2*ONE_MB]; // testCase3 = new byte[2*ONE_MB]; // testCase1 = null; // testCase2 = null; // testCase3 = null; // testCase4 = new byte[2*ONE_MB]; }
} 運行結果
![]() 結果分析 |
我們看到,沒有觸發GC日志,而數據是直接進入老生代的
3、 年長者(長期存活對象)進入老生代
代碼示例: package com.yhj.jvm.gc.longLifeTimeIntoOld; /** * @Described:當年齡大於一定值的時候進入老生代 默認值15歲 * VM params : -Xms20M -Xmx20M -Xmn10M -XX:MaxTenuringThreshold=1-XX:+PrintGCDetails -verbose:gc * Edon s0 s1 old age * 8 1 1 10 1 * @author YHJ create at 2012-1-3 下午05:39:16 * @FileNmaecom.yhj.jvm.gc.longLifeTimeIntoOld.LongLifeTimeIntoOld.java */ public class LongLifeTimeIntoOld {
private final static int ONE_MB = 1024*1024;
/** * @param args * @Author YHJ create at 2012-1-3 下午04:44:38 */ public static void main(String[] args) { @SuppressWarnings("unused") byte[] testCase1,testCase2,testCase3,testCase4; testCase1 = new byte[1*ONE_MB/4]; testCase2 = new byte[7*ONE_MB+3*ONE_MB/4]; testCase2 = null; testCase3 = new byte[7*ONE_MB+3*ONE_MB/4]; testCase3 = null; testCase4 = new byte[ONE_MB]; }
}
運行結果
![]() 結果分析 |
從代碼中我們可以看到,當testCase1划分為0.25MB數據,進行多次大對象創建之后,testCase1應該在GC執行之后被復制到s0區域(s0足以容納testCase1),但是我們設置了對象的年齡為1,即超過1歲便進入老生代,因此GC執行2次后testCase1直接被復制到了老生代,而默認進入老生代的年齡為15。我們通過profilter的監控工具可以很清楚的看到對象的年齡,如圖所示

右側的年代數目就是對象的年齡
4、 群體效應(大批中年對象進入老生代)
代碼示例 package com.yhj.jvm.gc.dynamicMoreAVG_intoOld; /** * @Described:s0占用空間到達50%直接進入老生代 * VM params : -Xms20M -Xmx20M -Xmn10M -XX:MaxTenuringThreshold=15-XX:+PrintGCDetails -verbose:gc * Edon s0 s1 old age * 8 1 1 10 15 * 0.5 0 0 7.5 * 7.5 0.5 0 7.5 * 7.5 0 0 8 * @author YHJ create at 2012-1-3 下午05:50:40 * @FileNmae com.yhj.jvm.gc.dynamicMoreAVG_intoOld.MoreAVG_intoOld.java */ public class MoreAVG_intoOld {
private final static int ONE_MB = 1024*1024;
/** * @param args * @Author YHJ create at 2012-1-3 下午04:44:38 */ public static void main(String[] args) { @SuppressWarnings("unused") byte[] testCase1,testCase2,testCase3,testCase4; testCase1 = new byte[7*ONE_MB+ONE_MB/2]; testCase2 = new byte[ONE_MB/2]; testCase3 = new byte[7*ONE_MB+ONE_MB/2]; testCase3 = null; testCase4 = new byte[7*ONE_MB+ONE_MB/2];
// testCase1 = new byte[7*ONE_MB+3*ONE_MB/4]; // testCase2 = new byte[ONE_MB/4]; // testCase3 = new byte[7*ONE_MB+3*ONE_MB/4];
}
} 運行結果
![]() 結果分析 |
我們看到,當創建后testCase3,testCase2被移動到s0區域,當被釋放后,繼續創建testCase3,按理說testCase2應該移動到s1區域,但是因為超過了s1區域的1/2,因此直接進入老生代
5、 擔保GC(擔保minorGC)
擔保GC就是擔保minorGC能夠滿足當前的存儲空間,而無需觸發老生代的回收,由於大部分對象都是朝生夕死的,因此,在實際開發中這種很起效,但是也有可能會發生擔保失敗的情況,當擔保失敗的時候會觸發FullGC,但是失敗畢竟是少數,因此這種一般是很划算的。
代碼示例 package com.yhj.jvm.gc.securedTransactions; /** * @Described:擔保交易測試 * VM params : -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -verbose:gc-XX:-HandlePromotionFailure 無擔保 * VM params : -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -verbose:gc-XX:+HandlePromotionFailure 有擔保 * Edon s0 s1 old * 8 1 1 10 * @author YHJ create at 2012-1-3 下午06:11:17 * @FileNmaecom.yhj.jvm.gc.securedTransactions.SecuredTransactions.java */ public class SecuredTransactions {
private final static int ONE_MB = 1024*1024;
/** * @param args * @Author YHJ create at 2012-1-3 下午04:44:38 */ public static void main(String[] args) { @SuppressWarnings("unused") byte[] testCase1,testCase2,testCase3,testCase4,testCase5,testCase6,testCase7; testCase1 = new byte[2*ONE_MB]; testCase2 = new byte[2*ONE_MB]; testCase3 = new byte[2*ONE_MB]; testCase1 = null; testCase4 = new byte[2*ONE_MB]; testCase5 = new byte[2*ONE_MB]; testCase6 = new byte[2*ONE_MB]; testCase4 = null; testCase5 = null; testCase6 = null; testCase7 = new byte[2*ONE_MB];
}
} 運行結果 1、 無擔保
2、 ![]() 有擔保 |

結果分析
我們可以很清楚的看到,當無擔保的時候,觸發了一次FullGC 而有擔保的情況下,只有monorGC則完成了回收,大大提升了效率。
當我們注釋掉對應的代碼
// testCase4 = null; // testCase5 = null; // testCase6 = null; |
的時候,就會引發擔保失敗,如下圖所示
JVM
默認情況是是開啟擔保的,無需設置參數。