阅读 163

JVM(四)JVM垃圾回收算法详解

JVM(四)JVM垃圾回收算法详解

一、垃圾回收算法概览 

  垃圾回收算法主要根据对象存活周期的不同将内存分为几部分。一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

  比如在新生代中,每次收集都会有大量对象(近99%)死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,不适合留出一大块空间用于复制,所以我们选择“标记-清除”或“标记-整理”算法进行垃圾收集。

  PS:“标记-清除”或“标记-整理”算法会比复制算法慢10倍以上。

标记-复制算法

  定义:把内存空间一分为二,在GC前,数据只占用一边的内存,GC以后把存活的对象复制到另一块区域中去,并对当前区域做清理

  缺点:不适用于老年代内存回收,因为有一半的内存空间一直闲置着不能放东西。

标记-清除算法

  定义:标记存活的对象, 统一回收所有未被标记的对象(一般选择这种)。或者标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

  缺点:

  • 效率问题 (如果需要标记的对象太多,效率不高)

  • 空间问题(标记清除后会产生大量不连续的碎片)

标记-整理算法

  定义:标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存【针对老年代设计】

二、垃圾收集器

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

  PS:目前没有完美的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器

Serial收集器新生代:-XX:+UseSerialGC  老年代:-XX:+UseSerialOldGC

  Serial(串行)收集器是最基本、历史最悠久的单线程垃圾收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World" ),直到它收集结束。

  新生代采用复制算法,老年代采用标记-整理算法。

  优点:简单而高效(与其他收集器的单线程相比)。Serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。

  PS:Serial Old【Serial收集器的老年代版本】在JDK1.5 以及以前的版本中与Parallel Scavenge收集器搭配使用,同时可以作为CMS收集器的后备方案

Parallel Scavenge收集器(新生代:-XX:+UseParallelGC  老年代:-XX:+UseParallelOldGC

  Parallel收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算 法、回收策略等等)和Serial收集器类似。

  新生代复制算法

  老年代标记-整理算法

  PS:默认的收集线程数跟cpu核数相同,当然也可以用参数(- XX:ParallelGCThreads)指定收集线程数,但是一般不推荐修改。

  使用场景:注重吞吐量以及CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器(JDK8默认的新生代和老年代收集器)。 

  • Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)PS:吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。

  • CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)

ParNew收集器(只有新生代:-XX:+UseParNewGC

  ParNew收集器其实跟Parallel收集器很类似,区别主要在于它可以和CMS收集器配合使用

  PS:除了Serial收集器外,只有它能与CMS收集器配合使用。

  新生代复制算法

CMS收集器(只有老年代:-XX:+UseConcMarkSweepGC

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

  老年代标记-整理算法

初始标记

  暂停所有的其他线程(STW),并记录下gc roots直接能引用的对象,速度很快。

并发标记

  并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象集合的过程, 这个过程耗时较长但是不需要停顿用户线程, 可以与垃圾收集线程一起并发运行。

  PS:因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变

重新标记

  重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到三色标记里的增量更新算法(见下面详解)做重新标记。

并发清理

  开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理(见下面三色标记算法详解)。

并发重置

  重置本次GC过程中的标记数据

  优点:

  并发收集、低停顿。

缺点:

  • 对CPU资源敏感(并发操作会和服务主线程抢资源)

  • 无法处理浮动垃圾(在并发标记和并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次gc再清理了)

  • 其使用的“标记-清除”算法会导致收集结束时会有大量空间碎片产生【可以通过参数-XX:+UseCMSCompactAtFullCollection让jvm在执行完标记清除后再做整理】

  • 执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况。特别是在并发标记和并发清理阶段会出现,一边回收,系统一边运行,也许没回收完就再次触发full gc,也就是"concurrent mode failure【并发失败】",此时会进入STW,转而用serial old垃圾收集器来回收

三、垃圾收集算法底层实现【三色标记】

  在并发标记的过程中,因为标记期间应用线程还在继续运行,对象间的引用可能发生变化,多标和漏标的情况就有可能发生,所以这里引入“三色标记”的概念,来分析垃圾回收器是如何处理这几种情况的。

  原理:把gcroots可达性分析遍历对象过程中遇到的对象, 按照“是否访问过”这个条件标记成以 下三种颜色:

  • 黑色: 表示对象已经被垃圾收集器访问过, 且这个对象的所有引用都已经扫描过。 如果有其他对象引用指向了黑色对象, 无须重新扫描一遍。

  PS:黑色对象不可能直接(不经过灰色对象) 指向某个白色对象。

  • 灰色: 表示对象已经被垃圾收集器访问过, 但这个对象上至少存在一个引用还没有被扫描过

  • 白色: 表示对象尚未被垃圾收集器访问过

  显然在可达性分析刚刚开始的阶段, 所有的对象都是白色的, 若在分析结束的阶段, 仍然是白色的对象, 即代表不可达,也就是需要被回收的对象。

多标-浮动垃圾

  主要发生在:并发标记过程

  场景:

  • 如果由于方法运行结束导致部分局部变量(gcroot)被销毁,这个gcroot引用的对象之前又被扫描过 (被标记为非垃圾对象),那么本轮GC不会回收这部分内存。

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

  PS:浮动垃圾并不会影响垃圾回收的正确性,只是需要等到下一轮垃圾回收中才被清除。 

漏标-读写屏障

  漏标会导致被引用的对象被当成垃圾误删除,这是严重bug!

  解决方案: 增量更新(Incremental Update) 和原始快照(Snapshot At The Beginning,SATB) 。

增量更新

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

原始快照【SATB】

  灰色对象要删除指向白色对象的引用关系时, 就将这个要删除的引用记录下来, 在并发扫描结束之后, 再将这些记录过的引用关系中的灰色对象为根, 重新扫描一次,这样就能扫描到白色的对象,将白色对象直接标记为黑色(目的就是让这种对象在本轮gc清理中能存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃圾) 

读写屏障

  简单的来说就是定义一个集合,在操作这些引用关系之前去把这些操作记录进集合中去,然后在重新标记阶段时,对这个集合内的数据节点进行再次处理。

  对于读写屏障,以JVM常见的垃圾收集器为例,并发标记时对漏标的处理方案如下:

  • CMS:写屏障 + 增量更新

  • G1Shenandoah:写屏障 + SATB

  • ZGC:读屏障

四、小问答

Q:为什么G1用SATB?CMS用增量更新?

  SATB相对增量更新效率会高(当然SATB可能造成更多的浮动垃圾),因为不需要在重新标记阶段再次深度扫描被删除引用对象。

  • CMS对增量引用的根对象会做深度扫描,因为CMS就一块老年代区域,扫描成本相对没那么高。

  • G1因为很多对象都位于不同的region,重新深度扫描对象的话G1的代价会比CMS高,所以G1选择SATB不深度扫描对象,只是简单标记,等到下一轮GC再深度扫描。

Q:使用CMS时有什么注意事项?

  可以调整Jvm参数设置当老年代占用到某个比例时,就触发fullgc,这样可以防止当并发收集失败而切换gc收集器【CMS -> Serial Old

  • -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC【默认是92(单位:百分比)】

Q:Jvm的某些参数前面会带有X或者XX是什么意思?

  X越多表示命令越有可能在后期的版本废除掉。


文章分类
后端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