咱们知道,JVM在进行垃圾收集时,须要先标记全部可达对象,而后再清除不可达对象,释放内存空间。那么,如何快速的找到全部可达对象呢?数组
最简单粗暴的实现,就是每次进行垃圾收集时,都对整个堆中的全部对象进行扫描,找到全部存活对象。逻辑是简单,但性能比较差。缓存
简单粗暴的实现方式,一般都是不可取的。那JVM是如何实现快速标记可达对象的?bash
答案是GC Roots。并发
GC Roots是垃圾收集器寻找可达对象的起点,经过这些起始引用,能够快速的遍历出存活对象。GC Roots最多见的是静态引用和堆栈的局部引用变量。然而,这不是咱们这讲的重点:)高并发
现代JVM,堆空间一般被划分为新生代和老年代。因为新生代的垃圾收集一般很频繁,若是老年代对象引用了新生代的对象,那么,须要跟踪从老年代到新生代的全部引用,从而避免每次YGC时扫描整个老年代,减小开销。post
对于HotSpot JVM,使用了卡标记(Card Marking)技术来解决老年代到新生代的引用问题。具体是,使用卡表(Card Table)和写屏障(Write Barrier)来进行标记并加快对GC Roots的扫描。性能
基于卡表(Card Table)的设计,一般将堆空间划分为一系列2次幂大小的卡页(Card Page)。this
卡表(Card Table),用于标记卡页的状态,每一个卡表项对应一个卡页。spa
HotSpot JVM的卡页(Card Page)大小为512字节,卡表(Card Table)被实现为一个简单的字节数组,即卡表的每一个标记项为1个字节。线程
当对一个对象引用进行写操做时(对象引用改变),写屏障逻辑将会标记对象所在的卡页为dirty。
OpenJDK/Oracle 1.6/1.7/1.8 JVM默认的卡标记简化逻辑以下:
CARD_TABLE [this address >> 9] = 0;
复制代码复制代码
首先,计算对象引用所在卡页的卡表索引号。将地址右移9位,至关于用地址除以512(2的9次方)。能够这么理解,假设卡表卡页的起始地址为0,那么卡表项0、一、2对应的卡页起始地址分别为0、5十二、1024(卡表项索引号乘以卡页512字节)。
其次,经过卡表索引号,设置对应卡标识为dirty。
每次对引用的更新,不管是否更新了老年代对新生代对象的引用,都会进行一次写屏障操做。显然,这会增长一些额外的开销。可是,与YGC时扫描整个老年代相比较,这个开销就低得多了。
不过,在高并发环境下,写屏障又带来了虚共享(false sharing)问题。
在高并发状况下,频繁的写屏障很容易发生虚共享(false sharing),从而带来性能开销。
假设CPU缓存行大小为64字节,因为一个卡表项占1个字节,这意味着,64个卡表项将共享同一个缓存行。
HotSpot每一个卡页为512字节,那么一个缓存行将对应64个卡页一共64*512=32KB。
若是不一样线程对对象引用的更新操做,刚好位于同一个32KB区域内,这将致使同时更新卡表的同一个缓存行,从而形成缓存行的写回、无效化或者同步操做,间接影响程序性能。
一个简单的解决方案,就是不采用无条件的写屏障,而是先检查卡表标记,只有当该卡表项未被标记过才将其标记为dirty。
这就是JDK 7中引入的解决方法,引入了一个新的JVM参数-XX:+UseCondCardMark,在执行写屏障以前,先简单的作一下判断。若是卡页已被标识过,则再也不进行标识。
简单理解以下:
if (CARD_TABLE [this address >> 9] != 0)
CARD_TABLE [this address >> 9] = 0;
复制代码复制代码
与原来的实现相比,只是简单的增长了一个判断操做。
虽然开启-XX:+UseCondCardMark以后多了一些判断开销,可是却能够避免在高并发状况下可能发生的并发写卡表问题。经过减小并发写操做,进而避免出现虚共享问题(false sharing)。
CMS在并发标记阶段,应用线程和GC线程是并发执行的,所以可能产生新的对象或对象关系发生变化,例如:
对于这些对象,须要从新标记以防止被遗漏。为了提升从新标记的效率,并发标记阶段会把这些发生变化的对象所在的Card标识为Dirty,这样后续阶段就只须要扫描这些Dirty Card的对象,从而避免扫描整个老年代。