JVM-垃圾收集器ParNew&CMS与底层三色标记算法详解(4)

垃圾收集算法

在这里插入图片描述

分代收集理论

为什么存在分代收集理论,由于Java 对象的生命周期不是一样的,有的朝生夕死,比如系统创建的订单对象,只要保存到数据库后,就可以被回收了,
有的长期存在,比如系统中的缓存对象,为了更好的回收对象,对对象进行分区管理,比如 年轻代、老年代 ,一般的对象都会在年轻代进行回收,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集,而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选 择“标记-清除”或“标记-整理”算法进行垃圾收集。注意,“标记-清除”或“标记-整理”算法会比复制算法慢10倍以上。

复制算法

复制算法及是把内存分成两块相同大小的内存,每次只使用其中一块内存空间,每次回收将存活的对象复制到另外一块内存中去,然后再把使用的空间一次清理掉
弊病:需要两块一样的空间,使用时只能使用其中一块内存空间
整理前:
在这里插入图片描述
整理后:
在这里插入图片描述

标记清除算法

标记清除算法分为两个阶段,
阶段一:标记
将存活的对象进行打标记
阶段二:清除
清除没有打标记对象
另外一种做法就是将需要回收对象进行打标记,清除打了标记对象
弊病:
效率问题:如果需要标记的对象过多,效率不高。
空间问题:回收后存在碎片问题。

回收前:
在这里插入图片描述
回收后:
在这里插入图片描述

标记清除整理算法

标记清除算法分为三个阶段,
阶段一:标记
将存活的对象进行打标记
阶段二:清除
清除没有打标记对象
阶段三:整理
整理上面两步产生的碎片空间
另外一种做法就是将需要回收对象进行打标记,清除打了标记对象

回收前:
在这里插入图片描述
回收后:
在这里插入图片描述

垃圾收集器

在这里插入图片描述
垃圾回收算法是理论,垃圾收集器即是对垃圾回收算法的实现。
虽然我们对各个收集器进行比较,但并非为了挑选出一个最好的收集器。因为直到现在为止还没有最好的垃圾收集器出 现,更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。试想一下:如果有一 种四海之内、任何场景下都适用的完美收集器存在,那么我们的Java虚拟机就不会实现那么多不同的垃圾收集器了。

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

Serial收集器 是一种串行收集器,在最初的电脑中不存在多核CPU,都是单核,Serial收集器因此而产生,它是一款单线程收集器,所谓的单线程收集器不仅仅指收集垃圾时是单线程,在回收垃圾时所有的工作线程都必须暂停( “Stop The World” ),直到它收集结束
在这里插入图片描述
Serial Old收集器是Serial收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在JDK1.5 以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案。

Parallel Scavenge收集器(-XX:+UseParallelGC(年轻代),-XX:+UseParallelOldGC(老年代))

Parallel Scavenge收集器 是Serial多线程的版本,收集垃圾时可以多线程收集,默认的收集线程数跟cpu核数相同,当然也可以用参数(- XX:ParallelGCThreads)指定收集线程数,但是一般不推荐修改,新生代采用复制算法,老年代采用标记-整理算法Parallel Scavenge收集器关注的是吞吐量,CMS更注重的是用户体验,所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值,也存在( “Stop The World” )
在这里插入图片描述
Parallel Old收集器是Parallel Scavenge收集器的老年代版本。使用多线程和“标记-清除-整理”算法。在注重吞吐量以及 CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器(JDK8默认的新生代和老年代收集 器)。

ParNew收集器(-XX:+UseParNewGC)

ParNew收集器其实跟Parallel收集器很类似,区别主要在于它可以和CMS收集器配合使用。
在这里插入图片描述
它是许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,只有它能与CMS收集器(真正意义上的并发收 集器,后面会介绍到)配合工作。

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

CMS收集器是一款注重用户体验的垃圾收集器,一种以获取最短回收停顿时间为目标的收集器
它是真正意义上实现并发的垃圾收集器,用户线程和垃圾回收线程同时进行。

CMS收集器回收的存在5个阶段:
在这里插入图片描述

