Skip to content

Commit

Permalink
Add more questions to CMS
Browse files Browse the repository at this point in the history
  • Loading branch information
flycash committed Jun 18, 2021
1 parent f78510a commit e873951
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 1 deletion.
Binary file added gc/img/cms_gc.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added gc/img/gc_roots.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
100 changes: 99 additions & 1 deletion gc/java_cms.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,103 @@ CMS 的面试,大多数时候来说,就是面一下算法的大概流程,
#### 类似问题
- CMS的执行步骤?
- CMS使用了什么算法?
- CMS 有几次停顿?是哪几次?

(下面我们分不同的阶段来看看面试官会怎么问)
(下面我们分不同的阶段来看看面试官会怎么问)

### CMS 的 GC root 有什么?
分析:之前在[总览](./algorithm.md)那里我们讨论过 GC root 大概有些什么。这里则是进一步细化了,在CMS这个回收期下,GC root 有一些什么。

其实我们大概都能够猜到主要的几个,比如说线程本身,线程栈上引用,本地方法栈上引用。然后还有一些稍微细想大概也能猜到的,比如说加载到的类,它持有的静态对象。

按照 Eclipse 的文档,它划分得非常详细:

![gc root 详细](./img/gc_roots.png)

我们先分个类:
1. Java 线程相关的:Java线程本身,Java的线程栈
2. 本地方法相关的:本地方法栈上变量,本地方法全局变量,本地方法局部变量
3. 类相关:系统类,类的静态变量
4. finalize 相关:finalize 是 JVM 管理的,所以也是 gc root
5. 同步相关:处于同步阻塞的对象。因为 JVM 的实现基本上都是把阻塞队列丢到一个本地(NATIVE)队列里面,所以它也不得不成为一个 gc root;
6. 其它

其实这几个子类,稍微想想都能理解,为什么它们会成为 gc root。

回答这个问题,入门是要答出1,2;进阶是要答出3,4,5。

所以亮点落在后面三个。

还有一个更加进阶的高级回答,是回答 CMS 如何处理年轻代指向老年代的引用。之前我们说过,跨代引用也会成为 gc root。比如说在 CMS 和 Parallel New 的组合之下,如果触发年轻代 GC,那么老年代指向年轻代的引用,是被记录在记忆集(卡表是它的实现)里面的,在 GC 启动的时候要扫描记忆集,找出这种跨代引用。

但是 CMS 是没有记录年轻代指向老年代的引用的。这意味着,在 CMS 启动的时候,它必须扫描整个年轻代才能找到所有年轻代指向老年代的引用。

这听起来就很可怕。但是考虑到我们有年轻代 GC,所以实际上 CMS 会等一段时间,如果这段时间出发了年轻代 GC,那么 CMS 扫描整个年轻代就变成了只需要扫描 Survivor 了。

所以我们的回答分成三段,依次递进。

答案:gc root 有很多。(先回答最常见的,线程和本地方法)最常见的就是线程本身和线程栈上对象(这是指引用),与之类似的是本地方法相关的,包括本地方法栈上对象,本地方法全局变量;(回答后面三种比较人知道的)而后还有系统类和类的静态变量,finalize 相关的,以及处于同步状态的对象。(一般是不需要解释为什么这三种也是 gc root,面试官如果没有提前了解的话,他也一时想不到为啥)

(高高级,讨论跨代)总而言之, gc root 可以理解为,所有的指向回收区域的外部引用。

比如说年轻代回收,需要扫描卡表找到老年代指向年轻代的引用;G1垃圾回收需要找到指向 Region 的外部引用。

(重点描述 CMS,当然如果你们之前的话题是G1,那就是重点描述G1,依次类推)而 CMS 则比较不一样,它因为没有记录年轻代指向老年代的引用,所以 CMS 需要扫描整个年轻代才能找到跨代引用。因此,CMS 会在准备启动 GC 之前等待一小段时间,看看这段时间内是否发生了年轻代 GC。如果发生了,那么 CMS 就只需要扫描 Survivor 区了。(这里也是埋伏笔,因为后面 GC 还可能问到,为什么我们要设置一些参数,以迫使 CMS 期间触发一些年轻代 GC)

