cms过程

这在HotSpot内存管理白皮书中得到了很好的解释: A collection cycle for the CMS collector starts with a short pause, called the initial mark, that identifies the initial set of live objects directly reachable from the application code. Then, during the concurrent marking phase, the collector marks all live objects that are transitively reachable from this set. Because the application is running and updating reference fields while the marking phase is taking place, not all live objects are guaranteed to be marked at the end of the concurrent marking phase. To handle this, the application stops again for a second pause, called remark, which finalizes marking by revisiting any objects that were modified during the concurrent marking phase. Because the remark pause is more substantial than the initial mark, multiple threads are run in parallel to increase its efficiency. At the end of the remark phase, all live objects in the heap are guaranteed to have been marked, so the subsequent concurrent sweep phase reclaims all the garbage that has been identified.

  • 初试标记
    初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。初始标记的过程是需要触发STW的,不过这个过程非常快,而且初试标记的耗时不会因为堆空间的变大而变慢,是可控的,因此可以忽略这个过程导致的短暂停顿。

  • 并发标记
    并发标记就是将初始标记的对象进行深度遍历,以这些对象为根,遍历整个对象图,这个过程耗时较长,而且标记的时间会随着堆空间的变大而变长。不过好在这个过程是不会触发STW的,用户线程仍然可以工作,程序依然可以响应,只是程序的性能会受到一点影响。因为GC线程会占用一定的CPU和系统资源,对处理器比较敏感。CMS默认开启的GC线程数是:(CPU核心数+3)/4,当CPU核心数超过4个时,GC线程会占用不到25%的CPU资源,如果CPU数不足4个,GC线程对程序的影响就会非常大,导致程序的性能大幅降低。

  • 重新标记
    由于并发标记时,用户线程仍在运行,这意味着并发标记期间,用户线程有可能改变了对象间的引用关系,可能会发生两种情况:一种是原本不能被回收的对象,现在可以被回收了,另一种是原本可以被回收的对象,现在不能被回收了。针对这两种情况,CMS需要暂停用户线程,进行一次重新标记。

  • 并发清理
    重新标记完成后,就可以并发清理了。这个过程耗时也比较长,且清理的开销会随着堆空间的变大而变大。不过好在这个过程也是不需要STW的,用户线程依然可以正常运行,程序不会卡顿,不过和并发标记一样,清理时GC线程依然要占用一定的CPU和系统资源,会导致程序的性能降低。

三色标记算法

标记的过程大致如下:

  • 刚开始,所有的对象都是白色,没有被访问。
  • 将GC Roots直接关联的对象置为灰色。
  • 遍历灰色对象的所有引用,灰色对象本身置为黑色,引用置为灰色。
  • 重复步骤3,直到没有灰色对象为止。
  • 结束时,黑色对象存活,白色对象回收。

这个过程正确执行的前提是没有其他线程改变对象间的引用关系,然而,并发标记的过程中,用户线程仍在运行,因此就会产生漏标和错标的情况。

漏标

原本不是垃圾,但是GC的过程中,用户线程将其引用关系修改,导致GC Roots不可达,成为了垃圾。这种情况还好一点,无非就是产生了一些浮动垃圾,下次GC再清理就好了。

  • 具体场景
    假设GC已经在遍历对象B了,而此时用户线程执行了A.B=null的操作,切断了A到B的引用。

本来执行了A.B=null之后,B、D、E都可以被回收了,但是由于B已经变为灰色,它仍会被当做存活对象,继续遍历下去。
最终的结果就是本轮GC不会回收B、D、E,留到下次GC时回收,也算是浮动垃圾的一部分。

错标

原本是垃圾,但是GC的过程中,用户线程将引用重新指向了它,这时如果GC一旦将其回收,将会导致程序运行错误。

具体场景:比如 GC线程到对象B的时候,业务线程建立了一个临时变量tmp指向C,比如tmp = C ,然后让B.C=null,切断引用,再让A去引用tmp ,A.C = tmp,这样就建立起A到C链接了。而GC线程到B的时候由于被C被切断,就不会把C标记成黑色,C就会被当成垃圾了。

  • 具体场景
    假设GC线程已经遍历到B了,此时用户线程执行了以下操作:
B.D=null;//B到D的引用被切断
A.xx=D;//A到D的引用被建立


B到D的引用被切断,且A到D的引用被建立。
此时GC线程继续工作,由于B不再引用D了,尽管A又引用了D,但是因为A已经标记为黑色,GC不会再遍历A了,所以D会被标记为白色,最后被当做垃圾回收。
可以看到错标的结果比漏表严重的多,浮动垃圾可以下次GC清理,而把不该回收的对象回收掉,将会造成程序运行错误。

错标只有在满足下面两种情况下才会发生:

只要打破任一条件,就可以解决错标的问题。

原始快照和增量更新

  • 原始快照打破的是第一个条件:当灰色对象指向白色对象的引用被断开时,就将这条引用关系记录下来。当扫描结束后,再以这些灰色对象为根,重新扫描一次。相当于无论引用关系是否删除,都会按照刚开始扫描时那一瞬间的对象图快照来扫描。

  • 增量更新打破的是第二个条件:当黑色指向白色的引用被建立时,就将这个新的引用关系记录下来,等扫描结束后,再以这些记录中的黑色对象为根,重新扫描一次。相当于黑色对象一旦建立了指向白色对象的引用,就会变为灰色对象。

  • 写屏障
    这个写屏障指的可不是并发编程里的写屏障哦!这里的写屏障指的是属性赋值的前后加入一些处理,类似于AOP。

  • CMS采用的方案就是:写屏障+增量更新来实现的,打破的是第二个条件。
    当黑色指向白色的引用被建立时,通过写屏障来记录引用关系,等扫描结束后,再以引用关系里的黑色对象为根重新扫描一次即可。

伪代码大致如下:

class A{
	private D d;

	public void setD(D d) {
		writeBarrier(d);// 插入一条写屏障
		this.d = d;
	}

	private void writeBarrier(D d){
		// 将A -> D的引用关系记录下来,后续重新扫描
	}
}

问题

为什么初始标记是单线程,重复标记是多线程

个人猜测,初始标记时,只标记gcroot中的对象,比较快,另外如果多个线程,扫描gcroot存在并发问题,故采用单线程

而重复标记相对工作量大些,另外是扫描在并发标记过程中引用关系变化的对象,故可通过一些同步队列的手段进行并发标记

为什么并发清理不会产生错标问题

错标指该对象还存在引用但是被标记为可回收;
在并发标记过程中,标记和用户进程是同时进行的,故会存在错标的问题,但是在重复标记过程中是stw的,故不会存在该问题,但是忽略弱引用及软引用,弱软引用见下一问题

弱引用、软引用在并发清理过程中再次被强引用占用问题

弱引用(weak reference) 是可以被GC强制回收的。当垃圾收集器发现一个弱可达对象(weakly reachable,即指向该对象的引用只剩下弱引用) 时, 就会将其置入相应的ReferenceQueue 中, 变成可终结的对象. 之后可能会遍历这个 reference queue, 并执行相应的清理。典型的示例是清除缓存中不再引用的KEY。

当然, 在这个时候, 我们还可以将该对象赋值给新的强引用, 在最后终结和回收前, GC会再次确认该对象是否可以安全回收。因此, 弱引用对象的回收过程是横跨多个GC周期的。

虚引用OOM问题

与软引用和弱引用不同, 虚引用不会被 GC 自动清除, 因为他们被存放到队列中. 通过虚引用可达的对象会继续留在内存中, 直到调用此引用的 clear 方法, 或者引用自身变为不可达。

也就是说,我们必须手动调用 clear()) 来清除虚引用, 否则可能会造成 OutOfMemoryError 而导致 JVM 挂掉. 使用虚引用的理由是, 对于用编程手段来跟踪某个对象何时变为不可达对象, 这是唯一的常规手段。 和软引用/弱引用不同的是, 我们不能复活虚可达(phantom-reachable)对象。