初始标记:
初始标记,标记的是GCroot 直接引用的对象,所以该阶段速度很快,存在STW ,为什么该阶段要STW,因为如果该阶段设计成并发模式,那就会出现一个问题就是在边标记GCROOT的同时用户线程也在不断的创建GCROOT的话,该阶段就不会结束,所以必须STW,
并发标记:
并发标记,标记的是从GCROOT 直接引用的对象开始遍历所有与之有关的对象,该阶段耗时比较长,该阶段是并发的模式,用户线程和回收线程同时进行,因此会存在一个问题,在A时间回收线程将A对象标记了,由于用户线程也在执行,导致A对象的状态产生了变化。
重新标记:
最终标记,最终标记是为了修正并发标记阶段中对象状态产生变化的对象,虽然CMS只回收老年代的垃圾对象,但是这个阶段依然需要扫描新生代,因为很多GC Root都在新生代,而这些GC Roots指向的对象又在老年代,这称为“跨代引用”,该阶段也存在STW,该阶段的耗时比初始标记要慢但是比并发标记要快,主要用到三色标记里的增量更新算法(见下面详解)做重新标记
并发回收:
并发回收,是回收垃圾阶段,对未标记的对象进行回收,回收线程和用户线程会同时进行,这个阶段如果有新增对象会被标记为黑 色不做任何处理(见下面三色标记算法详解)。
并发重置
重置本次GC过程中的标记数据。

结论:
优点: 并发进行,低停顿
缺点:
1.会于争抢CPU的资源。
2.无法处理浮动垃圾(并发标记、并发清理阶段对象状态产生变化的就是浮动垃圾,只能等到下次GC才能进行回收)。
3.由于CMS回收算法用的是标记清除所以会产生内存碎片,当然通过参数- XX:+UseCMSCompactAtFullCollection可以让jvm在执行完标记清除后再做整理。
4.执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并 发标记和并发清理阶段会出现,一边回收,系统一边运行,也许没回收完就再次触发full gc,也就是"concurrent mode failure",此时会进入stop the world,用serial old垃圾收集器来回收。

CMS的相关核心参数:

  1. -XX:+UseConcMarkSweepGC:启用cms
  2. -XX:ConcGCThreads:并发的GC线程数
  3. -XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)
  4. -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一 次
  5. -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)
  6. -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设 定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整
  7. -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,目的在于减少老年代对年轻代的引 用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在标记阶段
  8. -XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW
  9. -XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW;

垃圾收集底层算法实现

三色标记

在并发标记的过程中,因为标记期间应用线程还在继续跑,对象间的引用可能发生变化,多标和漏标的情况就有可能发生。 这里我们引入“三色标记”来给大家解释下,把Gcroots可达性分析遍历对象过程中遇到的对象, 按照“是否访问过”这个条件标记成以 下三种颜色:
在这里插入图片描述

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

多标-浮动垃圾
在并发标记、并发清理阶段存在一种现象就是,原本对象初始状态是标记状态,由于用户线程的介入,该对象变成了垃圾对象,但是该对象的状态还是标记状态,这种就是多标,只能在下一轮GC才被清理掉,另外,针对并发标记(还有并发清理)开始后产生的新对象,通常的做法是直接全部当成黑色,本轮不会进行清除。这部分 对象期间可能也会变为垃圾,这也算是浮动垃圾的一部分。

漏标-读写屏障
漏标会导致被引用的对象被当成垃圾误删除,这是严重bug,必须解决,
有两种解决方案:
增量更新(Incremental Update)
原始快照(Snapshot At The Beginning,SATB) 。
增量更新
就是当黑色对象插入新的指向白色对象的引用关系时, 就将这个新插入的引用记录下来, 等并发扫描结束之 后, 再将这些记录过的引用关系中的黑色对象为根, 重新扫描一次。 这可以简化理解为, 黑色对象一旦新插入了指向 白色对象的引用之后, 它就变回灰色对象了。
原始快照
就是当灰色对象要删除指向白色对象的引用关系时, 就将这个要删除的引用记录下来, 在并发扫描结束之后, 再将这些记录过的引用关系中的灰色对象为根, 重新扫描一次,这样就能扫描到白色的对象,将白色对象直接标记为黑 色(目的就是让这种对象在本轮gc清理中能存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃圾) 以上无论是对引用关系记录的插入还是删除, 虚拟机的记录操作都是通过写屏障实现的。

