垃圾收集算法
分代收集理论
大多数的垃圾收集器都采用此理论,根据存活周期的不同,一般将java堆分为年轻代和老年代,
不同的代可以选择不同的垃圾收集算法。
在年轻代中每次收集有大量的对象消亡,可以采用复制算法,效率比标记整理或标记清除高,只需要付出少量对象的复制成本就可以完成每次收集,老年代的对象存活几率较大,并且没有额外空间对其进行分配担保,所以需要用标记整理或标记清除算法
复制算法
将内存分为大小相同的两块,每次只使用其中的一块,当这块快用完了就将存活的对象复制到另一块上去,将这块使用过的内存空间清除。
标记整理算法
标记出需要回收的对象,让所有存活对象向内存的一边移动,清理掉边界外的内存
标记清除算法
标记出所有需要回收的对象,标完统一回收被标记的对象,也可以标记存活的对象回收未标记的对象,这种算法会产生内存碎片,标记的对象过多会有效率问题。
ParNew收集器(-XX:+UseParNewGC)
采用复制算法
相当于Serial收集器的多线程版本,用多线程进行垃圾收集,收集过程STW,主要搭配CMS,作为年轻代的收集器
CMS收集器(-XX:+UseConcMarkSweepGC)
采用标记清除算法,只能用于老年代
设计的目的是为了获取低回收停顿时间,让垃圾回收线程和用户线程(基本上)同时工作
回收过程
初始标记:
暂停所有其他线程,标记gcroots能直接引用的对象,速度很快
并发标记:
从gcroots直接关联的对象开始遍历整个对象图的过程,这个过程耗时很长但不需要暂停业务线程,由于业务线程在并行运行,会导致已经标记过的对象状态发生改变
重新标记:
为了修正并发标记期间状态发生改变的那一部分对象的标记记录,这个阶段会暂停其它线程比初始标记暂停时间稍长,但远远短于并发标记的时间,主要使用三色标记的增量更新处做重新标记
并发清理:
开启用户线程,垃圾收集线程对未标记的区域做清除。这个阶段如果有新增对象直接标记为黑色
并发重置:
重置GC过程中的标记数据
优点:
并发收集、低停顿
缺点:
对处理器资源非常敏感:
CMS默认启动的回收线程数是(处理器核心数量+3)/4,核心数越低,垃圾收集线程占的处理器运算资源比重越大
无法处理“浮动垃圾”:
由于进行垃圾收集的时候业务线程并发执行,就会产生新的垃圾,只能等到下次GC进行收集,
-XX:CMSInitiatingOccupancyFraction可以设置CMS触发的百分比,JDK6及以后提高到了92%,由于没有额外的空间进行担保,当剩余内存不足以分配新的对象就会导致并发失败(concurrent mode failure)此时会STW,用serial old垃圾收集器来单线程回收
收集结束时会有大量空间碎片产生:
空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。
-XX:+UseCMSCompactAtFullCollection(默认开启,FullGC之后做压缩整理(减少碎片))和
-XX:CMSFullGCsBeforeCompaction (多少次FullGC后压缩一次,默认0,代表每次full gc后都会压缩一次)这两个参数配置
算法底层实现
三色标记
- 白色:表示对象尚未被垃圾收集器访问过。在可达性分析刚开始阶段,所有对象都是属于这一状态,若分析阶段结束,仍是白色,既代表不可达
- 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其它对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能不经过灰色对象直接指向某个白色对象
- 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用没被扫描过
多标(浮动垃圾)
在并发标记过程中,由于局部变量gcroots被销毁,引用的对象之前又被扫描过,只能等下次gc回收,并发标记和并发清理后产生的新对象直接会标记为黑色,等下次gc时状态也可能发生改变,这些都称为浮动垃圾。
漏标(读写屏障)
主要使用增量更新和原始快照STAB解决
增量更新:当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了。
STAB:当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。这也可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索。这样就能扫描到白色的对象,将白色对象直接标记为黑色(目的就是让这种对象在本轮gc清理中能存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃圾)
写屏障
就是指在赋值操作前后,加入一些处理(可以参考AOP的概念)
- 写屏障实现SATB
当对象B的成员变量的引用发生变化时,比如引用消失(a.b.c = null),我们可以利用写屏障,将B原来成员变量的引用对象C记录下来
- 写屏障实现增量更新
当对象A的成员变量的引用发生变化时,比如新增引用(a.c = c),我们可以利用写屏障,将A新的成员变量引用对象C记录下来
记忆集与卡表
在新生代可以引入记录集(Remember Set)的数据结构(记录从非收集区到收集区的指针集合),避免把整个老年代加入GCRoots扫描范围。
垃圾收集场景中,收集器只需通过记忆集判断出某一块非收集区域是否存在指向收集区域的指针即可,无需了解跨代引用指针的全部细节。
卡表
卡表是使用一个字节数组实现:CARD_TABLE[ ],每个元素对应着其标识的内存区域一块特定大小的内存块,称为“卡页”。
hotSpot使用的卡页是2^9大小,即512字节。
一个卡页中可包含多个对象,只要有一个对象的字段存在跨代指针,其对应的卡表的元素标识就变成1,表示该元素变脏,否则为0。
GC时,只要筛选本收集区的卡表中变脏的元素加入GCRoots里。
卡表的维护
卡表变脏上面已经说了,但是需要知道如何让卡表变脏,即发生引用字段赋值时,如何更新卡表对应的标识为1。
Hotspot使用写屏障维护卡表状态。