一、垃圾回收机制的基本概念
1.什么是垃圾回收?
Garbage Collection 垃圾收集。这里所谓的垃圾指的是在系统运行过程当中所产生的一些无用的对象,这些对象占据着一定的内存空间,如果长期不被释放,可能导致OOM(堆溢出)。
2.JVM GC回收哪个区域内的垃圾?
内存区域中的程序计数器、虚拟机栈、本地方法栈这3个区域是线程独占区域,随着线程而生,线程而灭;方法结束或者线程结束时,内存会被自动释放,因此不需要考虑回收的问题。
而Java堆和方法区则不同,是线程共享区域,一个接口中的多个实现类需要的内存可能不同,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,GC关注的也是这部分内存,如果涉及到“内存”分配与回收也仅指着一部分内存。
3.Java中的对象引用
(1)强引用(Strong Reference):如“Object obj = new Object()”,这类引用是Java程序中最普遍的。只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。
(2)软引用(Soft Reference):它用来描述一些可能还有用,但并非必须的对象。在系统内存不够用时,这类引用关联的对象将被垃圾收集器回收。JDK1.2之后提供了SoftReference类来实现软引用。
(3)弱引用(Weak Reference):它也是用来描述非须对象的,但它的强度比软引用更弱些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2之后,提供了WeakReference类来实现弱引用。
(4)虚引用(Phantom Reference):最弱的一种引用关系,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的是希望能在这个对象被收集器回收时收到一个系统通知。JDK1.2之后提供了PhantomReference类来实现虚引用。
二、判断对象是否是垃圾的算法
引用计数算法:(老牌垃圾回收算法。无法处理循环引用,没有被Java采纳)
1.引用计数算法的概念:
引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象实例都有一个引用计数。当一个对象被创建时,且将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。
2.优缺点
优点:
- 引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。
缺点:
引用和去引用伴随加法和减法,影响性能
无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0。
上面的3个图中,对于最右边的那张图而言:循环引用的计数器都不为0,但是他们对于根对象都已经不可达了,但是无法释放。
引用计数算法无法解决循环引用问题,例如:
public class Object {
Object field = null;
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
public void run() {
Object objectA = new Object();
Object objectB = new Object();//位置1
objectA.field = objectB;
objectB.field = objectA;//位置2
//to do something
objectA = null;
objectB = null;//位置3
}
});
thread.start();
while (true);
}
代码解释:
代码中标注了1、2、3三个数字,当位置1的语句执行完以后,两个对象的引用计数全部为1。当位置2的语句执行完以后,两个对象的引用计数就全部变成了2。当位置3的语句执行完以后,也就是将二者全部归为空值以后,二者的引用计数仍然为1。根据引用计数算法的回收规则,引用计数没有归0的时候是不会被回收的。
对于我们现在使用的GC来说,当thread线程运行结束后,会将objectA和objectB全部作为待回收的对象。而如果我们的GC采用上面所说的引用计数算法,则这两个对象永远不会被回收,即便我们在使用后显示的将对象归为空值也毫无作用。
根搜索算法
1.根搜索算法的概念:
首先了解一个概念:根集(Root Set)
所谓根集(Root Set)就是正在执行的Java程序可以访问的引用变量(注意:不是对象)的集合(包括局部变量、参数、类变量),程序可以使用引用变量访问对象的属性和调用对象的方法。
这种算法的基本思路:
(1)通过一系列名为“GC Roots”的对象作为起始点,寻找对应的引用节点。
(2)找到这些引用节点后,从这些节点开始向下继续寻找它们的引用节点。
(3)重复(2)。
(4)搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,就证明此对象是不可用的。
Java和C#中都是采用根搜索算法来判定对象是否存活的。
2.标记可达对象:
JVM中用到的所有现代GC算法在回收前都会先找出所有仍存活的对象。根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图。下图3.0中所展示的JVM中的内存布局可以用来很好地阐释这一概念:
首先,垃圾回收器将某些特殊的对象定义为GC根对象。所谓的GC根对象包括:
(1)虚拟机栈中引用的对象(栈帧中的本地变量表);
(2)方法区中的常量引用的对象;
(3)方法区中的类静态属性引用的对象;
(4)本地方法栈中JNI(Native方法)的引用对象。
(5)活跃线程。
接下来,垃圾回收器会对内存中的整个对象图进行遍历,它先从GC根对象开始,然后是根对象引用的其它对象,比如实例变量。回收器将访问到的所有对象都标记为存活。
存活对象在上图中被标记为蓝色。当标记阶段完成了之后,所有的存活对象都已经被标记完了。其它的那些(上图中灰色的那些)也就是GC根对象不可达的对象,也就是说你的应用不会再用到它们了。这些就是垃圾对象,回收器将会在接下来的阶段中清除它们。
关于标记阶段有几个关键点是值得注意的:
1.开始进行标记前,会触发一次Stop The World(STW)暂停,会暂停全部线程,否则如果对象图一直在变化的话是无法真正去遍历它的。
2.暂停时间的长短并不取决于堆内对象的多少也不是堆的大小,而是存活对象的多少。因此,调高堆的大小并不会影响到标记阶段的时间长短。
3.在根搜索算法中,要真正宣告一个对象死亡,至少要经历两次标记过程:
如果对象在进行根搜索后发现没有与GC Roots相连接的引用链,那它会被第一次标记并且进行一次筛选。筛选的条件是此对象是否有必要执行 finalize()方法(可看作析构函数,类似于OC中的dealloc,Swift中的deinit)。当对象没有覆盖finalize()方法,或finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。
如果该对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为F-Queue队列中,并在稍后由一条由虚拟机自动建立的、低优先级的Finalizer线程去执行finalize()方法。finalize()方法是对象逃脱死亡命运的最后一次机会(因为一个对象的finalize()方法最多只会被系统自动调用一次),稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果要在finalize()方法中成功拯救自己,只要在finalize()方法中让该对象重新引用链上的任何一个对象建立关联即可。而如果对象这时还没有关联到任何链上的引用,那它就会被回收掉。
三、回收垃圾对象内存的算法
在根搜索算法的基础上,现代虚拟机的实现当中,垃圾搜集的算法主要有三种,分别是标记-清除算法、复制算法、标记-整理(标记-清除-整理)算法。这三种算法都扩充了根搜索算法,不过它们理解起来还是非常好理解的。
复制算法(新生代的GC)
1.复制算法的概念:
将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
复制算法因为会将存货对象全部复制,如果执行较多的复制操作,效率将会变低,因此复制算法适合对象存活率较低的新生代
一种典型的基于Coping算法的垃圾回收是stop-and-copy算法,它将堆分成对象区和空闲区,在对象区与空闲区的切换过程中,程序暂停执行。
2.标记-清除算法的缺点:
优点:
标记阶段和复制阶段可以同时进行。
每次只对一块内存进行回收,运行高效。
只需移动栈顶指针,按顺序分配内存即可,实现简单。
内存回收时不用考虑内存碎片的出现(得活动对象所占的内存空间之间没有空闲间隔)。
缺点:
- 需要一块能容纳下所有存活对象的额外的内存空间。因此,可一次性分配的最大内存缩小了一半。
标记-清除算法
1.标记清除算法的概念:
标记-清除算法是现代垃圾回收算法的思想基础。
标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。首先在标记阶段通过前面的根搜索算法来标记可达对象。因此,未被标记的对象就是未被引用的垃圾对象;然后,在清除阶段,清除所有未被标记的对象。
2.标记-清除算法详解:
它的做法是当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被成为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。
标记:标记的过程其实就是,遍历所有的GC Roots,然后将所有GC Roots可达的对象标记为存活的对象。
清除:清除的过程将遍历堆中所有的对象,将没有标记的对象全部清除掉。
也就是说,就是当程序运行期间,若可以使用的内存被耗尽的时候,GC线程就会被触发并将程序暂停,随后将依旧存活的对象标记一遍,最终再将堆中所有没被标记的对象全部清除掉,接下来便让程序恢复运行。
3.标记-清除算法的优缺点:
优点:
- 不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效。
缺点:
首先,它的缺点就是效率比较低(递归与全堆对象遍历),导致stop the world的时间比较长,尤其对于交互式的应用程序来说简直是无法接受。试想一下,如果你玩一个网站,这个网站一个小时就挂五分钟,你还玩吗?
第二点主要的缺点,则是这种方式清理出来的空闲内存是不连续的,这点不难理解,我们的死亡对象都是随即的出现在内存的各个角落的,现在把它们清除之后,内存的布局自然会乱七八糟。而为了应付这一点,JVM就不得不维持一个内存的空闲列表,这又是一种开销。而且在分配数组对象的时候,寻找连续的内存空间会不太好找。
标记—整理算法
1.标记整理算法的概念:
标记-整理算法采用 标记-清除 算法一样的方式进行对象的标记、清除,但在回收不存活的对象占用的空间后,会将所有存活的对象往左端空闲空间移动,并更新对应的指针。标记-整理 算法是在标记-清除 算法之上,又进行了对象的移动排序整理,因此成本更高,但却解决了内存碎片的问题。
2.标记-清除算法的优缺点:
标记/整理算法不仅可以弥补标记/清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价。
但是,标记/整理算法唯一的缺点就是效率也不高。
不仅要标记所有存活对象,还要整理所有存活对象的引用地址。从效率上来说,标记/整理算法要低于复制算法。
分代收集算法:(新生代的GC+老年代的GC)
JVM为了优化内存的回收,使用了分代回收的方式,对于新生代内存的回收(Minor GC)主要采用复制算法。而对于老年代的回收(Major GC),大多采用标记-整理算法。
1.标记-清除算法、复制算法、标记整理算法的总结:
三个算法都基于根搜索算法去判断一个对象是否应该被回收,而支撑根搜索算法可以正常工作的理论依据,就是语法中变量作用域的相关内容。因此,要想防止内存泄露,最根本的办法就是掌握好变量作用域,而不应该使用C/C++式内存管理方式。
在GC线程开启时,或者说GC过程开始时,它们都要暂停应用程序(stop the world)。
它们的区别如下:(>表示前者要优于后者,=表示两者效果一样)
(1)效率:复制算法 > 标记/整理算法 > 标记/清除算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。
(2)内存整齐度:复制算法=标记/整理算法>标记/清除算法。
(3)内存利用率:标记/整理算法=标记/清除算法>复制算法。
注意事项:
可以看到标记/清除算法是比较落后的算法了,但是后两种算法却是在此基础上建立的。
时间与空间不可兼得。
当前商业虚拟机的GC都是采用的“分代收集算法”,这并不是什么新的思想,只是根据对象的存活周期的不同将内存划分为几块儿,根据上面三种算法的选取应用。一般是把Java堆分为新生代和老年代:短命对象归为新生代,长命对象归为老年代。
少量对象存活,适合复制算法:在新生代中,每次GC时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成GC。
大量对象存活,适合用标记-清理/标记-整理:在老年代中,因为对象存活率高、没有额外空间对他进行分配担保,就必须使用“标记-清理”/“标记-整理”算法进行GC。
注:老年代的对象中,有一小部分是因为在新生代回收时,老年代做担保,进来的对象;绝大部分对象是因为很多次GC都没有被回收掉而进入老年代。
2.下边将会讲解它们如何工作的
第一次GC:
在不断创建对象的过程中,当Eden区域被占满,此时会开始做Young GC也叫Minor GC
1)第一次GC时Survivous中S0区和S1区都为空,将其中一个作为To Survivous(用来存储Eden区域执行GC后不能被回收的对象)。比如:将S0作为To Survivous,则S1为From Survivous。
2)将Eden区域经过GC不能被回收的对象存储到To Survivous(S0)区域(此时Eden区域的内存会在垃圾回收的过程中全部释放),但如果To Survivous(S0)被占满了,Eden中剩下不能被回收对象只能存放到Old区域。
3)将Eden区域空间清空,此时From Survivous区域(S1)也是空的。
4)S0与S1互相切换标签,S0为From Survivous,S1为To Survivous。
第二次GC:
当第二次Eden区域被占满时,此时开始做GC
1)将Eden和From Survivous(S0)中经过GC未被回收的对象迁移到To Survivous(S1),如果To Survious(S1)区放不下,将剩下的不能回收对象放入Old区域;
2)将Eden区域空间和From Survivous(S0)区域空间清空;
3)S0与S1互相切换标签,S0为To Survivous,S1为From Survivous。
第三次,第四次一次类推,始终保证S0和S1有一个空的,用来存储临时对象,用于交换空间的目的。反反复复多次没有被淘汰的对象,将会被放入Old区域中,默认15次(由参数--XX:MaxTenuringThreshold=15 决定)。
什么情况下对象会从新生代进入老年代?
- 一定次数的minor gc后
常规对象被创建之后是存储在年轻代的Eden区,每一个对象都有年龄,在YGC后,survivor1区还存活的对象的年龄全部+1,当对象年龄达到15时,被移交到老年代,15是系统默认的,我们可以通过JVM参数-XX:MaxTenuringThreshold来设置
- minor gc后survivor放不下
在MinorGC之后存活的对象超过了survivor区的大小,会将这些对象直接转移到老年代
- 动态对象年龄判断
如果再survivor区,有某一年龄的对象的总大小超过了survivor区大小的50%,年龄大于或等于该年龄的对象就可以直接进入老年代
- 大对象
所谓大对象就是指需要比较大的连续内存空间的Java对象,比如很长的字符串或数组,我们可以通过JVM参数-XX:PretenureSizeThreshold指定大对象的容量,单位是字节。
详情参考:https://www.pianshen.com/article/24491623364/
四、垃圾回收的类型
Minor GC、Major GC、Full GC、Mixed GC
1.Minor GC:
Minor GC是发生在新生代中的垃圾收集动作,采用的是复制算法。
对象在Eden和From区出生后,在经过一次Minor GC后,如果对象还存活,并且能够被to区所容纳,那么在使用复制算法时这些存活对象就会被复制到to区域,然后清理掉Eden区和from区,并将这些对象的年龄设置为1,以后对象在Survivor区每熬过一次Minor GC,就将对象的年龄+1,当对象的年龄达到某个值时(默认是15岁,可以通过参数 --XX:MaxTenuringThreshold设置),这些对象就会成为老年代。
但这也是不一定的,对于一些较大的对象(即需要分配一块较大的连续内存空间)则是直接进入老年代
2.Major GC:
CMS收集器中,当老年代满时会触发 Major GC。
目前,只有CMS收集器会有单独收集老年代的行为。其他收集器均无此行为。
3.Full GC
Full GC 对收集整堆(新生代、老年代)和方法区的垃圾收集。
当年老代满时会引发Full GC,Full GC将会同时回收新生代、年老代 ;
当永久代满时也会引发Full GC,会导致Class、Method元信息的卸载 。
调用System.gc时,系统建议执行Full GC,但是不一定会执行 。
老年代空间不足
方法区空间不足
通过 Minor GC 后进入老年代的空间大于老年代的可用内存
由Eden区、survivor space1(From Space)区向survivor space2(To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小 。
4.Mixed GC
混合GC收集整个新生代 和部分老年代的垃圾手机(目前仅G1 GC 收集器支持)
总结
Minor GC 是 清理 新生代中的Eden区,Survivor区满时不会触发 ;
Major GC 是 清理 老年代 ;
Full GC 是 清理整个堆和方法区,包括 年轻代、老年代和方法区。
都会 触发STW
五、垃圾回收器简介
需要注意的是,每一个回收器都存在Stop The World 的问题,只不过各个回收器在Stop The World 时间优化程度、算法的不同,可根据自身需求选择适合的回收器。
1.Serial(-XX:+UseSerialGC)
从名字我们可以看出,这是一个串行收集器。
Serial收集器是Java虚拟机中最基本、历史最悠久的收集器。在JDK1.3之前是Java虚拟机新生代收集器的唯一选择。目前也是ClientVM下ServerVM 4核4GB以下机器默认垃圾回收器。Serial收集器并不是只能使用一个CPU进行收集,而是当JVM需要进行垃圾回收的时候,需暂停所有的用户线程,直到回收结束。
使用算法:复制算法
JVM中文名称为Java虚拟机,因此它像一台虚拟的电脑在工作,而其中的每一个线程都被认为是JVM的一个处理器,因此图中的CPU0、CPU1实际上为用户的线程,而不是真正的机器CPU,不要误解哦。
Serial收集器虽然是最老的,但是它对于限定单个CPU的环境来说,由于没有线程交互的开销,专心做垃圾收集,所以它在这种情况下是相对于其他收集器中最高效的。
2.SerialOld(-XX:+UseSerialGC)**
SerialOld是Serial收集器的老年代收集器版本,它同样是一个单线程收集器,这个收集器目前主要用于Client模式下使用。如果在Server模式下,它主要还有两大用途:一个是在JDK1.5及之前的版本中与Parallel Scavenge收集器搭配使用,另外一个就是作为CMS收集器的后备预案,如果CMS出现Concurrent Mode Failure,则SerialOld将作为后备收集器。
使用算法:标记 - 整理算法
运行示意图与上图一致。
3.ParNew(-XX:+UseParNewGC)
ParNew其实就是Serial收集器的多线程版本。除了Serial收集器外,只有它能与CMS收集器配合工作。
使用算法:复制算法
ParNew是许多运行在Server模式下的JVM首选的新生代收集器。但是在单CPU的情况下,它的效率远远低于Serial收集器,所以一定要注意使用场景。
4.ParallelScavenge(-XX:+UseParallelGC)(JDK8默认)
ParallelScavenge又被称为吞吐量优先收集器,和ParNew 收集器类似,是一个新生代收集器。
使用算法:复制算法
ParallelScavenge收集器的目标是达到一个可控件的吞吐量,所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。如果虚拟机总共运行了100分钟,其中垃圾收集花了1分钟,那么吞吐量就是99% 。
5.ParallelOld(-XX:+UseParallelOldGC)(JDK8默认)
ParallelOld是并行收集器,和SerialOld一样,ParallelOld是一个老年代收集器,是老年代吞吐量优先的一个收集器。这个收集器在JDK1.6之后才开始提供的,在此之前,ParallelScavenge只能选择SerialOld来作为其老年代的收集器,这严重拖累了ParallelScavenge整体的速度。而ParallelOld的出现后,“吞吐量优先”收集器才名副其实!
使用算法:标记 - 整理算法
在注重吞吐量与CPU数量大于1的情况下,都可以优先考虑ParallelScavenge + ParalleloOld收集器。
6.CMS (-XX:+UseConcMarkSweepGC)
CMS是一个老年代收集器,全称 Concurrent Low Pause Collector,是JDK1.4后期开始引用的新GC收集器,在JDK1.5、1.6中得到了进一步的改进。它是对于响应时间的重要性需求大于吞吐量要求的收集器。对于要求服务器响应速度高的情况下,使用CMS非常合适。
CMS的一大特点,就是用两次短暂的暂停来代替串行或并行标记整理算法时候的长暂停。
使用算法:标记 - 清理
CMS的执行过程如下:
- 初始标记(STW initial mark)
在这个阶段,需要虚拟机停顿正在执行的应用线程,官方的叫法STW(Stop Tow World)。这个过程从根对象扫描直接关联的对象,并作标记。这个过程会很快的完成。
- 并发标记(Concurrent marking)
这个阶段紧随初始标记阶段,在“初始标记”的基础上继续向下追溯标记。注意这里是并发标记,表示用户线程可以和GC线程一起并发执行,这个阶段不会暂停用户的线程哦。
- 并发预清理(Concurrent precleaning)
这个阶段任然是并发的,JVM查找正在执行“并发标记”阶段时候进入老年代的对象(可能这时会有对象从新生代晋升到老年代,或被分配到老年代)。通过重新扫描,减少在一个阶段“重新标记”的工作,因为下一阶段会STW。
- 重新标记(STW remark)
这个阶段会再次暂停正在执行的应用线程,重新重根对象开始查找并标记并发阶段遗漏的对象(在并发标记阶段结束后对象状态的更新导致),并处理对象关联。这一次耗时会比“初始标记”更长,并且这个阶段可以并行标记。
- 并发清理(Concurrent sweeping)
这个阶段是并发的,应用线程和GC清除线程可以一起并发执行。
- 并发重置(Concurrent reset)
这个阶段任然是并发的,重置CMS收集器的数据结构,等待下一次垃圾回收。
CMS的缺点:
1.内存碎片。由于使用了 标记-清理 算法,导致内存空间中会产生内存碎片。不过CMS收集器做了一些小的优化,就是把未分配的空间汇总成一个列表,当有JVM需要分配内存空间的时候,会搜索这个列表找到符合条件的空间来存储这个对象。但是内存碎片的问题依然存在,如果一个对象需要3块连续的空间来存储,因为内存碎片的原因,寻找不到这样的空间,就会导致Full GC。
2.需要更多的CPU资源。由于使用了并发处理,很多情况下都是GC线程和应用线程并发执行的,这样就需要占用更多的CPU资源,也是牺牲了一定吞吐量的原因。
3.需要更大的堆空间。因为CMS标记阶段应用程序的线程还是执行的,那么就会有堆空间继续分配的问题,为了保障CMS在回收堆空间之前还有空间分配给新加入的对象,必须预留一部分空间。CMS默认在老年代空间使用68%时候启动垃圾回收。可以通过-XX:CMSinitiatingOccupancyFraction=n来设置这个阀值。
7.GarbageFirst(G1)
这是一个新的垃圾回收器,既可以回收新生代也可以回收老年代,SunHotSpot1.6u14以上EarlyAccess版本加入了这个回收器,Sun公司预期SunHotSpot1.7发布正式版本。通过重新划分内存区域,整合优化CMS,同时注重吞吐量和响应时间。杯具的是Oracle收购这个收集器之后将其用于商用收费版收集器。因此目前暂时没有发现哪个公司使用它,这个放在之后再去研究吧。
整理一下新生代和老年代的收集器。
新生代收集器:
Serial (-XX:+UseSerialGC)
ParNew(-XX:+UseParNewGC)
ParallelScavenge(-XX:+UseParallelGC)
G1 收集器
老年代收集器:
SerialOld(-XX:+UseSerialOldGC)
ParallelOld(-XX:+UseParallelOldGC)
CMS(-XX:+UseConcMarkSweepGC)
G1 收集器
目前了解的GC收集器就是这么多了,如果有哪位有缘的朋友看到了这篇文章,恰好有更好的GC收集器推荐,欢迎留言交流。