【ZGC】为什么初始标记需要STW(stop the world) ?
提出问题:
学习过 JVM 的同学应该都知道,目前并发垃圾回收器(CMS、G1、ZGC)针对并发标记的典型处理都是初始标记、并发标记和再标记。其中初始标记和再标记需要STW(stop the world) 。大家对于初始标记需要STW都习以为常,但是有没有仔细思考一下,为什么初始标记需要STW ?能不能把初始标记和并发标记放在一起并发执行,去掉STW ?
显然答案是不能,否则垃圾回收器的设计者们早就这样做了,JVM中所有的并发垃圾回收器在并发标记时都包含这两个阶段。那么问题来了,为什么不能?在这里进行STW的目的是什么?解决的是什么问题?
作出假设:
我们先做一个假设,假设将初始标记和并发标记进行合并,并且不需要STW。逻辑上似乎可行,我们从根集合出发直接使用并发标记开始标记,直到递归完所有的活跃对象。因为是并发处理,所以标记线程和应用程序线程并发地访问对象。存在的场景有以下几种:
- 1、只有应用程序线程访问对象。
- 2、只有标记线程访问对象。
- 3、标记线程和应用程序线程都访问了对象,并且应用程序线程在标记线程之后访问对象。
- 4、标记线程和应用程序线程都访问了对象,并且应用程序线程在标记线程之前访问对象。
对于情况1:如果应用程序线程创建新的对象,按照并发处理算法的介绍,新的对象被标记为活跃的;如果程序线程访问对象,而标记线程不会访问对象,说明应用程序线程把对象设置为垃圾(标记对象不会访问到对象,说明对象不可达),无须标记。但是按照目前并发标记算法的处理,会对对象进行标记,这种情况造成了错标,产生了浮动垃圾,不过浮动垃圾会在下一次垃圾回收中完成回收。
对于情况2:标记线程是从根集合出发,所有访问到的对象都可以被标记为活跃的。
对于情况3:两类线程都访问了对象,标记线程先访问对象,直接把对象标记为活跃的;应用程序线程后访问对象,即使应用程序线程把访问的对象设置为垃圾(如NULL),也不会存在问题,因为对象已经标记,这种情况造成了错标,产生了浮动垃圾,浮动垃圾会在下一次垃圾回收中完成回收。
对于情况4:应用程序线程先访问对象,标记线程后访问对象。假设标记线程访问对象开始标记时,应用程序线程更改了引用关系,标记线程访问到的则是新的对象。而引用变更前老的对象按照并发标记算法(ZGC使用SATB算法)中防止漏标的介绍,需要把访问的对象进行标记,但是应用程序线程发生在标记线程之前,也就是说应用程序线程根本不知道需要标记老对象,这就导致了漏标的情况。
解决第四种情况中漏标的办法就是使用目前所引入的初始标记。那么为什么引入初始标记的STW就可以解决这一问题?在初始标记中,标记线程会等待应用程序线程暂停,那么就不会发生第四种情况。为了使应用程序暂停的时间足够短,在初始标记中仅仅把根集合直接引用的对象找出来作为并发标记的输入。
得出结论:
并发垃圾回收器一般都需要分为初始标记、并发标记阶段,初始标记需要 STW,否则会产生错标和漏标的问题。
错标不会影响程序的正确性,只是造成所谓的浮动垃圾,在下一次垃圾回收时回收;但漏标会导致可达对象被当作垃圾回收,从而影响程序的正确性。