我们通过可达性分析算法从GC Roots中找到全局性的引用(例如常量或者类静态属性)或者是执行上下文(例如栈帧中的本地变量)中,尽管我们的目标非常明确,但是随着java的不断扩大,光一个方法区内的常量、类静态变量就有很多,我们通过该方法区一个一个查询,肯定效率上就有很大的消耗
在现存的收集器中在根节点枚举这一步骤都是要暂停用户线程的,但这样就会出现“stop the world”的现象(用户感觉到的就是系统的卡顿),我们作为软件的开发者,我们必须要降低“stop the world”现象的发生的次数,根节点枚举必须在一个保障一致性的快照中才得以进行(整个枚举期间执行子系统看起来像是在被冻结在某个时间点上)
现在Java虚拟机用的都是准确式垃圾收集,当所有的用户线程停顿下来的时候,我们不需要一个不漏的去检查所有执行上线文和全局的引用位置,Java虚拟机是使用OopMap的数据结构来直接得到哪些地方是存放着对象的引用,一旦类加载动作完成时,Java虚拟机就会将对象内的偏移量上是什么数据类型计算出来,在即时编译的时候,也会在特定的位置上记录下栈里和寄存器里哪些位置是引用,这样就避免了每次从GC Roots中开始查找
HotSpot没有为每条指令都生成一个OopMap,只是在特定的位置生成OopMap,这些特定的位置就是所谓的“安全点”,有了安全点的设定,也就意味着用户程序不能想当然的在任意位置都可以停顿下来进行垃圾收集,而是必须强制到所谓的安全点才可以停顿进行垃圾回收,安全点的选取也是有一定的说法的
安全点选取的标准是按照“是否让程序长时间执行的特征”,长时间执行最明显的特征就是指令序列的复用,具有这些功能的指令才会产生安全点
抢占式中断不需要线程的执行代码去主动配合,在垃圾收集发生时,系统会将所有的用户线程全部中断,如果发现某些线程中断的位置不是安全点的位置,然后就恢复这条线程的执行,然后过一会再进行中断,直到跑到安全点上
当垃圾收集需要中断线程的时候,不能直接对线程进行操作,仅仅的设置一个标志位,各个线程执行的过程中不停的去主动轮询这个标志,一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起
我们已经通过安全点已经解决了程序执行时让虚拟机进入垃圾回状态的问题,但是在程序不执行(所谓的程序不执行就是没有分配处理器的时间,例如用户线程处于Sleep状态或者Blocked状态)的时候呢?我们又有什么办法去进行垃圾回收呢?这时候我们就引入了安全区域来进行解决
安全区域就是能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中的任意地方开始进行垃圾收集都是安全的,我们也可以将安全区域看作为被伸长的一系列安全点
当线程要退出安全点的时候,它要检查虚拟机是否已经完成了根节点的枚举,如果完成了,那线程就当做什么事情也没发生过,继续执行,如果没有发生,它就必须一直等待,直到收到可以离开安全区域的型号为止
为了解决对象跨代引用所带来的问题,垃圾收集器在新生代建立了名为记忆集的数据结构,用于避免将整个老年代加进GC Roots扫描范围
记忆集时一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构(记忆集的数据结构可能不是上图我画的这样,感兴趣的同学可以自行下去查询资料进行了解)
在垃圾收集的场景中,收集器只需要判断出某一块非收集区域是否存在有指向了收集区域的指针就可以了,并不需要了解这些指针的细节,所有我们就可以选择一些粗犷的记录粒度来节省记忆集的存储和维护成本
在4.1中了解到的卡精度其实就是我们这里的卡页,这是目前最常用的一种记忆集实现形式
卡表最简单的形式只是一个字节数组
CARD_TABLE[this address>>9]=0;
字节数组的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个内存块被称之为“卡页”,一般来说,卡页大小都是以2的N次幂的字节数,一个卡页中内存中通常包含不止一个对象,只要卡页有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1,称之为这个元素变脏,没有则标识为0,在垃圾回收的时候,只要筛选出卡表中元素变脏的元素,就可以轻而易举的得出哪些卡页内存中存在跨代指针,将它们假如GC Roots中一并扫描
所谓的误标就是将“原本是黑色的对象被误标为白色的对象”,会产生对象消失,应该满足的条件:
在HotSpot虚拟机中,增量更新和原始快照这两种解决方案都是有实际应用的,例如,CMS是基于增量更新来做并发标记的,G1则是用原始快照来实现的