一、你认为垃圾回收器工作过程有哪些关键点?
STW
收集器在根节点枚举这步都是必须要暂停用户线程的( STW ),如果不这样的话在根节点枚举的过程中由于引用关系在不断变化,分析的结果就不准确。
安全点(safe point)
收集器在工作的时候某些时间是需要暂停正在执行的用户线程的( STW ),这个暂停也并不是说用户线程在执行指令流的任意位置都能停顿下来开始垃圾收集, 而是需要等用户线程执行到最近的安全点后才能够暂停。
安全点如何选取呢?,安全点的选取基本是以:”是否具有让程序长时间执行的特征“为标准选定的,而最明显的特征就是指令序列的复用,主要有以下几点:
- 1、方法调用
- 2、循环跳转
- 3、异常跳转等等
对于安全点另一个问题是:垃圾收集器工作时如何让用户线程都跑到最近的安全点停顿下来?有两种方案。
- 1、抢先式中断:不需要用户代码主动配合,垃圾收集发生时,系统把用户线程全部中断,如果发现用户线程中断的地方不在安全点上,就恢复这个线程执行让它执行一会再重新中断。不过现在的虚拟机几乎没有采用这种方式。
- 2、主动式中断:思想是当垃圾收集器需要中断线程的时候,不直接对线程操作,仅仅设置一个标志位,各个线程执行过程中会不停的去主动轮询这个标志,一旦发现中断标志为真时就自己再最近的安全点上主动挂起。
安全区域
安全点的设计似乎完美的解决了如何停顿用户线程,它能保证用户线程在执行时,不太长时间内就会遇到可进入垃圾回收的安全点,但是如果用户线程本身就没在执行呢?比如用户线程处于 sleep 或者 blocked 状态,这个时候它就无法响应虚拟机的中断请求,没办法主动走到安全的地方中断挂起自己,对于这种情况就必须引入安全区域( Safe Regin )来解决。
安全区域是指能够确保在某一段代码片段之中, 引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸了的安全点。
当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域,这段时间里 JVM 要发起 GC 就不必去管这些线程了。 当线程要离开安全区域时,它要检查 JVM 是否已经完成了根节点枚举(或者其他 GC 中需要暂停用户线程的阶段)
- 1、如果完成了,那线程就当作没事发生过,继续执行。
- 2、如果没完成,它就必须一直等待, 直到收到可以离开安全区域的信号为止
二、你了解过哪些常见的垃圾回收器
SerialGC
工作过程如下,主要特点是GC为单线程执行,新生代和老年代分别STW
ParallerGC
工作过程如下,主要特点是GC为多线程并行执行,新生代和老年代分别STW,,但是多线程执行明显减少了STW时间
CMS
略,由后面一个面试题讲解
G1
略,由后面一个面试题讲解
三、CMS垃圾回收器的回收过程、流程、缺点
CMS一般用于老年代GC
CMS的整体执行过程分成5个步骤,其中标记阶段包含了三步,具体细节如下:
- 1、初始标记:标记 GC Roots 直接关联的对象,会导致 STW ,但是这个没多少对象,时间短 。
- 2、并发标记:从 GC Roots 开始关联的所有对象开始遍历整个可达路径的对象,这步耗时比较长,所以它允许用户线程和GC线程并发执行,并不会导致STW ,但面临的问题是可能会漏标,多标,等问题。
- 3、重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段会导致 STW ,但是停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
- 4、并发清除:将被标记的对象清除掉,因为是标记-清除算法,不需要移动存活对象,所以这一步与用户线程并发运行。
- 5、重置线程:重置GC线程状态,等待下次CMS的触发,与用户线程同时运行。
当然,在CMS中也会出现一些问题,主要有以下几点:
1、CPU敏感:对处理器资源敏感,毕竟采用了并发的收集、当处理核心数不足 4 个时,CMS 对用户的影响较大,因为CMS默认启动的回收线程数量是:(CPU核数+3)/ 4。
2、浮动垃圾:由于 CMS 并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留到下一次 GC 时再清理掉,这一部分垃圾就称为“浮动垃圾”(比如用户线程运行产生了新的 GC Roots )。
由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收,在 1.6 的版本中老年代空间使用率阈值(92%) ;如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure ,这时虚拟机将临时启用 Serial Old 来替代 CMS,冻结用户线程的执行了回收老年代,这样会导致很长的停顿时间。
3、空间碎片:这是由于CMS采用的是标记-清除算法导致的,当碎片较多时,给大对象的分配带来很大的麻烦,为了解决这个问题,CMS 提供一个参数: -XX:+UseCMSCompactAtFullCollection ( HotSpot(TM) 64-Bit Server VM is deprecated ),一般是开启的,如果分配不了大对象,就进行内存碎片的整理过程;这个地方一般会使用 Serial Old ,因为 Serial Old是一个单线程,回收时会暂停用户线程,然后进行空间整理。所以如果分配的对象较大,且较多时,CMS 发生这样的情况会很卡。
四、并发标记的过程是怎么样的?
到目前为止,所有收集器在根节点枚举遍历其直接关联的对象时是要 STW的,并发收集器在继续往下进行可达性标记时是允许用户线程并发执行的,这样有效的减少了整体 STW 时间, 那这个并发标记到底是如何工作的呢?这就是我们要说的三色标记。
三色标记算法概述
首先约定好jvm在GC时会对对象进行颜色标记,按照对象是否被访问过这个条件将对象标记成以下三种颜色:
- 白色:表示该对象尚未被收集器访问过,在可达性分析结束后,仍为白色的对象表示不可达,即为垃圾。要被回收
- 灰色:表示该对象已被收集器访问过,但是这个对象至少存在一个引用还未被扫描
- 黑色:表示该对象已被收集器访问过,并且它的所有引用都已被扫描,黑色对象是安全存活的。
另外:对于黑色对象1 、如果有其他对象的引用指向了黑色对象,无需重新扫描一遍2 、黑色对象不可能绕过灰色对象直接指向白色对象。
下面我们根据可达性分析算法来看一下三色标记的过程:
三色标记过程
初始状态
首先所有对象都是白色的,进行 GC Roots 枚举, STW ,枚举后只有 GC Roots 是黑色的
初始标记
初始标记仅仅只是标记一下 GC Roots 能直接关联的对象,速度很快,也会STW 。
并发标记
这个阶段是并发执行, GC 线程扫描整个引用链,分两种情况:
- 1、没有子节点,将本节点标记为黑色。
- 2、有子节点,将当前节点标记为黑色,子节点标记为灰色。
就这样继续沿着对象图遍历下去:
重新标记
这一阶段是修正在并发标记阶段因用户线程并发执行而产生的一系列问题,继续标记,直至灰色对象没有其它子节点引用时结束,这一阶段需要STW 。
扫描完成后,黑色对象就是存活的对象,白色对象就是已消亡可回收的对象。
三色标记的问题
并发标记过程中,由于GC线程是和用户线程并发执行,这期间可能会发送变化,不做控制的话就会影响程序正确性,主要会产生下面两种问题
多标
如下图,假设已经遍历到 E(变为灰色了),此时应用程序将 D > E 的引用断开。
D > E 的引用断开之后, E、F、G 三个对象不可达,应该要被回收的。然而因为 E 已经变为灰色了,其仍会被当作存活对象继续遍历下去。最终的结果是:这部分对象仍会被标记为存活,即本轮 GC 不会回收这部分内存。
这部分本应该回收但是没有回收到的内存,被称之为 浮动垃圾 。浮动垃圾并不会影响应用程序的正确性,只是需要等到下一轮垃圾回收中才被清除。
另外,针对并发标记开始后的新创建的对象,通常的做法是直接全部当成黑色,本轮不会进行清除。这部分对象期间可能会变为垃圾,这也算是浮动垃圾的一部分。
漏标
假设 GC 线程已经遍历到 E(变为灰色了),此时应用线程断开 E > G 的引用,同时添加 D > G 的引用。
这时切回到 GC 线程,因为 E 已经没有对 G 的引用了,所以不会将 G 置为灰色;尽管因为 D 重新引用了 G,但因为 D 已经是黑色了,不会再重新做遍历处理。最终导致的结果是:G 会一直是白色,最后被当作垃圾进行清除。这直接影响到了应用程序的正确性,是不可接受的。
对于这种情况,我们需要将 G 这类对象记录下来,作为灰色对象在重新标记阶段继续向下遍历,当然这个阶段需要 STW 。
很显然,记录操作在并发标记阶段至关重要,那么这个事谁来做?怎么做呢?这就要提到JVM中很重要的一个概念------读写屏障
解决漏标问题
读写屏障
针对于漏标问题,JVM 团队采用了读屏障与写屏障的方案。其目的很简单,就是在读写前后将 G 这类对象给记录下来。
读屏障
oop oop_field_load(oop* field) {
// 读屏障-读取前操作
pre_load_barrier(field);
return *field;
}
当读取成员变量之前,先记录下来,这种做法是保守的,但也是安全的。因为【一个或者多个黑色对象重新引用了白色对象】,重新引用的前提是:得获取到该白色对象,此时已经读屏障就发挥作用了。
写屏障
所谓的写屏障,其实就是指给某个对象的成员变量赋值操作前后,加入一些处理(类似 Spring AOP 的概念)。
void oop_field_store(oop* field, oop new_value) {
// 写屏障-写前操作
pre_write_barrier(field);
*field = new_value;
// 写屏障-写后操作
post_write_barrier(field, value);
}
增量更新与原始快照
解决漏标问题,不同收集器采用的方案也不一样
增量更新
-
并发标记阶段:应用程序运行时,写屏障捕捉新增引用关系(例如,
A -> B
),并将新增引用记录到缓冲区。 -
即时标记:对每个新增引用,递归标记新增引用对象及其下游对象,确保它们被标记为“存活”。
-
重新标记阶段:暂停应用线程,检查并处理所有剩余的引用变更,确保标记完整无误。
原始快照
- 1、当某个对象断开其属性的引用时,利用写屏障,将断开之前的引用记录下来
- 2、尝试保留开始时的对象引用图,即原始快照,当某个时刻的 GC Roots 确定后,当时的对象引用图就已经确定了。
- 3、后续并发标记是按照开始时的快照走,比如 E > G ,即使期间发生变化,通过写屏障记录后,保证标记还是按照原本的视图来
- 4、重新标记阶段再对变化的引用进行检查处理
五、G1了解不,说说G1,相对于CMS收集器的优点
G1特点概览
G1最大的特点:支持用户给定一个期望的STW时间,每次GC时尽可能的贴近于STW时间内做GC
G1将内存布局分为多个Region,所以每次GC都会选择某个Region,哪个Region回收的价值(更贴近于STW时间的价值更大)更大,就回收哪个
我们再来详细看一下G1
G1设计思想
1、思想转变:要实现这个目标,首先要有一个思想上的转变,G1收集器出现之前的其他所有收集器,他们的收集范围要么是新生代( Minor GC ),要么是老年代( Major GC ),要么是整堆( Full GC ),而G1跳出了这个樊笼,它可以面向堆内存任何部分来组成回收集( Collection Set , CSet )进行回收,衡量标准不再是它属于哪个分代,而是哪个回收集中存放的垃圾最多,回收收益最大,就回收哪个。
2、新的内存布局:当然G1能达到这个目标的关键在于G1开创了基于Region 的堆内存布局,当然也依然遵循了分代收集理论,但是堆内存布局与其他收集器有明显差异,G1不在坚持固定大小以及固定数量的分代区域划分,而是把连续的java堆内存划分成多个大小相等的独立区域( Region ),每个Region 可以根据需要扮演新生代的 Eden , Survivor ,或者老年代。收集器能够对扮演不同角色的 Region 采用不同的策略去处理,这样无论是对于新创建的对象还是对于熬过很多次垃圾收集的旧对象都有很好的收集效果。
Region 中还有一类特殊的 Humongous 区域,专门用来存储大对象。 G1认为只要大小超过了一个 Region 容量一半的对象即可判定为大对象,对于那些超过了整个 Region 容量的超级大对象,将会被存放在 N 个连续的Humongous Region 之中。
3、回收策略:G1之所以能建立可预测的“停顿时间模型”的原因在于它将Region 作为单次回收的最小单元,即每次回收的空间都是 Region 的整数倍,同时G1会去追踪各个 Region 里面垃圾的“价值”(回收所获得的空间大小以及回收所需要的时间的经验值),然后在后台维护一个优先级列表,每次根据用户设定停顿时间( -XX:MaxGCPauseMillis=time ,默认200毫秒),优先回收价
值收益最大的那些 Region 。
G1的三大问题如何解决
1、跨 Region 引用如何解决:前面我们知道通过记忆集( RSet )解决跨代引用,但是在G1中,每个 Region 都需要维护自己的记忆集,记录别的 Region指向自己,但是G1中的 Region 数量要比传统收集器的分代数量明显多的多,所以G1中使用记忆集要比其他收集器有着更高的内存占用负担,根据经验,G1至少要耗费大约相当于java堆容量的10%~20%。
2、并发标记问题:如何保证并发标记阶段GC收集线程与用户线程互不干扰,当然G1是通过原始快照( SATB )解决的(CMS是通过增量更新实现的)。
3、如何建立可靠的可预测模型:用户通过 -XX:MaxGCPauseMillis=time,参数指定的停顿时间只是一个期望值但是G1怎么做才能满足用户的期望呢?
G1收集器在收集过程中会记录每个 Region 的回收耗时,每个 Region 记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析出平均值,标准偏差,置信度等统计信息。根据这些信息决定 Region 的回收价值。
运行过程
- 1、初始标记:标记出 GC Roots 直接关联的对象,并且修改TAMS指针的值,这个阶段速度较快,STW,单线程执行,
- 2、并发标记:从 GC Root 开始对堆中的对象进行可达新分析,找出存活对象,这个阶段耗时较长,但可以和用户线程并发执行。
- 3、重新标记:修正在并发标记阶段因用户程序执行而产生变动的标记记录,即处理 SATB 记录。STW,并发执行。
- 4、筛选回收:筛选回收阶段会对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划,筛出后移动合并存活对象到空Region,清除旧的,完工。因为这个阶段需要移动对象内存地址,所以必须STW。
优缺点
- 1、并发性:继承了CMS的优点,可以与用户线程并发执行。当然只是在并发标记阶段。其他还是需要STW
- 2、分代GC:G1依然是一个分代回收器,但是和之前的各类回收器不同,它同时兼顾年轻代和老年代。而其他回收器,或者工作在年轻代,或者工作在老年代;
- 3、空间整理:G1在回收过程中,会进行适当的对象移动,不像CMS只是简单地标记清理对象。在若干次GC后,CMS必须进行一次碎片整理。而G1不同,它每次回收都会有效地复制对象,减少空间碎片,进而提升内部循环速度。
- 4、可预测性:为了缩短停顿时间,G1建立可预存停顿的模型,这样在用户设置的停顿时间范围内,G1会选择适当的区域进行收集,确保停顿时间不超过用户指定时间。