Java语言出来以前,你们都在拼命的写C或者C++的程序,而此时存在一个很大的矛盾,C++等语言建立对象要不断的去开辟空间,不用的时候有须要不断的去释放控件,既要写构造函数,又要写析构函数,不少时候都在重复的allocated,而后不停的~析构。因而,有人就提出,能不能写一段程序在实现这块功能,每次建立,释放控件的时候复用这段代码,而无需重复的书写呢?java
1960年 基于MIT的Lisp首先提出了垃圾回收的概念,用于处理C语言等不停的析构操做,而这时Java尚未出世呢!因此实际上GC并非Java的专利,GC的历史远远大于Java的历史!算法
那究竟GC为咱们作了什么操做呢?缓存
一、 哪些内存须要回收?网络 二、 何时回收?多线程 三、 如何回收?并发 |
这时候有人就会疑惑了,既然GC已经为咱们解决了这个矛盾,咱们还须要学习GC么?固然固然是确定的,那究竟何时咱们还须要用到的呢?oracle
一、 排查内存溢出jvm 二、 排查内存泄漏分布式 三、 性能调优,排查并发瓶颈ide |
咱们知道,GC主要处理的是对象的回收操做,那么何时会触发一个对象的回收的呢?
一、 对象没有引用
二、 做用域发生未捕获异常
三、 程序在做用域正常执行完毕
四、 程序执行了System.exit()
五、 程序发生意外终止(被杀进程等)
其实,咱们最容易想到的就是当对象没有引用的时候会将这个对象标记为可回收对象,那么如今就有一个问题,是否是这个对象被赋值为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; } } |
程序的运行结果回事什么样子的呢?
咱们来看这段代码
一、 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); |
一、 当程序执行第一行是,由于这个对象没有值,结果确定是null
二、 程序第二行给该对象赋值为新开辟的一个对象
三、 第三行打印的时候,确定是第二行对象的hash代码
四、 第四行将该对象从新置为null
五、 第五行触发GC
六、 为了保证GC可以顺利执行完毕,第六行等待100毫秒
七、 第七行打印对应的值,回事null么?必定会是null么?
咱们来看一下对应的运行结果
本例中打印了
GC的日志,让咱们看的更清晰一点,咱们很清晰的看出,最后一句打印的不是null,而且子啊以前,还出现了逃逸的字样。说明这个对象逃逸了,在垃圾回收以前逃逸了,咱们再来看这个pojo的写法,就会发现,咱们重写了方法finalize,而这个方法就至关于C++中的析构方法,在GC回收以前,会先调用一次这个方法,而这个方法又将this指针指向他本身,所以得以成功逃逸!可见,并非这个对象被赋值为null以后就必定被标记为可回收,有可能会发生逃逸!
下面咱们来看一下几种垃圾收集算法
一、 在JDK1.2以前,使用的是引用计数器算法,即当这个类被加载到内存之后,就会产生方法区,堆栈、程序计数器等一系列信息,当建立对象的时候,为这个对象在堆栈空间中分配对象,同时会产生一个引用计数器,同时引用计数器+1,当有新的引用的时候,引用计数器继续+1,而当其中一个引用销毁的时候,引用计数器-1,当引用计数器被减为零的时候,标志着这个对象已经没有引用了,能够回收了!这种算法在JDK1.2以前的版本被普遍使用,可是随着业务的发展,很快出现了一个问题
当咱们的代码出现下面的情形时,该算法将没法适应
a) ObjA.obj = ObjB
b) ObjB.obj - ObjA
这样的代码会产生以下引用情形 objA指向objB,而objB又指向objA,这样当其余全部的引用都消失了以后,objA和objB还有一个相互的引用,也就是说两个对象的引用计数器各为1,而实际上这两个对象都已经没有额外的引用,已是垃圾了。
二、 根搜索算法
根搜索算法是从离散数学中的图论引入的,程序把全部的引用关系看做一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点之后,继续寻找这个节点的引用节点,当全部的引用节点寻找完毕以后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。
目前java中可做为GC Root的对象有
一、 虚拟机栈中引用的对象(本地变量表)
二、 方法区中静态属性引用的对象
三、 方法区中常量引用的对象
四、 本地方法栈中引用的对象(Native对象)
说了这么多,其实咱们能够看到,全部的垃圾回收机制都是和引用相关的,那咱们来具体的来看一下引用的分类,到底有哪些类型的引用?每种引用都是作什么的呢?
Java中存在四种引用,每种引用以下:
一、 强引用
只要引用存在,垃圾回收器永远不会回收
Object obj = new Object();
//可直接经过obj取得对应的对象 如obj.equels(new Object());
而这样 obj对象对后面new Object的一个强引用,只有当obj这个引用被释放以后,对象才会被释放掉,这也是咱们常常所用到的编码形式。
二、 软引用
非必须引用,内存溢出以前进行回收,能够经过如下代码实现
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;
sf.get();//有时候会返回null
这时候sf是对obj的一个软引用,经过sf.get()方法能够取到这个对象,固然,当这个对象被标记为须要回收的对象时,则返回null;
软引用主要用户实现相似缓存的功能,在内存足够的状况下直接经过软引用取值,无需从繁忙的真实来源查询数据,提高速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据。
三、 弱引用
第二次垃圾回收时回收,能够经过以下代码实现
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
wf.get();//有时候会返回null
wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾
弱引用是在第二次垃圾回收时回收,短期内经过弱引用取对应的数据,能够取到,当执行过第二次垃圾回收时,将返回null。
弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,能够经过弱引用的isEnQueued方法返回对象是否被垃圾回收器
四、 虚引用(幽灵/幻影引用)
垃圾回收时回收,没法经过引用取到对象值,能够经过以下代码实现
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等,这些类频繁的调用自定义类加载器,都须要动态的加载和卸载了,以保证永久带不会溢出,他们经过自定义的类加载器进行了各项操做,所以在实际的应用开发中,类也是被常常加载和卸载的,方法区也是会被回收的!可是方法区的回收条件很是苛刻,只有同时知足如下三个条件才会被回收!
一、全部实例被回收
二、加载该类的ClassLoader被回收
三、Class对象没法经过任何途径访问(包括反射)
好了,咱们如今切入正题,Java1.2以前主要经过引用计数器来标记是否须要垃圾回收,而1.2以后都使用根搜索算法来收集垃圾,而收集后的垃圾是经过什么算法来回收的呢?
一、 标记-清除算法
二、 复制算法
三、 标记-整理算法
咱们来逐一过一下
一、 标记-清除算法
标记-清除算法采用从根集合进行扫描,对存活的对象对象标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如上图所示。
标记-清除算法不须要进行对象的移动,而且仅对不存活的对象进行处理,在存活对象比较多的状况下极为高效,但因为标记-清除算法直接回收不存活的对象,所以会形成内存碎片!
二、 复制算法
复制算法采用从根集合扫描,并将存活对象复制到一块新的,没有使用过的空间中,这种算法当控件存活的对象比较少时,极为高效,可是带来的成本是须要一块内存交换空间用于进行对象的移动。也就是咱们前面提到的
s0 s1等空间。
三、 标记-整理算法
标记
-整理算法采用标记-清除算法同样的方式进行对象的标记,但在清除时不一样,在回收不存活的对象占用的空间后,会将全部的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,所以成本更高,可是却解决了内存碎片的问题。
咱们知道,JVM为了优化内存的回收,进行了分代回收的方式,对于新生代内存的回收(minor GC)主要采用复制算法,下图展现了minor GC的执行过程。
对于新生代和旧生代,
JVM可以使用不少种垃圾回收器进行垃圾回收,下图展现了不一样生代不通垃圾回收器,其中两个回收器之间有连线表示这两个回收器能够同时使用。
而这些垃圾回收器又分为串行回收方式、并行回收方式合并发回收方式执行,分别运用于不一样的场景。以下图所示
下面咱们来逐一介绍一下每一个垃圾回收器。
一、 Serial收集器
看名字咱们均可以看的出来,这个属于串行收集器。其运行示意图以下
Serial
收集器是历史最悠久的一个回收器,JDK1.3以前普遍使用这个收集器,目前也是ClientVM下 ServerVM 4核4GB如下机器的默认垃圾回收器。串行收集器并非只能使用一个CPU进行收集,而是当JVM须要进行垃圾回收的时候,须要中断全部的用户线程,知道它回收结束为止,所以又号称“Stop The World” 的垃圾回收器。注意,JVM中文名称为java虚拟机,所以它就像一台虚拟的电脑同样在工做,而其中的每个线程就被认为是JVM的一个处理器,所以你们看到图中的CPU0、CPU1实际为用户的线程,而不是真正机器的CPU,你们不要误解哦。
串行回收方式适合低端机器,是Client模式下的默认收集器,对CPU和内存的消耗不高,适合用户交互比较少,后台任务较多的系统。
Serial收集器默认新旧生代的回收器搭配为Serial+ SerialOld
二、 ParNew收集器
ParNew收集器其实就是多线程版本的Serial收集器,其运行示意图以下
一样有
Stop The World的问题,他是多CPU模式下的首选回收器(该回收器在单CPU的环境下回收效率远远低于Serial收集器,因此必定要注意场景哦),也是Server模式下的默认收集器。
三、 ParallelScavenge
ParallelScavenge又被称为是吞吐量优先的收集器,器运行示意图以下
ParallelScavenge
所提到的吞吐量=程序运行时间/(JVM执行回收的时间+程序运行时间),假设程序运行了100分钟,JVM的垃圾回收占用1分钟,那么吞吐量就是99%。在当今网络告诉发达的今天,良好的响应速度是提高用户体验的一个重要指标,多核并行云计算的发展要求程序尽量的使用CPU和内存资源,尽快的计算出最终结果,所以在交互很少的云端,比较适合使用该回收器。
四、 ParallelOld
ParallelOld是老生代并行收集器的一种,使用标记整理算法、是老生代吞吐量优先的一个收集器。这个收集器是JDK1.6以后刚引入的一款收集器,咱们看以前那个图之间的关联关系能够看到,早期没有ParallelOld以前,吞吐量优先的收集器老生代只能使用串行回收收集器,大大的拖累了吞吐量优先的性能,自从JDK1.6以后,才能真正作到较高效率的吞吐量优先。其运行示意图以下
五、
SerialOld
SerialOld是旧生代Client模式下的默认收集器,单线程执行;在JDK1.6以前也是ParallelScvenge回收新生代模式下旧生代的默认收集器,同时也是并发收集器CMS回收失败后的备用收集器。其运行示意图以下
六、
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的回收!
七、 GarbageFirst(G1 )
咱们再来看垃圾回收器的总图,刚才咱们能够看到,我在图上标记了一个?,其实这是一个新的垃圾回收器,既能够回收新生代也能够回收旧生代,SunHotSpot 1.6u14以上EarlyAccess版本加入了这个回收器,sun公司预期SunHotSpot1.7发布正式版,他是商用高性能垃圾回收器,经过从新划份内存区域,整合优化CMS,同时注重吞吐量和响应时间,可是杯具的是被oracle收购以后这个收集器属于商用收费收集器,所以目前基本上没有人使用,咱们在这里也就很少介绍,更多信息能够参考oracle新版本JDK说明。
下面咱们再来看下JVM的一些内存分配与回收策略
一、 优先在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区域
二、 大对象直接进入老生代
代码示例 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日志,而数据是直接进入老生代的
三、 年长者(长期存活对象)进入老生代
代码示例: 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的监控工具能够很清楚的看到对象的年龄,如图所示
右侧的年代数目就是对象的年龄
四、 群体效应(大批中年对象进入老生代)
代码示例 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,所以直接进入老生代
五、 担保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];
}
} 运行结果 一、 无担保
二、 ![]() 有担保 |
结果分析
咱们能够很清楚的看到,当无担保的时候,触发了一次FullGC 而有担保的状况下,只有monorGC则完成了回收,大大提高了效率。
当咱们注释掉对应的代码
// testCase4 = null; // testCase5 = null; // testCase6 = null; |
的时候,就会引起担保失败,以下图所示
JVM
默认状况是是开启担保的,无需设置参数。