垃圾收集器


一、垃圾收集算法

    ① 分代收集理论

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

      比如在新生代,每次收集都会有大量的对象死去,所以可以选择复制算法,只需要付出少量对象的复制就可以完成每次垃圾收集。

        而老年代的对象存活几率比较高,而且没有额外的空间对它分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。注意,“标记-清除”或“标记-整理”算法会比复制算法慢10倍以上。

 ② 复制算法

    核心:为了解决效率问题,引出了复制算法。它将内存分为大小相同的两块空间,每次只使用一块。当这一块的内存使用完后,将存活对象复制到另外一块内存,然后再把当前使用空间一次清理掉。

    优点:效率高。 缺点:浪费内存空间,内存使用率低。

 ③ 标记-清除算法

    核心:分为两个阶段“标记”和“清除”。 将存活对象标记出来,然后对未标记对象进行清除。或者反过来,对未存活对象标记,然后进行清除。

         缺点: 1.如果标记的对象太多,那么回收效率不高  2. 内存碎片化,会产生大量的内存碎片。     

 ④ 标记-整理算法

    核心:区别于标记清除算法的地方在于,标记完对象后,会先将对象移动到一端(向同一端移动),然后对分界线另一端进行清除。

二、垃圾收集器(优化垃圾收集器最核心的点就是为了减少STW时间)

  本文着重讲解CMS垃圾收集器和G1垃圾收集器

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

    ① Serial

  单线程收集器(新生代采用复制算法,老年代采用标记-整理算法)

    ② Paraller Scavenge

  Serial收集器的多线程版本。默认的收集线程数跟cpu核数相同,当然也可以用参数(-XX:ParallelGCThreads)指定收集线程数,但是一般不推荐修改。Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。

  CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。

    ③ ParNew 

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

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

   ④  CMS(Concurrent Mark Sweep)

  <1> 是一种以获取最短回收停顿时间为目标的收集器。非常符合使用在注重用户体验的系统中,第一次实现了让垃圾收集线程和用户线程(基本上)同时工作。 

  从名字中的Mark Sweep这两个词可以看出,CMS收集器是一种 “标记-清除”算法实现的。它的整个回收过程大概如下:

  

  1. 初始标记:暂停所有的其他线程(STW),并记录下所有 GC Roots(局部变量表中引用的对象、本地方法引用的对象、方法区中静态属性引用的对象、方法区中静态常量池中引用的对象)直接能引用的对象,速度很快。

  2. 并发标记:并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但是不需要停顿用户线程, 可以与垃圾收集线程一起并发运行。因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。

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

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

  5. 并发重置:重置本次GC过程中的标记数据。

  <2> 优点:并发收集、低停顿。

  <3> 缺点:① 对CPU资源敏感(会和服务抢资源)。 ② 无法处理浮动垃圾(在并发标记和并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次gc再清理了) ③ 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生,当然通过参数-

  XX:+UseCMSCompactAtFullCollection可以让jvm在执行完标记清除后再做整理 ④ 执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并发标记和并发清理阶段会出现,一边回收,系统一边运行,

  也许没回收完就再次触发full gc,也就是"concurrentmode failure",此时会进入stop the world,用serial old垃圾收集器来回收。

   <4>  CMS的相关核心参数

    ① -XX:+UseConcMarkSweepGC  启用cms垃圾收集器

    ②-XX:ConcGCThreads  并发的GC线程数

    ③-XX:+UseCMSCompactAtFullCollection  FullGC之后做压缩整理(减少碎片)

    ④-XX:CMSFullGCsBeforeCompaction  多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一次 

    ⑤ -XX:CMSInitiatingOccupancyFraction  当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)

    ⑥-XX:+UseCMSInitiatingOccupancyOnly  只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整

    ⑦-XX:+CMSScavengeBeforeRemark  在CMS GC前启动一次minor gc,目的在于减少老年代对年轻代的引用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在标记阶段

    ⑧ -XX:+CMSParallellnitialMarkEnabled  表示在初始标记的时候多线程执行,缩短STW

    ⑨-XX:+CMSParallelRemarkEnabled  在重新标记的时候多线程执行,缩短STW

   ⑤  G1(garbage first)

    明天写。。。

  

  三色标记:

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

  下三种颜色:

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

  

  多标-浮动垃圾

  在并发标记过程中,如果由于方法运行结束导致部分局部变量(gcroot)被销毁,这个gcroot引用的对象之前又被扫描过
  (被标记为非垃圾对象),那么本轮GC不会回收这部分内存。这部分本应该回收但是没有回收到的内存,被称之为“浮动
  垃圾”。浮动垃圾并不会影响垃圾回收的正确性,只是需要等到下一轮垃圾回收中才被清除。
  另外,针对并发标记(还有并发清理)开始后产生的新对象,通常的做法是直接全部当成黑色,本轮不会进行清除。这部分
  对象期间可能也会变为垃圾,这也算是浮动垃圾的一部分。

  

  漏标-读写屏障


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