(再进阶)该参数是`XX:CMSWaitDuration`,一般设置为覆盖一个年轻代GC周期。但是设置过长可能导致老年代空间完全耗尽。(这里我们只说完全耗尽,然后看面试官会不会问 promotion failed 之类的问题,他可能进一步会询问这会出现什么问题,但是这里,我们就暂时停下来,参考后面的**CMS 来不及回收老年代会发生什么事情**

#### 类似问题
- CMS 如何处理年轻代?年轻代是用 Parallel New 来回收的,CMS 会利用年轻代 GC的结果来减少初始标记和再标记两个停顿过程

### CMS 来不及回收老年代会发生什么事情

分析:这其实考察的是 CMS的一个异常情况。回忆一下,什么时候会触发 CMS?除了第一次以外,其他时候都是JVM觉得应该GC了,就会触发GC。它会控制住GC频率,确保 CMS GC 发生期间,不会出现老年代空间不足的问题。

但是很显然,总有意外,于是就会出现 concurrent mode failure. 比如耳熟能详的 promotion failure。

那么在这种情况下,会出现什么问题呢?

当然就是整个 CMS 会失败,退化为串行GC。它使用的是古老的 Serial Old 来GC,整个过程都是 STW 的。

答案:会出现 concurrent mode failure(并发模式失败),于是退化为 Serial Old 来GC,GC 时间会猛增。

(接下来我们讨论一下为什么会出现这种情况,作为进一步解释,亮点一。这也是一个单独可能出现的面试题)一般出现是两种情况,年轻代 GC 提升对象失败,或者超大对象直接分配在老年代失败。更深层次的原因可能是年轻代 GC 太频繁,导致对象被迅速提升到老年代,也可能是老年代碎片太严重,导致找不到足够大的连续内存来容纳对象。

(接下来讨论解决方案,亮点二。这也本身就是一个面试题)我们需要尽量避免这种出现情况:
1. 大多数时候,最简单的方案是增加堆的大小;
2. 其次我们可以调整老年代和年轻代的比例,但是调大老年代的比例意味着年轻代GC更加频繁,治标不治本;
3. 让 CMS GC的时候整理内存,即触发压缩过程。即设置`-XX:UseCMSCompactAtFullCollection``-XX:CMSFullGCBeforeCompaction=5`参数。这个过程会导致更长的STW时间(压缩的过程是STW的);同时`CMSFullGCBeforeCompaction`这个参数,过小会导致频繁的内存压缩,性能很差;
4. 设置`-XX:CMSInitiatingOccupancyFraction=70``-XX:+UseCMSInitiatingOccupancyOnly`参数,即让 JVM 永远在使用量达到我们设置的阈值的时候就开始CMS(这里是70%)

#### 类似问题
- 如何解决 CMS 内存碎片?还能咋的,只能是开启压缩了。主要是解释`CMSFullGCBeforeCompaction`参数过大过小会有什么问题;
- `CMSInitiatingOccupancyFraction`参数有什么作用?告诉JVM**第一次**CMS在使用了多少内存后开始GC,如果配合`UseCMSInitiatingOccupancyOnly`则是让JVM永远使用我们设置的比率,而不必自适应计算
- 什么是 concurrent mode failure?有什么后果
- 什么是 promotion failure?有什么后果
- 过早 promotion 会有什么后果?容易导致 Full GC,容易导致 promotion failure

### CMS 为什么会有内存碎片

分析:综合考察内存正向分配和GC回收。这道题还是比较有水平的。首先要先揭示CMS的内存管理,核心就是两点:标记清扫和空闲链表。

我个人认为是标记清扫这种特性决定了空闲链表这种管理空间内存的方式。

答案:原因在于 CMS 使用了标记-清扫的内存回收算法,和空闲链表的内存管理方式。每一次 CMS GC 之后,幸存的对象会把连续内存划分成一段段,

![空闲内存被分成一段一段](./img/cms_gc.png)

多次 GC 之后,就会导致所有的空闲内存都很小,以至于明明还有很多空闲内存,但是却找不到任何一块足够大的内存来存放新对象。这就是内存碎片。


(其实这里还有一种可能,是面试官问的其实是并发过程中产生的浮动碎片,其实就是该被回收的没被回收,但是大多数时候,是指这个清扫导致的内存碎片化)

(接下来我们讨论解决方案,在前面提到过,就是`UseCMSCompactAtFullCollection``-XX:CMSFullGCBeforeCompaction` 参数)我们可以通过设置`UseCMSCompactAtFullCollection``-XX:CMSFullGCBeforeCompaction`参数来控制 CMS 执行压缩,并且控制在多少次 GC 触发一次压缩。

#### 如何引导
- 讨论到正向的内存管理
- 讨论到操作系统内存管理
- 讨论到指针碰撞

## Reference
[Oracle documents - CMS](https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html)
[Java gc roots](https://help.eclipse.org/2021-06/index.jsp?topic=%2Forg.eclipse.mat.ui.help%2Fconcepts%2Fgcroots.html&cp=37_2_3)

0 comments on commit e873951

Please sign in to comment.