JVM常见垃圾收集器与底层三色标记算法

74 阅读7分钟

垃圾收集算法

image.png

分代收集理论

根据各个年代的特点选择合适的垃圾收集算法。

标记复制算法

image.png

标记清除算法

算法分为"标记"和"清除"阶段,它会带来两个很明显的问题:

  1. 效率问题(如果需要标记的对象太多,效率不高)
  2. 空间问题(标记清除后会产生大量不连续的碎片)

image.png

标记整理算法

标记过程一样,但后续步骤不是直接对可回收对象回收,而是让存活对象向一端移动,然后直接清理掉端边界以外的内存。

image.png

垃圾收集器

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。 image.png

Serial收集器(-XX:+UseSerialGC -XX:+UseSerialOldGC)

新生代采用复制算法,老年代采用标记整理算法,它仅适用于单核情况。 image.png

Parallel Scavenge收集器(-XX:+UseParallelGC -XX:+UseParallelOldGC)

Serial收集器的多线程版本,默认收集线程和cpu核数相同(JDK8默认)。 image.png

ParNew收集器(-XX:+UseParNewGC)

和Parallel主要区别是可以和CMS收集器配合使用。(ParNew+CMS为公司常用的主流组合)

CMS收集器(-XX:+UseConcMarkSweepGC(old))

它是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

它是基于标记清除算法实现的,整个过程分为以下步骤:

  • 初始标记:暂停其他线程(STW),记录下GC Roots直接能引用的对象,速度快。
  • 并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程。
  • 重新标记:修正并发标记期间因为用户线程继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段停顿时间比初始标记长,但远远小于并发标记时间。主要采用三色标记里的增量更新算法做重新标记。
  • 并发清理:开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理。
  • 并发重置:重置本次GC过程的标记数据。

image.png 主要优点:并发收集,低停顿

主要缺点:

  • 对CPU资源敏感(会和服务抢资源)
  • 无法处理浮动垃圾(在并发标记和并发清理产生的垃圾,只能等到下次gc来处理)
  • 算法会有大量空间碎片产生,可通过-XX:+UseCMSCompactAtFullCollection可以让jvm在执行完标记清除后再做整理(对性能有影响)
  • 执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况(concurrent mode failure),特别在并发标记和并发清理阶段出现,此时会进入STW,用serial old垃圾收集器来回收

CMS核心参数

  1. -XX:+UseConcMarkSweepGC:启用CMS垃圾回收器。
  2. -XX:ConcGCThreads:设置并发的GC线程数。
  3. -XX:+UseCMSCompactAtFullCollection:在Full GC之后进行压缩整理,以减少碎片。
  4. -XX:CMSFullGCsBeforeCompaction:设置在多少次Full GC之后进行一次压缩整理,默认值是0,表示每次Full GC之后都会进行压缩整理(整理对性能有影响)。
  5. -XX:CMSInitiatingOccupancyFraction:当老年代的使用率达到这个比例时,会触发Full GC。默认值是92,表示老年代使用了92%的空间时会触发Full GC。
  6. -XX:+UseCMSInitatingOccupancyOnly:只使用设定的回收阈值(由-XX:CMSInitiatingOccupancyFraction设定)。如果不指定这个参数,JVM在第一次使用设定值后,后续会自动调整。
  7. -XX:+CMSScavengeBeforeRemark:在CMS GC前执行minor gc,目的在于减少老年代对年轻代的引用,降低标记(80%)时的开销。
  8. -XX:+CMSParallellnitialMarkEnabled:初始标记多线程执行,缩短(STW)。
  9. -XX:+CMSParallelRemarkEnabled:重新标记多线程执行,缩短(STW)。

三色标记算法

三色标记

  • 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无需重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。
  • 灰色:表示对象已经被垃圾收集器访问过,但这个对象至少存在一个引用还没被扫描过。
  • 白色:表示对象尚未被垃圾收集器访问过,显然在可达性分析刚开始阶段,所有对象都是白色的,若在分析结束的阶段,仍是白色的对象,表示不可达。

image.png

图例为正在GC的某一时刻中对象的关系图,A的引用B已经被扫描所以A标记为黑色,而B的引用D还没有被扫描到,所以B只能是灰色,若此时B指向D的引用为null,而A指向了D时,会产生漏标的严重问题。

并发问题

多标-浮动垃圾

在并发标记过程中,如果由于方法运行结束导致部分局部变量(gcroot)被销毁,这个gcroot引用的对象之前又被扫描过 (被标记为非垃圾对象),那么本轮GC不会回收这部分内存。这部分本应该回收但是没有回收到的内存,被称之为“浮动垃圾”。浮动垃圾并不会影响垃圾回收的正确性,只是需要等到下一轮垃圾回收中才被清除。

另外,针对并发标记(还有并发清理)开始后产生的新对象,通常的做法是直接全部当成黑色,本轮不会进行清除。这部分 对象期间可能也会变为垃圾,这也算是浮动垃圾的一部分。

漏标-读写屏障

漏标会导致被引用的对象被当成垃圾误删除,这是严重bug,必须解决,有两种解决方案: 增量更新和原始快照

增量更新就是当黑色对象插入新的指向白色对象的引用关系时, 就将这个新插入的引用记录下来, 等并发扫描结束之 后, 再将这些记录过的引用关系中的黑色对象为根, 重新扫描一次。 这可以简化理解为, 黑色对象一旦新插入了指向 白色对象的引用之后, 它就变回灰色对象了

原始快照就是当灰色对象要删除指向白色对象的引用关系时, 就将这个要删除的引用记录下来, 在并发扫描结束之后, 再将这些记录过的引用关系中的灰色对象为根, 重新扫描一次,这样就能扫描到白色的对象,将白色对象直接标记为黑色(目的就是让这种对象在本轮gc清理中能存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃圾) 以上无论是对引用关系记录的插入还是删除, 虚拟机的记录操作都是通过写屏障实现的。所谓的写屏障,其实就是指在赋值操作前后,加入一些处理(可以参考AOP的概念)

注:CMS通过增量更新实现,G1通过原始快照实现,ZGC通过读屏障实现。