上海房地产网站建设报价,app页面设计图片,福州网站建设哪家强,结婚证一键制作本文已收录至GitHub#xff0c;推荐阅读 #x1f449; Java随想录
微信公众号#xff1a;Java随想录 原创不易#xff0c;注重版权。转载请注明原作者和原文链接 文章目录 跨代引用问题记忆集卡表写屏障写屏障的伪共享问题 前面我们讲了可达性分析和根节点枚举#xff0c…本文已收录至GitHub推荐阅读 Java随想录
微信公众号Java随想录 原创不易注重版权。转载请注明原作者和原文链接 文章目录 跨代引用问题记忆集卡表写屏障写屏障的伪共享问题 前面我们讲了可达性分析和根节点枚举介绍完了GC的前置工作下面开始讲GC的工作过程。 然而在GC开始工作之前有一个不得不解决的问题摆在我们面前「跨代引用问题」。
本篇文章就来聊聊什么是跨代引用问题以及JVM是如何解决跨代引用问题的。
跨代引用问题
跨代引用是指新生代中存在对老年代对象的引用或者老年代中存在对新生代的引用。
为什么说这是一个问题呢请看下图。 假如现在要进行一次只局限于新生代区域的YGC但新生代中的对象是完全有可能被老年代所引用的为了找到新生代中的存活对象不得不遍历整个老年代来确保可达性分析结果的正确性。
首先我们得明确一点跨代引用是极少的这很重要。
举个例子说明如果某个新生代对象存在跨代引用由于老年代对象难以消亡该引用会使得新生代对象在收集时同样得以存活进而在年龄增长之后晋升到老年代中这时跨代引用也随即被消除了。
这简直就是原子弹炸鸟起重机吊鸡毛。因为跨代引用是极少的为了找出那么一点点跨代引用却得遍历整个老年代
而JVM里GC回收无疑是非常频繁的动作如果每次都这么搞性能肯定吃不消无疑会为内存回收带来很大的性能负担。 别慌JVM的设计者已经考虑到了这个场景并想到了解决办法那就是使用一种叫做「记忆集Remembered Set」的数据结构。
记忆集
记忆集位于新生代中是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。用以避免把整个老年代加进GC Roots扫描范围。 记忆集的作用和我们之前讲的OopMap很相似维护了类似一种映射表的关系避免了全局扫描本质是用空间换时间。
此后当发生YGC时只要把记忆集加进来一起扫描就能知道新生代对象被老年代引用的情况而不必扫描整个老年代
虽然说增加了维护记忆集的成本但比起收集时扫描整个老年代来说这波还是血赚
上面不知道大家有没有留意我的说辞「抽象数据结构」。意思就是说记忆集是一种逻辑上的概念并没有规定具体的实现类似方法区。
在HotSpot中采用卡表去实现记忆集。可以把记忆集和卡表的关系理解为Map跟HashMap。
卡表
卡表可以理解为是记忆集的具体实现英文叫Card Table。
垃圾收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向了收集区域的指针就可以了并不需要了解这些跨代指针的全部细节。
那设计者在实现记忆集的时候便可以选择更为粗犷的记录粒度来节省记忆集的存储和维护成本下面列举了一些可供选择当然也可以选择这个范围以外的的记录精度 其中第三种「卡精度」所指的就是「卡表」的方式去实现记忆集 这也是目前最常用的一种记忆集实现形式HotSpot采用的就是卡表。
在HotSpot虚拟机里面卡表采用的是字节数组的形式。以下这行代码是HotSpot默认的卡表标记逻辑
CARD_TABLE [this address 9] 0;字节数组CARD_TABLE的每一个元素都对应着其标识的内存区域中一块特定大小的内存块这个内存块被称作「卡页Card Page」。
一般来说卡页大小都是以2的N次幂的字节数通过上面代码可以看出HotSpot中使用的卡页是2的9次幂即512字节。
意味着如果卡表标识内存区域的起始地址是0x0000的话数组CARD_TABLE的第0、1、2号元素分别对应了地址范围为0x00000x01FF、0x02000x03FF、0x04000x05FF的卡页内存块 如图所示 一个卡页的内存中通常包含不止一个对象只要卡页内有一个或更多对象的字段存在着跨代指针那就将对应卡表的数组元素的值标识为1称为这个元素变脏Dirty没有则标识为0。
简单来说就是卡页的字节数组只有0和1两种状态1表示哪些内存区域存在跨代指针那么只要把1的加入GC Roots中一并扫描就能知道哪些进行跨代引用了这样就不用挨个去扫描了。
OK到了这步我们的思路就清晰了。
可以把老年代划分为一个个内存区域每块内存区域分别对应卡表的元素然后把卡表中变脏的元素直接加入GC Roots中一并扫描跨代引用问题就迎刃而解了。 如图对象A在老年代 0x00000x01FF 内存区域被引用那只要把对应的卡表标记为1YGC的时候扫描卡表就能知道对象A被老年代哪块内存区域引用了。
but我们还剩下一个问题卡表元素如何维护类似问题OopMap也遇到过。
卡表元素如何维护何时变脏谁来把它们变脏 HotSpot解决的办法是使用写屏障。
写屏障
先来解决何时变脏的问题这个问题很简单即其他分代区域中对象引用了本区域对象时其对应的卡表元素就应该变脏变脏时间点原则上应该发生在引用类型字段赋值的那一刻。
但问题是如何变脏即如何在对象赋值的那一刻去更新维护卡表。
在HotSpot虚拟机里是通过「写屏障Write Barrier」解决的。 注意这里提到的 写屏障 和 volatile 的写屏障不是一回事。 写屏障可以看作在虚拟机层面对「引用类型字段赋值」这个动作的AOP切面在引用对象赋值时会产生一个环形Around通知。用过Spring的弟兄们对AOP肯定不陌生。 在赋值前的部分的写屏障叫作「写前屏障Pre-Write Barrier」在赋值后的则叫作「写后屏障Post-Write Barrier」。
HotSpot虚拟机的许多收集器中都有使用到写屏障但直至G1收集器出现之前其他收集器都只用到了写后屏障。
应用写屏障后虚拟机就会为所有赋值操作生成相应的指令一旦收集器在写屏障中增加了更新卡表操作无论更新的是不是老年代对新生代对象的引用每次只要对引用进行更新就会产生额外的开销不过这个开销与YGC时扫描整个老年代的代价相比还是低得多的。
当引入一个解决方案的时候随之而来的可能还有其他问题。卡表在高并发场景下还面临着「伪共享False Sharing」问题。
写屏障的伪共享问题
伪共享是处理并发底层细节时一种经常需要考虑的问题号称并发的「隐形杀手」。
现代中央处理器的缓存系统中是以缓存行Cache Line为单位存储的当多线程修改互相独立的变量时如果这些变量恰好共享同一个缓存行就会彼此影响写回、无效化或者同步而导致性能降低。 core1 更新 A同时 core2 更新 B由于数据的读取和更新是以「缓存行」为单位的这就意味着当这两件事同时发生时就产生了竞争导致 core1 和 core2 有可能需要重新刷新自己的数据缓存行被对方更新了最终导致系统的性能大打折扣这就是伪共享问题。
为了避免伪共享问题一种简单的解决方案是不采用无条件的写屏障而是先检查卡表标记只有当该卡表元素未被标记过时才将其标记为变脏。
即将卡表更新的逻辑变为以下代码所示
if (CARD_TABLE [this address 9] ! 0)
CARD_TABLE [this address 9] 0;相当于说其实就是多了一个「if 判断条件」。
在JDK 7之后HotSpot虚拟机增加了一个新的参数「-XXUseCondCardMark」此参数默认是关闭的用来决定是否开启卡表更新的条件判断。
开启会增加一次额外判断的开销但能够避免伪共享问题两者各有性能损耗是否打开要根据应用实际运行情况来进行测试权衡。
看到这本篇文章就结束啦这章讲了跨代引用和记忆集。
GC收集还有很多是需要我们去搞清楚的。知道的越多不知道的越多这只是个开端一起期待下篇的「三色标记算法」吧。 感谢阅读如果本篇文章有任何错误和建议欢迎给我留言指正。 老铁们关注我的微信公众号「Java 随想录」专注分享Java技术干货文章持续更新可以关注公众号第一时间阅读。 一起交流学习期待与你共同进步