电子说
上面说到过现在虚拟机采用的几乎都是主动式中断来中断线程,而其实现又是通过 「线程执行过程中不断轮询标志位」 产生自陷异常信号在异常处理表中进行中断线程,
大家有没有发现有个小bug:如果我轮询的操作一直得不到执行呢?这个时候我又该如何让虚拟机进入垃圾回收状态。
其实不一定都需要进行中断线程来保证,回想下STW是为什么:因为如果这个时候用户线程还在执行的话内存中的引用关系可能会发生变化,所以才需要进行STW。如果一个线程没有得到CPU时间片执行(java中的线程对应于操作系统的线程,对应关系也可以找笔者之前的关于SignCatcher对线程的理解进行查阅),但是我可以确保其中一部分代码区域是不会改变内存引用关系的,这样也可以不用管这些线程。
引入Safe Region(安全区域)解决
❝
“安全区域:这部分代码不会使内存中的引用关系发生变化”,因此只要进入了安全区域,虚拟机就不会管这些线程。当线程离开安全区域后,如果这个时候引用链还没有形成(也就是通过GC Roots遍历堆内存)那么是不能离开的,一直等待直至引用链形成(或者完成了垃圾回收器需要暂停用户线程的阶段)收到信号为止。
❞
根据堆中的不同区域(分代设计)和回收内存空间来判定分为不同的GC名称:局部回收:Minor GC,MajorGC,..... 整个内存回收:Full GC
如果存在“跨代引用”(最典型的比如老年代对象引用年轻代对象),比如发生Minor GC时,只遍历普通的GC Roots对象其实结果并不准确( 「某些对象虽然本身不属于GC Roots但是随着经历的GC次数变多成为老年代对象」 ),如果这个时候将这个引用的年轻代对象标记为垃圾清除后,老年代中的对象就会有问题,所以引用链形成的过程中还需要 「遍历整个老年代来保证结果准确」 。
!
跨域可以理解为跨内存访问或者访问其他分代里面的内存
上面遍历整个老年代这个过程听起来就很耗时哈哈,事实也确实如此。那么我们可以引入这么一个概念:如果你引用了其他内存里面的对象那么我把你存放到其他内存里面的一个数据结构里面,之后其他内存回收的时候只需要把之前添加到数据结构里面的对象加入到GC Roots中即可。
我们优化一下: 「每个不同的分代中都存着一个数组」 ,这个数组中对堆内存进行一个映射, 我数组中的每一小块对应的元素是分代中固定大小的内存(比如我第一个数组下标表示我引用的是0到100,第二个数组下标表示引用的是100-200以此类推)。 「当我第一个数组下标对应内存跨域引用了其他分代中的内存,我将把第一个数组下标对应的内存的元素值标识为1代表脏(Dirty)」 ,没有则为0。当垃圾回收时,我就知道哪部分内存是跨代引用并将他们加入到GC Roots进行扫描(将数组中元素为1对应的内存对象加入GC Roots中)。
根据我映射的内存大小精度又可以进行细分:1.字长精度:只记录一个机器字长(处理器的寻址位数)该字包含跨代指针
2.对象精度:记录一个对象(对象字段中含有跨代指针)
3.卡精度:记录一块内存区域(该区域有对象包含跨代指针)
采用“卡精度”的记忆集是通过“卡表”这个数据结构来实现的。
使用精度为卡,这个记忆集的实现方式也被称为 「卡表」 ,卡表中其实是字节数组结构,每个数组中的元素都对应一部分指定大小内存块,这部分内存被称作 「卡页」 ,当卡页中的内存块中引用了其他的内存块中的一个或多个对象,就会将卡页中的元素值变为一。变为一的就是脏数据,收集时讲这部分内存加入到gc roots中。也就是这样的:
一, 「何时进行更新卡表?」 先看我这张图哈哈,字不好看,但是大致意思是差不多的。
❝
我在写后屏障中进行更新卡表就可以保证我的卡表记录是正确的。
❞
二, 「“伪共享引起的问题”」 上面刚刚讲过CPU的缓存行技术,简单来说就是如果两个线程中两个独立的变量在同一块缓存行中,那么不管是哪个线程修改,另外一个线程都需要重新从主存中读取,而设置缓存行就是为了加快读取效率,所以这样势必会降低效率。
想想刚刚我们记忆集处理方式,如果卡页对应的内存中发生跨代引用,那么就会对卡表进行更新;上面说的“伪共享”也会在这里出现而且影响性能,比如: 「一个缓存行六十四个字节;一个卡表中的一个元素是一个字节,每个元素对应的一个卡页存储的是512字节,也就是一个卡表中64个元素在一个缓存行,而这64个元素对应的总卡页内存为32KB(64 X 512字节)」 ,如果两个线程中的变量分配到了这部分内存中,之后变量发生跨代引用更新卡表元素时就会导致另一个线程的缓存行失效而从主存中去拿。所以应该减少更新卡表这个操作,如果已经更新过脏数据了就不需要进行更新卡表了。
全部0条评论
快来发表一下你的评论吧 !