补充CMS和G1垃圾回收器

阿里的多租户JVM

  1. 每个租户单独的空间
  2. session based GC

    1. 一次web请求,产生很多对象,这次请求结束后,这些对象也就没用啦.所以是不是可以即时把这些没用的对象回收掉,以提升效率呢?

基本概念

card table(大家都有)

使用Root searching找到所有存活对象时,可能会在年轻代和老年代间互相引用
如果只是年轻代的YGC,总不能去全量扫描老年代吧?那样效率会很底,因为Old区很大
为了提高效率,JVM设计了card table

JVM会把对象分成一个个的card(类比与内存分页)
当老年代的一个card中有对象指向年轻代时,这个card就被标记为dirty,下次扫描时,只需要扫描Old区的dirty card
会使用一个card table记录所有的card中,具体哪个对象dirty
card table由bitmap实现

CSet(Collection Set)(只有G1有)

一组可被回收的分区的集合,GC时只需要扫描CSet
在CSet中存活的数据会在GC过程中被移动到另一个可用分区
CSet中的分区可以来自Eden,Survivor或者Old
CSet会占用整个堆空间不到1%的大小

RSet(Remember Set)(只有G1有)

记录了其他Region中的对象到本Region的引用
RSet的价值在于,它使得垃圾回收器不需要扫描整个堆找到谁引用了当前分区中的对象,
而是只需要扫描RSet即可
比CardTable更高效
RSet会稍微影响效率,因为记录RSet需要时间
(记录RSet的这个过程,在GC中被称为写屏障,和线程同步的内存屏障不是一回事)

CMS

CMS的缺点:产生内存碎片,浮动垃圾(每次不能回收干净垃圾)
碎片和浮动垃圾到一定程度后,可能会出现concurrent mode failure,会触发Serial 单线程的FGC,很慢很慢
CMS有这些缺点,所以没有任何版本的JDK默认是CMS的.
但是CMS有着卓越的历史贡献,有了CMS才有了后来的G1和ZGC

4个阶段

黄色箭头表示GC线程,蓝色箭头表示用户的工作线程
image.png

  1. initial mark

    1. 找到并标记根对象GC roots<br /> STW,但是根对象很少,所以很快
  2. concurrent mark

    1. 顺着上一步的根对象Root searching, 找到并标记所有存活的对象<br /> 和用户线程并发执行,是最耗时的一个阶段
  3. remark

    1. 上一步并发标记的同时,因为用户线程在同时运行,所以可能会有并发标记阶段一个对象被标记后,状态又发生了 变化,和标记不同了<br />(应该会由存活对象变为垃圾,而不太可能从垃圾变为存活对象,因为已经是垃圾了,就没人引用了呀,这个回头查查资料,貌似finalize方法可以从垃圾变为存活对象)<br />这时候就重新标记一下,标记出那些标记的同时又发生状态变化的对象<br />STW,时间很短
  4. concurrent sweep

    1. 初始标记,并发标记,重新标记后,把垃圾回收调<br /> 和用户线程并发执行,所以清理的同时也会产生新的垃圾,这称为浮动垃圾<br /> 浮动垃圾会在下一次的GC是回收掉

G1

G1之前的垃圾回收器(Serial,Parallel Scavenge,CMS等)都是把内存从物理上分块的(为了分代)
G1是物理上不分代,只在逻辑上分代,分成了很多Region,用到了分而治之的思想
G1比较适合用在多核CPU,较大内存(几十G)的机器上,暂停时间很短,同时保证较高的吞吐量

据说G1和PS相比,吞吐量低了10%~15%
但是G1的停顿时间很短,只有200ms

为啥叫G1呢?其实是Garbage First,垃圾第一
它会把内存从逻辑上分为很多个块,当GC时,优先回收那些垃圾多的块

特点

  1. 并发收集(三色标记,颜色指针)
  2. 压缩空间,不会延长GC的暂停时间
  3. 更容易预测GC的暂停时间
  4. 适用于不需要实现很高吞吐量的场景(响应时间优先)

    分而治之的思想

    把内存分成了很多小块(Region),最大到32M
    每个分区都可能是年轻代或者老年代,但是同一时刻是能是某一个代
    年轻代,Eden,Survivor,Old这些概念还在,只是逻辑上的概念,方便复用之前分代框架的逻辑
    当GC时,优先回收那些垃圾多的分区,这样提高效率
    image.png

    G1的内存区域不是固定的Eden或者Survivor或者Old

    很灵活

新老年代比例

会动态调整这个比例,5%~60%
一般不要手动指定(之前的GC的新老年代比例都是固定的,不指定的话有默认值)
因为这是G1预测停顿时间的基准

GC触发条件

  1. YGC,Eden空间不足时触发,多线程并行执行
  2. MixedGC,占用内存到达一定阈值时触发,相当于CMS,并发执行
  3. FGC,Old空间不足,或者System.gc(),JDK10之前是串行的

有人会问,既然G1分了这么多Region,每次都是回收一部分Region,是不是就不会产生FGC啦?
其实不是,当对象产生的特别快,GC回收不过来时,仍会触发YGC
G1的FGC,在JDK<10时,是串行的;JDK10开始并行FGC
所以我们调优时要尽量避免FGC

如何尽量避免FGC

如果G1触发FGC了,应该:

  1. 扩大内存
  2. 提高CPU性能,以提高垃圾回收的速度
  3. 降低MixedGC触发的阈值,让MixedGC提早发生(默认45%)

    MixedGC

    针对所有的Region,不区分Young还是Old,选择垃圾最多的Region
    相当于CMS,和它几乎一摸一样
    区别是回收时是筛选回收,只回收那些垃圾最多的Region;回收时会复制压缩,碎片很少
    当程序使用堆内存到达一定阈值后,会触发MixedGC
    指定阈值-XX:InitiatingHeapOccupacyPercent

垃圾回收的其他算法

CMS和G1都用到了并发标记算法,就是一边标记垃圾的同时,一边对象引用关系在发生改变(产生新垃圾或者垃圾”变废为宝”)

三色标记法

三色标记主要是为了解决漏标的问题
把对象在逻辑上分为三种颜色(表示标记的完成度):

  1. 黑色:自身和成员变量均已标记完成
  2. 灰色:自身被标记,成员变量未被标记
  3. 白色:未被标记的对象

    漏标

    漏标是指一个存活对象,在某种情况下,没有被标记到,而被当成垃圾回收了

什么情况下会漏标

举例,并发标记前,有个灰色对象 引用-> 白色对象,
如果在并发过程中,满足两个条件时会漏标:
1.有个黑色对象引用了白色对象,
2.并且这个灰色不引用白色了
此时如果不对黑色对象重新扫描,则会漏标
(扫描灰色时,会把那个白色对象当作垃圾回收掉)

(在并发标记过程中,Mutator删除了所有灰色到白色的引用,则会漏标,此时白色对象应该被回收?)

怎么解决漏标的问题

  1. incremental update : 增量更新,关注引用的增加(CMS使用)

当黑色对象引用增加时,把黑色重新标记为灰色,下次重新扫描属性

  1. SATB,snapshot at the beginning,关注引用的删除(G1使用,因为有RSet,可以提高效率)

当灰色对白色的引用消失时,要把这个引用推到GC的堆栈,保证白色还能被GC扫描到(在重新标记时)
(G1的对象通过RSet可以知道谁引用了自己)