一、垃圾回收算法
一般对jvm的垃圾回收算法都是基于分代收集思想实现的,基于jvm的堆内存分代的划分,一般根据对象在堆中的存活周期,划分为年轻代和老年代,基于不同代的特点选择最合适的垃圾回收算法,一般有三种算法:
下面仔细分析下各种算法
1.标记-复制
标记-复制算法是通过标记出非垃圾对象(存活对象),并且把存活对象复制到堆中另外一块空白内存中,然后把原来的内存空间全部回收掉。一般用于年轻代
标记复制算法一般把内存分为大小相等的两块,一块用于分配对象,一块作为保留内存空间(暂时不使用)。当使用的那块空间满了之后,经过GC 把存活对象复制到保留内存空间中,然后清空那块内存空间。如此反复。
优点:
1.实现简单,逻辑简单易理解。
2.效率高,在存活对象较少的时候,因为需要复制和标记的对象较少,所以效率很高。因此这个算法一般比较适用于存活对象较少的地方,比如年轻代,因为年轻代几乎都是朝生夕死的对象,每次GC能存活下来的对象占少数,所以适用标记-复制算法效率较高。
缺点:
1.浪费内存空间,因为该算法需要两块内存空间,而且长时间有一块内存空间是不能使用的(只能作为保留内存空间用于复制存活对象时使用)。
2.因为挪动了对象在内存中的位置 所以需要更新地址引用,也是影响效率的。
2.标记-清除
标记-清除算法通过标记出非垃圾对象,然后直接清除未被标记的垃圾对象。(也可以反过来标记垃圾对象 清除标记过的对象 一般不用这种方式)
这种算法也是比较简单的,但是也会带来问题:
缺点:
1.效率问题,标记的对象很多时,影响GC效率
2.碎片空间,GC后会产生大量的空间碎片,可能出现如果来了一个大点对象,无法找到连续的内存分配给它。
优点:
1.因为不用移动存活的对象,不用更新对象的地址引用
2.相比于复制算法,不会浪费内存空间
3.标记-整理
标记整理算法分为三步:标记,清除,整理三部分,其中标记与标记-清除算法一样,只不过在后面稍微有点不同,标记完存活对象后,会把存活对象向一端移动,把垃圾对象向另一端移动,移动完之后,把另一端的垃圾对象全部回收掉。
这种算法可以解决标记-清除算法产生的碎片空间问题
优点:
1.不会产生碎片空间
2.对比复制算法 不会浪费内存空间
缺点:
1.标记对象太多效率不高
2.对象挪动,需要更新对象地址引用
二、垃圾收集器
随着应用对GC效率的要求的越来越高,市场出现了各种各样的垃圾收集器:
1.Serial(-XX:UseSerialGC 年轻代 -XX:UseSerialOldGC 老年代)
是一个串行收集器,使用单线程收集,在进行垃圾收集时,使用单线程收集垃圾并且在回收垃圾时会触发STW(stop the world)暂停java中所有的用户线程,直到收集结束。年轻代:标记-复制算法,老年代:标记-整理算法
在Serial收集器收集时使用单线程收集,所以收集所需要的时间会比较长,导致STW的时间相对较长,但是也是因为单线程这个特性,可以不用进行多线程的切换和交互,保证了单线程的回收效率最高。
Serial Old是Serial老年代的版本,用于老年代的来及收集,而且当CMS(另一个垃圾收集器 下面详解)在并发收集出现问题时,作为后备的收集方案使用。
2.Parallel(-XX:UseParallelGC 年轻代 -XX:UseParallelOldGC 老年代)
Parallel收集器,基于Serial收集器的一个改进,由于Serial收集器使用单线程收集,导致STW时间过长,效率不高,Parallel收集器采用多线程收集,可以在GC时,大幅度减少STW的时间,提升用户的体验。
多线程并发收集,提高了整体的收集效率。年轻代采用标记-复制算法,老年代采用标记-整理算法。
Parallel Old是老年代版本,用于老年代的收集。JDK8默认的年轻代和老年代的收集器。
3.ParNew(-XX:UseParNewGC 年轻代)
ParNew和Parallel收集器类似,唯一的区别在于ParNew可以配合CMS(老年代的一款收集器)收集器使用,而Parallel不可以。
ParNew只用于年轻代的收集,不可用于老年代,所以往往配合CMS一起使用。
使用标记-复制算法。
4.CMS(-XX:UseConcMarkSweepGC 老年代)
CMS收集器,全称 Concurrent Mark Sweep 是一款真正意义上的并发收集器,为了解决垃圾收集时停止java用户线程时间过长导致的槽糕的用户体验,CMS采用分段多次+并发标记的思想,使得一次长时间的STW分为多段短时间的STW,这可以有效的缓解一次STW时间很长的缺点。基本上实现了让垃圾回收线程与应用线程同时工作。
CMS收集主要分为五个步骤:
- 初始标记:暂停当前所用应用线程,然后从GC Roots开始标记,本次标记只标记直接引用的对象,效率高,速度很快。
- 并发标记:这个阶段就是开始从GC Roots开始标记整个对象引用链路,不在是直接引用了,耗时较长,效率不高,但是并不用停止用户线程,对用户影响不大。但是并发标记会导致,标记过程中 对象的状态发生改变,例如 GC线程刚标记完一个对象为非垃圾对象,此时用户线程删除了对象的引用,导致其成为垃圾对象了,反之亦然,这就会存在并发标记的不确定性。
- 重新标记:这个阶段是需要停止所有用户线程的,然后对于上一个阶段标记的结果进行修正,也就是对那些状态发生变化的对象重新标记。这个阶段的STW时间比初始标记要长,但是远远低于并发标记的耗时。这一阶段主要使用了三色标记中的增量更新算法进行重新标记。
- 并发清理:这个阶段主要对未标记的垃圾对象进行并发清理的,不用停止用户线程,所以对象用户体验影响不大。但是并发清理也会存在对象状态在清理期间发生变化的情况,主要分类两种情况:漏标和多标(下面会对此说明)。在并发清理的阶段如果产生了新的对象,该对象被直接标记为黑色,这样就不会多回收存活的对象的问题了。
- 并发重置:重置本次GC过程中的标记数据。
从这几个阶段我们很明显感受到和其他几款收集器的不同,
优点:可以并发收集,用户线程停顿时间(STW)低。
缺点也很明显:
1.对CPU资源敏感,可能会和用户线程抢夺资源。
2.在并发标记和清理时,对象状态发生变化,非垃圾对象变为垃圾对象,而我们确标记了,本次未回收掉,产生浮动垃圾。
3.它使用的是标记-清除算法,因此会产生大量的碎片空间。不过可以通过设置参数(-XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理)减少碎片。
4.在本次GC还未完成时,由于用户线程继续运行产生新的垃圾,导致又一次触发full GC了,本次回收失败(concurrent mode failure)那么CMS只有停止所有用户线程 ,然后使用Serial Old收集器进行收集。因此Serial Old收集还有作为CMS预备收集器的作用。一旦产生这种情况,并发收集就转为单线程收集,回收效率低下,停顿时间长,所以尽量要避免发生这样的情况。可以通过设置-XX:CMSInitiatingOccupancyFraction(当老年代达到这个值是触发CMS回收)参数尽可能的避免发生这中情况。
CMS的核心参数:
- -XX:+UseConcMarkSweepGC:启用cms
- -XX:ConcGCThreads:并发的GC线程数
- -XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)
- -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一 次
- -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)
- -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设 定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整
- -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,目的在于减少老年代对年轻代的引 用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在标记阶段
- -XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW
- -XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW;
扩展:三色标记法
并发阶段 因为有用户线程的运行,可能会导致之前标记的存活对象变成了垃圾对象,在回收时,这个对象还是被标记为存活对象,所以CMS并不会回收它(其实此时它已经是垃圾对象了),这种垃圾对象就是浮动垃圾。对于这种情况CMS并不会去处理它,而是等待下一次的GC在清理。
漏标——可能导致回收存活的对象(CMS对此有两种解决方法)
场景:并发清理时,有心的对象产生,此时新产生的对象并没有被标记,在回收时会回收掉所有未被标记的对象(新生对象也在此列),这就是漏标会导致的问题。
两种解决方案:增量更新(Incremental Update) 和原始快照(Snapshot At The Beginning,SATB)