写屏障
给某个对象的成员变量赋值时,其底层代码大概长这样:

 /** 
   * @param field 某对象的成员变量,如 a.b.d 
   * @param new_value 新值,如 null 
   * 
  */ 
   void oop_field_store(oop* field, oop new_value) { 
    *field = new_value; 
    // 赋值操作 7 }

所谓的写屏障,其实就是指在赋值操作前后,加入一些处理(可以参考AOP的概念):

void oop_field_store(oop* field, oop new_value) { 
pre_write_barrier(field); 
// 写屏障‐写前操作 
*field = new_value; 
post_write_barrier(field, value); 
// 写屏障‐写后操作 5 
}

写屏障实现SATB
当对象B的成员变量的引用发生变化时,比如引用消失(a.b.d = null),我们可以利用写屏障,将B原来成员变量的引用 对象D记录下来:

 void pre_write_barrier(oop* field) { 
	oop old_value = *field; // 获取旧值 
 	remark_set.add(old_value); // 记录原来的引用对象 
}

写屏障实现增量更新
当对象A的成员变量的引用发生变化时,比如新增引用(a.d = d),我们可以利用写屏障,将A新的成员变量引用对象D 记录下来:

void post_write_barrier(oop* field, oop new_value) {
 	remark_set.add(new_value); // 记录新引用的对象 
 }

读屏障

oop oop_field_load(oop* field) { 
 	pre_load_barrier(field); // 读屏障‐读取前操作
 	return *field; 
  }

读屏障是直接针对第一步:D d = a.b.d,当读取成员变量时,一律记录下来:

void pre_load_barrier(oop* field) { 
	oop old_value = *field; 
	remark_set.add(old_value); // 记录读取到的对象 
}

对于读写屏障,以Java HotSpot VM为例,其并发标记时对漏标的处理方案如下:
CMS:写屏障 + 增量更新
G1,Shenandoah:写屏障 + SATB
ZGC:读屏障

为什么G1用SATB?CMS用增量更新?
我的理解:SATB相对增量更新效率会高(当然SATB可能造成更多的浮动垃圾),因为不需要在重新标记阶段再次深度扫描 被删除引用对象,而CMS对增量引用的根对象会做深度扫描,G1因为很多对象都位于不同的region,CMS就一块老年代 区域,重新深度扫描对象的话G1的代价会比CMS高,所以G1选择SATB不深度扫描对象,只是简单标记,等到下一轮GC 再深度扫描。

记忆集与卡表

在新生代做GCRoots可达性扫描过程中可能会碰到跨代引用的对象,这种如果又去对老年代再去扫描效率太低了。 为此,在新生代可以引入记录集(Remember Set)的数据结构(记录从非收集区到收集区的指针集合),避免把整个 老年代加入GCRoots扫描范围。事实上并不只是新生代、 老年代之间才有跨代引用的问题, 所有涉及部分区域收集 (Partial GC) 行为的垃圾收集器, 典型的如G1、 ZGC和Shenandoah收集器, 都会面临相同的问题。 垃圾收集场景中,收集器只需通过记忆集判断出某一块非收集区域是否存在指向收集区域的指针即可,无需了解跨代引 用指针的全部细节。 hotspot使用一种叫做“卡表”(cardtable)的方式实现记忆集,也是目前最常用的一种方式。关于卡表与记忆集的关系, 可以类比为Java语言中HashMap与Map的关系。 卡表是使用一个字节数组实现:CARD_TABLE[ ],每个元素对应着其标识的内存区域一块特定大小的内存块,称为“卡 页”。 hotSpot使用的卡页是2^9大小,即512字节
在这里插入图片描述
一个卡页中可包含多个对象,只要有一个对象的字段存在跨代指针,其对应的卡表的元素标识就变成1,表示该元素变脏,否则为0. GC时,只要筛选本收集区的卡表中变脏的元素加入GCRoots里。
卡表的维护
卡表变脏上面已经说了,但是需要知道如何让卡表变脏,即发生引用字段赋值时,如何更新卡表对应的标识为1。 Hotspot使用写屏障维护卡表状态。

G1中每一个region都维护其他region引用该region对象的信息(point out)。实际实现还是CardTable,只不过多了一个哈希表(稀疏表)。

CMS 中只维护了一个卡表

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值