jvm 优化篇-(8)-跨代引用问题(RememberSet、CardTable、ModUnionTable、DirtyCard)

死神-露琪亚

1、什么是跨代引用?

红色的线表示由虚拟机栈中发出的引用。显然B--->A、E--->F都是跨代引用。


跨代引用逻辑图

2、跨代引用对MonitorGC的影响

JVM GC 判断对象是否可以回收使用可达性分析的方法,可达性分析首先需要找到 GC Roots 对象。

常规GC-Root:

  • 1)虚拟机栈(栈帧中的局部变量表)中引用的对象【Stack Local】。
  • 2)本地方法栈(native方法)引用的对象。
  • 3)方法区中类静态属性、静态方法引用的对象。
  • 4)方法区中常量引用的对象。

MonitorGC个性GC-Root:

  • 5) RememberSet数据结构(CardTable是具体实现类似数组)

这里重点讲一下CardTable:
        作用:采用空间换时间,不需要扫描整个Heap空间,降低MonitorGC耗时。跨代引用带来的问题,采用CardTable很好的规避了遍历整个老年代的问题。
        HotSpot JVM的卡页(Card Page)大小为512字节,卡表(Card Table)被实现为一个简单的字节数组,即卡表的每个标记项为1个字节。

CardTable标记着是否存在老年代引用新生代

如上图发现:B、C所在的Card Page在CardTable中被标记上了。

【思考】MonitorGC的GC-Root中包括CardTable,如何提高MonitorGC效率?
就需要降低跨代引用的对象,尽量设置稍大些的From、To区域,尽量将对象消灭在Young Gen区域。同时也表示对象不是越早进入老年代越好(老年代对象引用新生代对象就是一个很好的说明)!!!。

3、跨代引用对CMS中 OldGC的影响

CMS-OldGC回收♻️分为7个步骤:


CMS的7个步骤
重点步骤解读:
  • 1、初始标记(Initial Mark)

    • 目标:进行可达性分析,标记GC ROOT能直接关联到的对象。
    • 标记范围:Young Gen + Old Gen
    • 线程:JDK1.7是单线程,JDK1.8是多线程(-XX:+CMSParallelInitialMarkEnabled调整)
    • STW:触发Stop-The-World
    • 特点:速度极快
  • 2、并发标记(Concurrent Mark)

    • 目标:遍历阶段1初始标记出来的存活对象,然后继续递归标记这些对象可达的对象。(黑白灰三色标记法)。
    • 标记范围:Young Gen + Old Gen
    • STW:不触发
    • 特点:慢,很耗时。
    • 特殊操作:通过Old区卡片标记(Card Marking),提前把老年代空间逻辑划分为相等大小的区域(Card Page),如果引用关系发生改变,JVM会将发生改变的区域标记位“脏区”(Dirty Card)。JVM会对“并发标记”阶段应用线程新产生的对象及对象涉及的引用修改做记录(Mod-Union Table)
  • 3、预清理(Preclean)

    • 目标:2阶段中记录在Mod-Union Table的这些脏区会被找出来,刷新引用关系,清除“脏区”标记。
    • 标记范围: Old Gen。
    • 线程:GC线程和应用线程也是并发执行
    • STW:不触发
    • 特点:速度一般
  • 4、可中断的预清理(Concurrent Abortable Preclean)

    • 目标:降低垃圾回收时对应用的暂停时间,整个过程最耗时步骤在5(最终标记),所以4步骤提前进行。
    • 触发条件:此阶段在Eden区使用超过2M时启动,当然2M是默认的阈值,可以通过参数修改。
    • 标记范围:Young Gen + Old Gen
      • 处理From、To区对象,标记可达到老年代的对象。
      • 和3阶段一样,扫描处理Dirty Card中的对象。
    • 终止逻辑:CMS为了避免这个阶段没有等到Minor GC而陷入无限等待,提供了参数CMSMaxAbortablePrecleanTime ,默认为5s,含义是如果可中断的预清理执行超过5s,不管发没发生Minor GC,都会中止此阶段,进入Remark。
    • 线程:GC线程和应用线程也是并发执行。
    • STW:不触发
    • 特点:速度一般
    • 特殊:下一个阶段要执行最耗时且STW的Remark阶段,Remark阶段会将整个YoungGen作为GC-Root进行操作,如果4阶段能触发一次MonitorGC就再好不过了。
  • 5、重新标记(Final ReMark)

    • 目标:GC事件中第二次(也是最后一次)STW阶段,目的是完成老年代中所有存活对象的标记。
    • 标记范围:Young Gen + Old Gen
    • 线程:GC线程独占执行
    • STW:触发Stop-The-World
    • 特点:速度较慢,可以说是整个CMS-Old GC的瓶颈点
    • 特殊:
      • 1、遍历新生代对象,重新标记
      • 2、根据GC Roots,重新标记
      • 3、遍历老年代的Dirty Card,重新标记
  • 6、并发清除(Concurrent Sweep)

    • 目标:根据标记结果清除垃圾对象。
    • 标记范围:Old Gen。
    • 线程:GC线程和应用线程也是并发执行
    • STW:不触发
    • 特点:速度一般。
  • 7、并发重置(Concurrent Reset)

    • 目标:重置CMS算法相关的内部数据, 为下一次GC循环做准备。
    • 线程:GC线程和应用线程也是并发执行
    • STW:不触发
    • 特点:速度一般。
7步图解:
三阶段:初始标记

二阶段:并发标记

三阶段:并发预清理

六阶段:并发清除

4、问题思考:

【思考】CMS oldgc中remark耗时问题,跨代引用是图一中的B--->A 还是 E--->F?
答案:E--->F。
通过:-XX:+CMSScavengeBeforeRemark ,触发MonitorGC降低的就是YoungGen区的对象,从而达到标记源头减少,降低remark时间。
【思考】CMS 跨代引用是图一中的B--->A 有何危害,虚拟机是如何避开的?
答案:CardTable。
逻辑:采用创建CardTable空间区域,来避免扫整个老年代。当B无引用的时候,cardTable会被标记,一次MonitorGC就会回收♻️掉。B是如何做到没引用的,同E--->F是一样的逻辑。

【思考】CMS里有两个需要STW的阶段:initial mark,remark、这两个标记有什么不一样么?使用的GC-Root一样么?
答案:不一样。
初始标记:使用的是常规的GC-Root(虚拟机栈栈帧中的局部变量表、本地方法栈native方法、方法区中类的静态属性和方法、方法区中常量等引用的对象)
重新标记:

  • 常规的GC-Root。(只有MonitorGC才会使用跟可达分析)
  • 新生代所有对象。
  • 遍历老年代的DirtyCard(ModUnionTable)。

【思考】同时带来了另外一个问题,MonitorGC可没有遍历整个老年代,而是采用CardTable通过时间换空间的做法,CMS-OldGC为什么采用遍历新生代所有的对象呢?
        这就是YoungGen 与 OldGen存在明显的差异:

  • 老年代对象都是经历过多次GC回收♻️存活下来了,对象存在的变数极低。所以采用空间换时间效果较好。
  • 新生代对象属于变化特别快的区域,如果采用空间换时间,既浪费了空间,也没有提升性能。

【思考】既然initial mark阶段+concurrent mark阶段已经扫果了young gen 为何还要再次Remark?

【思考】CardTable与mod-union table有什么关系,都是干什么的?

你可能感兴趣的:(jvm 优化篇-(8)-跨代引用问题(RememberSet、CardTable、ModUnionTable、DirtyCard))