垃圾收集算法
分代收集理论
当前虚拟机的垃圾收集都是采用分代收集算法,这种算法没有什么新思想,只是依据对象的存活周期不同将内存分为几块.一般将Java堆分为新生代和老年代,这样就可以根据各个年代的特点选择合适的垃圾收集算法.
比如在新生代中,每次收集都会有大量对象(近99%)死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集.而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择"标记-清除"或"标记-整理"算法进行垃圾收集.注意:“标记-清除"或"标记-整理算法会比复制算法慢10倍以上”.
标记-复制算法
为了解决效率问题,"复制"收集算法出现了.它可以将内存分为大小相同的两块,每次使用其中的一块.当这一块内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉.这样就每次的内存回收都是对内存区间的一半进行回收.
标记-清除算法
算法分为"标记"和"清除"阶段;标记存活的对象,统一回收所有未被标记的对象(一般选择这种);也可以反过来,标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象.它是最基础的收集算法,比较简单,但是会带来两个明显的问题:
- 效率问题:如果需要标记的对象太多,效率不高
- 空间问题:标记清除后会产生大量不连续的碎片
标记-整理算法
根据老年代的特点特出的一种标记算法,标记过程仍然与"标记-清除"算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存.
垃圾收集器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现.
虽然我们对各个收集器进行比较,但并非为了挑选出一个最好的收集器.因为直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体的应用场景选择适合自己的垃圾收集器.试想一下:如果有一种四海之内,任何场景下都适用的完美收集器存在,那Java虚拟机就不会实现那么多不同的垃圾收集器了.
Serial收集器
-XX:+UseSerialGC -XX:+UseSerialOldGC
Serial(串行)收集器是最基本,历史最悠久的垃圾收集器了.看名字就知道这个收集器是一个单线程收集器.它的"单线程"的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程(“Stop The World”),直到它收集结束.
新生代采用复制算法,老年代采用标记-整理算法
虚拟机的设计者当然直到STW带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍在继续).
但是Serial收集器有没有优于其他垃圾收集器的地方呢?
当然有,它 简单而高效(与其他垃圾收集器的单线程相比).Serial收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率.
Serial Old收集器是Serial收集器的老年代版本,它同样是一个单线程收集器,主要有以下两种用途:
- 在JDK1.5及以前的版本中与Parallel Scavenge收集器搭配使用
- 作为CMS收集器的后备方案
Parallel Scavenge收集器
年轻代:-XX:UseParallelGC
老年代:-XX:UseParallelOldGC
Parallel 收集器其实就是 Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数,收集算法,回收策略等等)和Serial收集器类似.默认的收集线程与CPU核数相同,当然也可以用参数(-XX:ParallelGCThreads
)指定收集线程数,但一般不推荐修改.
Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU).CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验).所谓吞吐量就是CPU中用于运行用户代码的时间和CPU总消耗时间的比值.Parallel Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对收集器运作不太了解的话,可以选择把内存管理优化交给虚拟机负责.
新生代采用复制算法,老年代采用标记-整理算法.
Parallel Old收集器是Parallel Scavenge收集器的老年代版本.使用多线程和"标记-整理"算法.在注重吞吐量以及CPU资源的场合,都可以优先考虑Parallel Scavenge收集器和Parallel Old收集器(JDK8默认的新生代和老年代收集器).
ParNew收集器
-XX:+UseParNewGC
ParNew收集器其实跟 Parallel收集器很类似,区别主要在于它可以和CMS收集器配合使用.
新生代采用复制算法
它是许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,只有它能与CMS收集器配合工作
CMS收集器
-XX:+UseConcMarkSweepGC
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器.它非常符合在注重用户体验的应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作.
从名字中 Mark Sweep 这两个词可以看出,CMS收集器是一种 标记-清除 算法实现的,它的运作过程相比于前面集中垃圾收集器来说更加复杂一些.整个过程分为四个步骤:
- 初始标记:暂停所有的其他线程(STW),并记录下GC Roots 直接能引用的对象,速度很快.
- 并发标记:并发标记就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行.因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变
- 重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运作而导致标记产生变动的那一部分对象的标记记录(主要是处理漏标问题),这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短.主要用到三色标记里的增量更新算法做重新标记
- 并发重置:重置本次GC过程中的标记数据
从它的名字就可以看出它是一款优秀的垃圾收集器,主要有点:**并发收集,低停顿.**但是它有下面几个明显的缺点:
- 对CPU资源敏感(会和应用程序抢资源)
- 无法处理 浮动垃圾 (在并发标记和并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次GC再清理了)
- 使用的回收算法"标记-清除"算法会导致收集结束时会有 大量的空间碎片产生,当然通过参数
-XX:+UseCMSCompactAtFullCollection
让JVM在执行完标记清除后在做整理 - 执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并发标记和并发清理阶段会出现,一边回收,系统一边运行,也许还没回收完就再次触发Full GC,也就是"concurrent mode failure",此时会进入STW,用Serial Old垃圾收集器来回收
CMS相关核心参数
-XX:+UseConcMarkSweepGC
:启用CMS-XX:ConcGCThreads
:并发的GC线程数-XX:UseCMSCompactAtFullConllection
:FullGC之后做压缩整理(减少碎片)-XX:CMSFullGCBeforeCompaction
:多少次FullGC之后压缩一次,默认是0,代表每次FullGC都会压缩一次-XX:CMSInitiatingOccupancyFraction
:当老年代使用达到该比例会触发Full GC(默认92.当老年代使用空间达到92%以后触发Full GC)-XX:+UseCMSInitiatingOccupancyOnly
:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction
设定的值),如果不指定,JVM仅在第一次使用设定值,后续会动态调整-XX:+CMSScavengeBeforeRemark
:在CMS GC前启动一次Minor GC,降低CMS GC标记阶段(也会对年轻代一起做标记,如果在Minor GC就清除了很多垃圾对象,标记阶段就会减少一些标记时间)时的开销,一般CMS的GC耗时80%都在标记阶段-XX:+CMSParallellnitialMarkEnabled
:表示在初始标记阶段多线程执行,缩短STW-XX:+CMSParallelRemarkEnabled
:在重新标记阶段多线程执行,缩短STW
垃圾收集算法底层实现
三色标记
在并发标记的过程中,因为标记期间应用线程还在继续跑,对象间的引用可能发生变化,多标和漏标的情况就有可能发生.漏标的问题主要引入了三色标记算法来解决.
三色标记算法是把GC Roots可达性分析遍历对象过程中遇到的对象,按照"是否访问过"这个条件标记成以下三种颜色:
- 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过,黑色的对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍.黑色对象不可能直接(不经过灰色对象)指向某个白色对象.
- 灰色:表示对象已经被垃圾收集器访问过,但这个对象至少存在一个引用还没有被扫描过.
- 白色:表示对象尚未被垃圾收集器访问过.显示在可达性分析刚刚开始的阶段,所有对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达
public class ThreeColorRemarkDemo {
public static void main(String[] args) {
A a = new A();
// 开始做并发标记
D d = a.b.d; // 读
a.b.d = null; // 写
a.d = d; // 写
}
}
class A{
B b = new B();
D d = null;
}
class B{
C c = new C();
D d = new D();
}
class C{}
class D{}
漏标问题复现:
假设A a = new A();后开始做并发标记,从a指向A.从A执行B.从B指向C,此时将A和C记为黑色.B由于还没有扫描到D记为灰色.
a.b.d = null;将B和D之间的引用给干掉了.
在并发标记的过程中,应用线程是可以正常执行的.代码此时将a.d = d;但是由于A是黑色.在后面重新标记的过程中是不会扫描黑色的就会出现漏标的问题.
多标-浮动垃圾
在并发标记过程中,如果由于方法运行结束导致部分局部变量(GC Roots)被销毁,这个GC Roots引用的对象之前又被扫描过(被标记为非垃圾对象).那么本轮GC不会回收这部分内存,这部分本该回收但是没有回收的内存,被称之为"浮动垃圾",浮动垃圾并不会影响垃圾回收的正确性,只是需要等到下一轮回收中才被清除.
另外,针对并发标记(还有并发清理)开始后产生的新对象,通常做法是直接全部当成黑色,本轮不会进行清除.这部分对象期间可能也会变成垃圾.这也算是浮动垃圾的一部分.
漏标-读写屏障
漏标会导致被引用的对象被当成垃圾误删除,这是严重bug,必须解决,有以下两种解决方案:
- 增量更新(Incremental Update):当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次.这可以简化理解为:黑色对象一旦插入了指向白色对象的引用之后,它就变回灰色对象了.
- 原始快照(Snapshot At The Beginning,SATB):当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,再并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,再重新扫描一次.这样就能扫描到白色的对象,将白色对象直接标记为黑色(目的就是让这种对象再本轮gc清理中能存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃圾)
以上无论是对引用关系记录的插入还是删除,虚拟机的记录操作都是通过 写屏障 实现的.