分代收集
当前商业虚拟机的垃圾收集器大多采用“分代收集”的方案
这主要基于两个分代假说之上
- 弱分代假说:绝大多数对象都是朝生夕灭的
- 强分代假说:熬过越多次垃圾收集器过程的对象就越难以消亡
在我看来,这两句话可以简化为:对象基本都是两级分化的(要么活的特别久[如spring mvs的controller组件,他会与jvm共存亡知道jvm终止或者人为移除他,但这种情况很少见]要么只活一下[如一个单独的业务方法中产生的对象,这种对象通常是业务代码执行完返回后就没有用了]),并且大多数都是活不了多久的对象,一个单独业务方法中说不定就能创建上百个对象,而这种在虚拟机栈栈帧被pop之后就算垃圾了。
所以基于这个理论可以将对象分到不同区域去存储,首先刚刚创建的对象放到统一一个分区中(年轻代),如果他能熬过垃圾回收,就判定他为“难以消亡的对象”,此时把他放到老年代内存区中,这块内存很少进行垃圾回收,也就加快了垃圾回收的效率。
依此,把分代收集理论放到具体的java虚拟机中,设计者就会至少将堆区划分为两个部分(新生代和老年代)。
跨代引用问题
因为现在将对象分到了不同的分区中,就会存在跨代引用问题,这通常是指新生代中有对象被老年代中的对象引用了,而minor gc只会从gc roots中枚举出新生代中的root对象然后向下通过引用进行标记,如果引用了就是代表被引用了的对象,否则就代表是垃圾。但是如果老年代中有对象对新生代中的某个对象有引用,那将是发现不了的,因为引用链在遇到是其他非当前gc分区的时候是不会继续向下追踪的
记忆集和卡表
首先贴出《深入理解Java虚拟机》书中对记忆集的解释
记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构,如果我们不考虑效率和成本的话,最简单的实现可以用非手机区域中所有含跨代引用的对象数组来实现这个数据结构
class RemerberdSet{
Object[] set[{跨代引用数}];
}
但是这样做的空间占用和维护成本较高,所以可以采用一种更高细粒度的方法来记录,只需要记录某一块内存范围是否存在跨代引用。
总的来说:卡表是记忆集的一种具体实现方式。
卡表的最简单形式可以只是一个字节数组(Hotspot也确实是这样做的)
CARD_TABLE [this address >> 9] = 0;
右移九位代表每个卡表保存一块大小为512字节的堆内存块
这样相比较于原先傻瓜式的记录每个跨代引用的对象引用光是内存方面就节省了数倍。
一个卡页的内存中通常包含不止一个对象,只要卡页里有一个对象存在着跨代指针,那就将对应卡表的数组元素的值标识为1,称这个元素变脏,没有则标识为0,在垃圾收集发生时,只要筛选出卡表变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把他们加入GCRoots中一并扫描。
看到把他们加入GCRoots中一并扫描,就基本明白了,将他们重新逻辑加入引用链中就可以继续向下扫描,判断这个跨代引用的对象是否是垃圾。
写屏障
《深入理解Java虚拟机》的一段话:
卡表元素何时变脏的答案是很明确的——有其他分代区域中的对象引用了本区域对象时,其对应的卡表元素就应该变脏…
加入是解释执行的字节码那相对好处理,虚拟机负责每条字节码指令的执行,有充分的接入空间,但在编译执行的场景中就必须找到一个在机器码层面的手段,把维护卡表的动作当道每一个赋值操作之中。
上文已经知道了如何解决跨代引用问题,但是如何去维护这个卡表还是个问题,这就提出了写屏障这种手段,虽然这看似很高级,但实际就是在每次赋值操作前的切面进行一系列维护卡表的操作。
伪共享
同时,写屏障在高并发场景下还会有伪共享问题:
现代CPU的缓存系统是以缓存行为单位存储,当多线程修改互相独立的变量,如果这些变量恰好共享一个缓存行,他们就会变为同步进行,影响性能,为了解决这个问题可以在标记卡表前判断一下当前卡表元素是否已经被标记
if (CARD_TABLE[this address >> 9] != 0)
CARD_TABLE[this address >> 9] = 0
对于这个条件并不是强制开启判断的,JDK7以后,HotSpot虚拟机增加了一个新的参数-XX:+UseCondCardMark用来决定是否开启卡表更新的条件判断