郑州 网站建设的公司,餐饮设计网站建设,让百度收录自己的网站,房地产网站开发商一、根节点枚举 固定可作为GC Roots的节点主要在全局性的引用#xff08;如常量或类静态属性#xff09;与执行上下文#xff08;如栈帧中的本地变量表#xff09;中#xff0c;尽管目标明确#xff0c;但查找要做到高效很难。现在java应用越来越庞大#xff0c;光方法区…一、根节点枚举 固定可作为GC Roots的节点主要在全局性的引用如常量或类静态属性与执行上下文如栈帧中的本地变量表中尽管目标明确但查找要做到高效很难。现在java应用越来越庞大光方法区的大小就常有数百上千兆里面的类、常量等更是恒河沙数逐个检查以这里为起源的引用肯定得消耗不少时间。 同时迄今为止所有收集器在根节点枚举这一步时都是必须暂停用户线程的。根节点枚举必须在一个保障一致性的快照中进行。一致性的意思是整个枚举期间执行子系统看起来就像被冻结在某一个时间点上不会出现分析过程中根节点集合的对象引用关系还在不断的变化的情况若这点不能满足分析结果准确性也就无法保证。 由于目前主流java虚拟机使用的都是准确式垃圾收集准确式内存管理虚拟机可以知道内存中某个位置的数据具体是什么类型所以当用户线程停顿时不需要一个不漏的检查完所有执行上下文和全局的引用位置虚拟机应当是有办法直接得到哪些地方存放着对象的引用。在HotSpot的解决方案里是使用一组称为OopMap的数据结构来达到这个目的。 类加载完成时HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来在即时编译过程中也会在特定的位置记录下栈里和寄存器里哪些位置时引用。这样收集器扫描时就可以直接知道这些信息并不需要真正一个不漏从方法区等GC Roots考试查找。 下面的代码是 HotSpot 虚拟机客户端模式下生成的一段 String::hashCode() 方法的本地代码可以看到在0x026eb7a9 处的 call 指令有 OopMap 记录它指明了 EBX 寄存器和栈中偏移量为 16 的内存区域 中各有一个普通对象指针Ordinary Object Pointer OOP 的引用有效范围为从 call 指令开始直到0x026eb730指令流的起始位置 142 OopMap 记录的偏移量 0x026eb7be 即 hlt 指令为止。 二、安全点 HotSpot没有为每一条指令都生成OopMap,上面提到的“类加载完成时HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来在即时编译过程中也会在特定的位置记录下栈里和寄存器里哪些位置时引用”中提到的特定的位置记录了这些信息这些位置被称为安全点Safepoint。因此用户程序执行时并非在任意位置都能停下来进行垃圾收集强制要求必须执行到安全点后才能暂停。所以安全点的选定既不能太少以至于让收集器等待时间过长也不能太频繁以至于过分增大运行时的内存负荷。 安全点位置的选取标准是否具有让程序长时间执行的特征。因为每条指令执行的时间都非常短暂程序不太可能因为指令流长度太长这样的原因而长时间执行长时间执行的最明显特征就是指令序列的复用例如方法调用、循环跳转、异常跳转等都属于指令序列复用所以只有具有这些功能的指令才会产生安全点。 如何让所有线程都跑到最近的安全点停顿下来 抢先式中断 抢先式中断不需要线程的执行代码主动去配合。 在垃圾收集发生时系统首先把所有用户线程全部中断如果发现有用户线程中断的地方不在安全点上就恢复这条线程执行让它一会儿再重新中断直到跑到安全点上。 现在几乎没有虚拟机实现采用抢先式中断来暂停线程响应GC事件。 主动式中断 设置一个标志位线程执行时会不停的主动轮询这个标志一旦发现中断标志位为真自己在最近的安全点上主动中断挂起。 轮询标志的地方和安全点是重合的另外还要加上所有创建对象和其他需要在Java堆上分配内存的地方这是为了检查是否即将要发生垃圾收集避免没有足够内存分配新对象。 由于轮询操作在代码中会频繁出现这要求它必须足够高效。HotSpot 使用内存保护陷阱的方式把轮询操作精简至只有一条汇编指令的程度。 下面代码清单中的 test 指令就是 HotSpot 生成的轮询指令当需要暂停用户线程时虚拟机把0x160100 的内存页设置为不可读那线程执行到 test 指令时就会产生一个自陷异常信号然后在预先注册的异常处理器中挂起线程实现等待这样仅通过一条汇编指令便完成安全点轮询和触发线程中断了。 三、安全区域 程序“不执行”时没有分配处理器时间如sleep和blocked状态线程无法响应虚拟机的中断请求不能走到安全点主动挂起虚拟机也不可能等线程重新被激活。所以引入安全区域。 安全区域是指能够确保在某一段代码片段之中引用关系不会发生改变。因此在这个区域中任意地方开始垃圾收集都是安全的。可以看作扩展延伸了的安全点。 进入安全区域的代码会标识自己已经进入安全区域虚拟机发起垃圾收集时不用去管这些线程。当线程要离开安全区域时他要检查虚拟机是否已经完成了根节点枚举或垃圾收集过程中其他需要暂停用户线程的阶段完成那线程就当作没事发生继续执行。否则一直等待直到收到可以离开安全区域的信号。
四、记忆集与卡表 在分代收集中为了解决对象跨代引用所带来的问题在新生代中建立记忆集的数据结构用以避免把整个老年代加进GC Roots扫描范围。事实上所有部分区域收集行为的垃圾收集器都会有跨代引用问题。 记忆集是一种记录从非收集区域指向收集区域的指针集合的抽象数据结构。不考虑效率和成本最简单的实现用非收集区域中所有含跨代引用的对象数组来实现这个数据结构。 在垃圾收集场景中收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向收集区域的指针就行不需要了解跨代指针的全部细节。 设计者在实现记忆集的时候便可以选择更为粗犷的记录粒度来节省记忆集的存储和维护成本下面列举了一些可供选择当然也可以选择这个范围以外的的记录精度
字节精度每个记录精确到机器字长就是处理器的寻址位数如常见的32位或64位这个精度决定了机器访问物理内存地址的指针长度该字包含跨代指针。对象精度每个记录精确到一个对象该对象里有字段含有跨代指针。卡精度每个记录精确到一块内存区域该区域内有对象含有跨代指针。用一种称为“卡表”的方式去实现记忆集是常用记忆集实现方式之一。 记忆集其实是一种“抽象 ” 的数据结构抽象的意思是只定义了记忆集的行为意图并没有定义其行为的具体实现。卡表就是记忆集的一种具体实现它定义了记忆集的记录精度、与堆内存的映射关系等。 卡表最简单的形式可以只是一个字节数组下面这行代码是HotSpot默认的卡表标记逻辑
CARD_TABLE[this address 9] 0;字节数组CARD_TABLE 的每一个元素都对应着其标识的内存区域中一块特定大小的内存块这个内存块被称作“ 卡页 ” Card Page 。一般来说卡页大小都是以 2 的 N 次幂的字节数通过上面代码可以看出HotSpot 中使用的卡页是 2 的 9 次幂即 512 字节地址右移 9 位相当于用地址除以 512 。那如果卡表标识内存区域的起始地址是0x0000 的话数组 CARD_TABLE 的第 0 、 1 、 2 号元素分别对应了地址范围为0x0000 0x01FF 、 0x0200 0x03FF 、 0x0400 0x05FF 的卡页内存块如下图所示。 一个卡页的内存中通常包含不止一个对象只要卡页内有一个或更多对象的字段存在着跨代指针那就将对应卡表的数组元素的值标识为1称为这个元素变脏 Dirty 没有则标识为 0 。在垃圾收集发生时只要筛选出卡表中变脏的元素就能轻易得出哪些卡页内存块中包含跨代指针把它们加入GC Roots 中一并扫描。 五、写屏障 何时变脏有其他分代区域中对象引用了本区域对象时其对应的卡表元素就应该变脏变脏时间点原则上应该发生在引用类型字段赋值的那一刻。 如何变脏即如何更新维护卡表
若是解释执行的字节码虚拟机负责每条字节码指令的执行有充分的介入空间。若是编译执行经过即时编译后的代码已经是纯粹的机器指令流了这就必须找到一个在机器码层面的手段把维护卡表的动作放到每一个赋值操作之中。 HotSpot通过写屏障Writer Barrier技术维护卡表。写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切面在引用对象赋值时会产生一个环形Around通知供程序执行额外的动作也就是说赋值前后都在写屏障的覆盖范围内。在赋值前的部分的写屏障叫做写前屏障Pre-Write Barrier赋值后则叫写后屏障Post-Write Barrier。
G1之前只用到写后屏障。
写后屏障更新卡表如下图。 除了写屏障开销外相较于扫描整个老年代的代价低卡表在高并发下面临着“伪共享”问题。中央处理器的缓存系统是以缓存行为单位存储多线程修改独立变量这些变量恰好共享同一个缓存行就会彼此影响写回无效化或同步而导致性能降低。 解决伪共享办法不采用无条件的写屏障先检查卡表标记只有卡表元素未被标记过时才将其标记变脏即卡表更新逻辑变为
if(CARD_TABLE[this address 9] ! 0)CARD_TABLE[this address 9] 0;在JDK 7 之后 HotSpot 虚拟机增加了一个新的参数 -XX UseCondCardMark 用来决定是否开启卡表更新的条件判断。开启会增加一次额外判断的开销但能够避免伪共享问题两者各有性能损耗是否打开要根据应用实际运行情况来进行测试权衡。 六、并发的可达性分析 在根节点枚举这个步骤中GC ROOTS相比起整个java堆中全部的对象已经减少了很多且在各种优化技巧如OopMap的加持下它带来的停顿已经非常短暂且相对固定。可从GC Roots继续往下遍历对象图这一步骤的停顿时间必定与java堆容量成正比关系堆越大存储的对象越多对象图结构越复杂要标记更多对象而产生的停顿时间的更长。 要知道包含“ 标记 ” 阶段是所有追踪式垃圾收集算法的共同特征如果这个阶段会随着堆变大而等比例增加停顿时间其影响就会波及几乎所有的垃圾收集器同理可知如果能够削减这部分停顿时间的话那收益也将会是系统性的。 首先了解一下为什么在一个能保障一致性的快照下才能进行对象图的遍历我们使用三色标记辅助推导 白色对象尚未被垃圾收集器访问到。在可达性分析刚刚开始阶段所有阶段对象都是白色分析结束阶段仍为白色即代表不可达。黑色对象已经被垃圾收集器访问过且这个对象的所有引用都已经扫描过。黑色对象代表已经扫描过它是安全存活的如有其他对象引用指向黑色对象无须重新扫描。黑色对象不可能直接不经过灰色对象指向某个白色对象。灰色对象已经被垃圾收集器访问过但这个对象上至少存在一个引用还没有被扫描过。 可达性分析的扫描过程可以看作对象图上一股以灰色为波峰的波纹从黑向白推进的过程。用户线程冻结不会有任何问题。但用户线程并发收集器在标记时用户线程在修改引用会导致两种结果一种是把原本消亡的对象错误标记为存活即产生浮动垃圾下次收集即可可以容忍。另一种是把原本存活的对象标记为已消亡这就很致命了程序肯定会因此发生错误下面演示这样的致命错误是怎样产生的。 “对象消失”问题原本应该是黑色的对象被误标为白色。
“对象消失”问题产生的条件(需要同时满足)
赋值器插入了一条或多条从黑色对象到白色对象的新引用赋值器删除了全部从灰色对象到该白色对象的直接或间接引用
解决“对象消失”问题增量更新 和 原始快照。
增量更新破坏第一个条件当黑色对象插入新的指向白色对象的引用关系时就将这个新插入的引用记录下来等并发扫描结束后再将这些记录过的引用关系中的黑色对象为根重新扫描一次。简化理解为黑色对象一旦新插入了指向白色对象的引用之后他就变回灰色对象了。原始快照破坏第二个条件当灰色对象要删除指向白色对象的引用关系时就将这个要删除的引用记录下来在并发扫描结束之后再将这些记录过的引用关系中的灰色对象为根重新扫描一次。简化理解为无论引用关系删除与否都会按照刚刚开始扫描那一刻的对象图快照来进行搜索。 以上无论是对引用关系记录的插入还是删除虚拟机的记录操作都是通过写屏障实现的。 在 HotSpot虚拟机中增量更新和原始快照这两种解决方案都有实际应用譬如CMS 是基于增量更新来做并发标记的G1 、 Shenandoah 则是用原始快照来实现。