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个字节。
如上图发现:B、C所在的Card Page在CardTable中被标记上了。
【思考🤔】MonitorGC的GC-Root中包括CardTable,如何提高MonitorGC效率?
就需要降低跨代引用的对象,尽量设置稍大些的From、To区域,尽量将对象消灭在Young Gen区域。同时也表示对象不是越早进入老年代越好(老年代对象引用新生代对象就是一个很好的说明)!!!。
3、跨代引用对CMS中 OldGC的影响
CMS-OldGC回收♻️分为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,重新标记
- 目标:GC事件中第二次(也是最后一次)STW阶段,目的是完成老年代中
-
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有什么关系,都是干什么的?