拖了很久又开始了,果然躲不过,逃不了。能写多少写多少吧,看天意
预备知识:简介JVM
当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”的理论进行设计,在这个理论下有分为两个分代假说上:
由此奠定了多款常用的垃圾收集器的一致的设计原则:垃圾收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄分配到不同的区域之中存储。其目的是将需要经常清理的部分和不需要经常清理的部分分类,然后对不同区域进行不同的清理。同时兼顾了垃圾收集的时间开销和内存的空间有效利用。因而有了“Minor GC”、“Major GC”、“Full GC” 这样回收类型的划分。
假如现在要进行一次只局限于新生代区域的收集(Minor GC),但是新生代中对象完全有可能被老年代所引用,为了找出该区域中存活的对象,不得不在固定的GC Roots之外,再额外遍历整个老年代中所有对象来确保可达性分析的正确。返回来也是如此(这种情况会出现在CMS收集器中)。针对这个问题,所以之前的理论添加第三条经验法则:
为了减少跨代引用导致的扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在及存在在哪些跨代引用,之需要在新生代上建立一个全局的数据结构(记忆集),这个结构把老年代分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后在发生Minor GC的时候,只有包含了跨代引用的小块内存里的对象才会被加入GC Roots进行扫描,前提是需要在对象改变引用关系时维护记录数据的正确性,会增加一些运行时的开销,但比起扫描整个老年代来说是更划算的。
如果想实现记忆集在不考虑效率和成本的前提下,最简单的方案就是用非收集区域的所有含跨代引用的对象的数组来实现这个数据结构。然后这样的成本太高,所以选择更粗犷的记录粒度来节省记忆集的存储和维护成本。
第三种 “卡精度”所指的是一种称为“卡表(Card Table)”的方式区实现记忆集,这也是目前最常用的一种记忆集实现形式。
卡表的最简单形式可以是一个字节数组,HotSpot虚拟机确实也是这样做的。其默认的卡表标记逻辑是:
字节数组CARD_TABLE的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个内存块被称为“卡页”,卡页大小都是以2的N次幂的字节数,HotSpot中用的是2的9次幂,即512字节。一个卡页的内存中通常包含步指一个对象,只要卡页内一个或者多个对象的字段存在着跨代指针,那就将对应的卡表的数组元素的值标志位1,称之为“元素变脏 Dirty”,字垃圾回收时,只要筛选出卡表中变脏的元素,得到哪些卡页内存中包含跨代指针,把它们加入GC Roots中一并扫描。
上面说到卡表元素变脏,那么它们何时变脏、谁来把它们变脏?何时变脏的答案很明确——有其他分代区域中对象引用了本区域对象时,其对相应的卡表元素就应该变脏。时间点应该原则上应该发生在引用类型字段赋值的那一刻。
如何变脏——在HotSpot虚拟机里时通过写屏障(Write Barrier)技术维护卡表状态。注意这里的“写屏障”与并发下的内存屏障不是一回事,这里的的写屏障以及读屏障类似虚拟机层面对引用类型字段赋值这个动作的AOP切面,分为写前屏障(Pre-Write-Barrier)和写后屏障(Post-Write-Barrier)。直到G1出现前,HotSpot中大多数虚拟机都只用到了写后屏障。这种写屏障是有额外的开销的,但是对比扫描整个老年代的开销来说明显的减少了。
另外除了写屏障的开销外,卡表在高并发场景下还面临着“伪共享(False Sharing)”的问题。伪共享问题是处理并发底层细节时需要考虑的问题,现在中央处理器的缓存系统中时以“缓存行(Cache Line)”为单位存储的。缓存行的大小为64字节,而一个卡表元素占一个字节,即64个卡表共享一个缓存行,高并场景下彼此会相互影响(写回、无效化或者同步)导致性能降低,这就是伪共享问题。为了解决这种问题,避免伪共享的发生,一种简单的解决方案是不采用无条件的写屏障,而是采用先检查卡表标记,只有当卡表元素未被标记时才将其标记变脏。在JDK 7之后,HotSpot虚拟机增加了一个新的参数-XX:+UseConditionCardMark,用来决定是否开启卡表更新条件判断,开启后会增加一次额外判断开销。
想解决或降低用户线程的停顿,就要先搞清楚为什么必须在一个能保障一致性的快照上才能进行对象图的遍历?为了能解释清除这个问题,引入了三色标记(Tri-colorMarking)作为工具来辅助推导,把遍历对象图过程中遇到的对象,按照“是否访问过”这个条件标记成以下三种颜色:
当用户线程与收集器是并发工作的,那么收集器在对象图上标记颜色,同时用户线程在修改引用关系——即修改对象图的结构,这样会出现两种后果:一种是把原来消亡的对象错误标记伪存活,虽然不是好事,但可以容忍只是产生一点逃过本次收集的浮动垃圾而已,下次收集清理掉就好。另一种把原来存活的对象误标为死亡,这就是非常致命的后果,程序肯定会因此发生错误。
理论上证明了,当且仅当以下两个条件同时满足时,会产生“对象消失”的问题,即原本应该时黑色的对象被误标为白色:
所以只要破坏这两个条件种的一个,从而到达解决并发扫描时的对象消失问题。由此分别产生了两种解决方案:增量更新(Incremental Update)和原始快照(Snapshot At The Beginning ,SATB)。
增量更新是破环了上述的第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束后,再将这些记录过的引用关系的黑色对象为根,重新扫描一次。这样可以简化理解为:当黑色对象一旦新插入了指向白色对象的引用后,它自身就变回了灰色对象了。
原始快照破坏的第二条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用关系记录下来,再并发扫描结束之后,再将这些记录过的引用关系中灰色对象为根,重新扫描一次,这可以简化理解为:无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照图来进行搜索。
无论是引用关系的插入还是删除,虚拟机的记录操作都是通过写屏障实现的。在HotSpot虚拟机中,增量更新和原始快照这两种解决方案都有实际的应用,譬如,CMS 是基于增量更新做并发标记,G1、Shenandoah则是用原始快照来实现的。