在分析JVM的分代垃圾收集算法的时候,可能存在老年代对新生代的引用,无法真正确定对象已死。
一、解决跨代引用:记忆集
记忆集(Remembered Set):一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构,在对象层面来说就是非收集区域对象对收集区域对象的引用的记录。
它存放在收集区域,比如在新生代里面存放着老年代对新生代对象的每一个引用。这样在收集新生代的时候,我们就可以根据记忆集知道哪些对象被老年代对象所引用,不能回收,这就解决了跨代引用的问题。
记忆集根据记录的精度分三类:
字长精度:记录的是老年代指向新生代地址。
对象精度:记录的是老年代引用的新生代对象。
卡精度:记录的是新生代一段地址是否存在被老年代引用的记录。
二、记忆集的实现:卡表
卡表(Card Table):是以第三种卡精度的方式实现的记忆集,也是目前最常用的方式。记忆集是抽象的概念,而卡表就是记忆集的一种具体实现。
卡表最简单的形式可以是一个字节数组,HotSpot就是这样实现的。
CARD_TABLE [this address >> 9] = 0;
大致示意图:
把地址的值右移9位相当于除于512就是卡表索引,每字节512为一组对应卡表同一个元素,一组就是一个卡页,如果这个卡页中只要有一个对象被其他区域对象所引用,对应卡表元素的值就变成1,也就是所谓的元素变脏。
在垃圾回收时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页对应的内存包含跨代指针,把他们加入GC Rootsz中一并扫描。
三、卡表数据的收集:写屏障
写屏障:可以看成是虚拟机层面在”引用类型字段赋值“这个动作的AOP切面,引用对象赋值的时候产生一个环形通知,进行一些额外的处理,这样就是引用对象赋值这个操作都在写屏障的覆盖范围内,赋值前的写屏障叫写前屏障,复制后的写屏障叫写后屏障。
这样我们就可以通过写屏障,一旦发生赋值操作就可以把引用的更新写进卡表。
四、并发的可达性标记
上一篇文章我们知道,有些垃圾收集器实现了用户线程和收集器的标记线程并发运行的场景,但是用户现场很可能造成引用的更改,那么标记对象可能就不准确。
像下面这种情况:
在可达性分析中我们把完成分析的标成黑色,正在分析的标成灰色,未分析的标成白色。在第一步正在分析对象B,对象C引用着对象D。这时用户线程使对象A引用对象D,而对象C不在引用对象D。由于A已经分析过了,不会再进行分析,最终就会造成对象D没有在引用链上而被回收,这样系统就会出现异常了!
要解决这个问题,首先就要分析出现这个问题出现的原因,要同时满足如下两个条件:
赋值器插入了一条或多条从黑色到白色对象的新引用;
赋值前删除了全部从灰色到该白色对象的直接或间接引用;
所以要解决这个问题也简单,只要破坏一个条件就不会出现了。最终有两种解决方案:增量更新(Incremental Update)和原始快照(Snapshot At The Beginning,SATB)
1、增量更新
只要有黑色对象新加了指向白色对象的引用关系,把这个新插入的引用记录下来,等并发扫描结束后,在将这个黑色对象为根重新扫描。
增量更新破坏了第一个条件,使新加的对象都能重新回到引用链上。
2、原始快照
当灰色对象删除指向白色对象的引用关系时,就记录这个将要删除的引用,并发扫描结束后,在以白色对象为根重新扫描一次。
原始快照破坏的是第二个条件,也就是说不管引用关系是否删除,都会按照扫描那一刻的对象图快照进行搜索。这样就保证了对象一定能在调用链上,不过会有少量确实应该回收的存在。
CMS用的增量更新,G1则是原始快照.
五、总结
记忆集记录着收集区域被其他区域引用的数据(地址,对象,内存段)。
卡表是记忆集的具体实现。卡表可以是一个简单的字节数组结构,数组的索引表示的是收集区域的一段内存区域,而元素的值就代表着对应的地址上是否被其他区域所引用。
JVM通过实现AOP切面对“引用对象赋值”操作进行监听,实现把跨代引用记录到记忆集中。
并发的可达性标记可能造成存活对象丢失。丢失的条件是对象从还没有被分析完成对象的引用中移除,然后又被加入到已经完成分析对象的引用中,导致对象没有标记在引用链上而被回收。解决的方案分别是增量更新(破坏第一条)、原始快照(破坏第二条)。
CMS收集器采用的是增量更新,G1则是采用的原始快照。
Java程序员日常学习笔记,如理解有误欢迎各位交流讨论!
领取专属 10元无门槛券
私享最新 技术干货