Skip to content

Commit

Permalink
Add ARC content
Browse files Browse the repository at this point in the history
  • Loading branch information
flycash committed Jun 3, 2021
1 parent a4eda95 commit b09a997
Showing 1 changed file with 83 additions and 15 deletions.
98 changes: 83 additions & 15 deletions gc/algorithm.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,19 @@

## 总览

### 两大步骤
### 收集器与回收器

首先,算法可以分成两个大问题
1. 如何找到存活对象:基本上就是两类,引用计数和标记;
2. 怎么回收空间:基本上也是两类,复制或者清扫
1. 如何找到存活对象:基本上就是两类,引用计数和标记(也叫做跟踪式)
2. 怎么回收空间:基本上也是两类,复制,压缩或者清扫。复制是指直接将存活对象拷贝到另外一块内存区域;压缩是指,将存活对象挪到一起;清扫实际上,大多数时候,就是做一些标记,标记内存可用

这里要强调一下清扫。清扫明面上是指,我将垃圾扫掉,实际上是指将内存返回给内存分配器。那么问题就来了,内存分配器怎么维护这些空闲内存?基本上这里又是两种选择,一种是位图,一种是空闲链表法。相比之下,采用复制的算法,基本上就只需要维护寥寥几个地址。

而且不管哪种法子,都会造成内存分配碎片。例如我这里有一块4M的空闲内存块,分配出去了3.8M。后面的对象申请内存,都大于0.2M,那么我们就这一块内存永远也不会被分配出去。随着不断分配-回收,整个过程产生越来越多这种小的碎片,直到再也找不到任何一块足够大的内存来容纳新对象。

因此清扫的算法,通常伴随一个压缩的过程。所谓压缩,简单的实现是直接执行一次复制,将活着的对象直接复制到一块完整的内存。还有一种做法是挪动对象,填满这些碎片,腾出来的空间组成一块大的内存。

如果笛卡尔积一下,就有了:
1. 引用计数-复制,引用计数-清扫;
2. 标记-复制,标记-清扫;
1. 引用计数-复制,引用计数-压缩,引用计数-清扫;
2. 标记-复制,标记-压缩,标记-清扫;

考虑压缩,就还多一类,比如”标记-清扫-压缩”
不过引用计数我们面试比较少遇到,它在实际中用得也不多,标记类用得比较少

### 并发与并行

Expand All @@ -31,22 +27,94 @@

但是并行不一定意味着并发。例如 Java 里面的 Parallel New,就是GC线程并行收集,这个就不是并发的,因为没有应用线程此时也在运转。

典型的并发 GC:
1. Java 上 HotSpot 实现的 CMS,G1,ZGC
2. Golang GC;

典型的并行 GC:HotSpot 带 Parallel 关键字的,Old Parallel, Parallel New

### 分代

不是所有 GC 都有分代的!!

分代主要是基于分代假说,又分成强分代假说和弱分代假说。

强分代假说:
弱分代假说:
强分代假说:大部分刚分配的对象会在短时间内死掉
弱分代假说:越是生存时间长的对象,越不容易死掉

### 分区 or 分 Region
(类比“**幼儿夭折**”和“**老不死**”)

它有一点点类似分代的概念。但是它本质上就是把内存分成一块一块,然后尽量把回收范围局限在某些块内。例如典型的 G1 回收器,golang 的 span。
![HotSpot分代的效果](https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/img/jsgct_dt_003_alc_vs_srvng.png)

典型的分代 GC:HotSpot 的大部分GC都是
### 增量回收

增量回收核心在于,回收的时候并不是将整个堆,或者整个分代回收掉,而是只回收部分。实施增量回收核心就在于避免在一次GC中消耗太多资源,典型的就是 G1 采用了增量回收来避免停顿时间超长。

典型的增量回收GC:HotSpot G1

## 面试题

### 你了解 XXX 算法吗?
分析:这就是送分题,考察基本概念。这一大类题目,要想回答出来亮点,可以指出哪些垃圾回收器使用了该算法,然后指出这种算法的优缺点(可以进行一些横向比较)。更进一步,如果能够记住并且理解,可以结合内存分配器来一起说。因为特定的算法,限制住了内存分配器的实现。

记住步骤:
1. 基本流程
2. 优缺点

下面我们一个个算法说过去。

#### 标记-复制

(首先回答基本流程)标记-复制算法,在GC开始的时候,会从 GC root 出发标记,沿着对象的引用链,标记存活的对象。在标记完成之后,将存活的对象复制到另外一块内存。Java的Serial New, Parallel New 和 G1 都是采用了标记复制算法。

(讨论优缺点)该算法的优点是,复制会保证我们能够得到一块连续的内存,可以采用高效率的内存分配方案(叫做bump-the-pointer,其实就是指针移动)。缺点则是内存利用率不高,极端情况下,我们只能利用一半内存,另外一半内存要作为复制的目标内存。而且复制也是一个消耗极大的过程。

#### 标记-压缩

(首先回答基本流程)标记-压缩算法,在GC开始的时候,会从 GC root 出发标记,沿着对象的引用链,标记存活的对象。在标记完成之后,将存活的对象全部挪到一侧。这个过程类似于标记-复制。比较有名的采用了这个算法的就是 HotSpot 里面的 Serial Old 和 Parallel Old。

(讨论优缺点)该算法的优点是,复制会保证我们能够得到一块连续的内存,可以采用高效率的内存分配方案(叫做bump-the-pointer,其实就是指针移动)。缺点则是压缩过程非常耗时,涉及到了大量对象移动。比如 CMS 在启用压缩之后,这个过程是 STW 的,导致 GC 停顿时间特别长。

#### 标记-清扫

(首先回答基本流程)标记-压缩算法,在GC开始的时候,会从 GC root 出发标记,沿着对象的引用链,标记存活的对象。在标记完成之后,垃圾回收器会把空闲内存交回内存分配器。最有名的标记清扫垃圾回收器是 CMS 回收器。

(讨论优缺点)该算法的优点是,清扫的过程比复制和压缩要快很多。但是带来的缺点是,内存分配要更加复杂,并且空闲内存不再是一个连续的块。比如说 CMS 就采用了空闲链表来管理空闲内存。

#### 引用计数

引用计数是在对象里面维护一个计数,标记有多少对象使用到了该对象。如果计数为0,就表明该对象可以被回收了。

该算法的优点是实现简单,开销很小并且被摊到了整个应用存活周期。缺点则是,循环引用难以解决。所谓的循环引用,是指多个对象之间互相引用,最终行成一个环,因此它们的计数永远都不会为0。Swift 的垃圾回收就是采用了引用计数(赫赫有名的ARC),还有很多智能指针也是用引用计数来实现的。

(注意,如果记得住,可以接着回答**如何解决循环引用**

### 如何解决引用计数中的循环引用问题?
分析:难题。其实大多数情况下,面试官问出来这个问题,也没指望我们能回答出来,就是抱着万一你知道的心态。当然,如果你面的语言,就是用了引用计数来做GC的话,那么这会比较重要。对于 Java,golang 开发来说,稍微知道一点就可以。当然,我也不太熟悉,所以这里就是大概描述一下。依据我的个人经验,只要不是面啥JVM开发,凑合知道这么一点内容,就能应付过去了。

答案:一般是有三种策略:
1. 采用特殊引用。例如使用 weak reference,弱引用。这一类的做法是用户需要自己显式管理自己的引用,在出现循环引用的地方,将一部分引用修改为 weak reference,从而所谓的 strong reference 就不再组成环;
2. 采用后备的追踪式收集器。一般来说,是把可能出现环的对象单独处理,用追踪式的收集器标记一遍,这些就是存活对象。(Python就是这种策略,不了解算法细节)
3. 采用试探删除策略。该方法类似于图里面去除环的算法,尝试把某些引用删掉。如果删掉之后别的对象的计数变为0,那么说明这些对象只有环内的引用,因此是可回收对象。

#### 如何引导
- 在聊到了引用计数的时候。这个是一个比较安全的亮点,就是认真研究过循环引用处理方案的面试官不多

#### 类似问题
- 追踪式垃圾回收器如何解决循环引用问题?这个问题其实是吓人的,因为追踪式的一般都是使用三色法来追踪对象,天然就解决了,可以参考后面的三色法面试题

### 什么是三色法

### 什么是安全点?

### 为什么要分代?
分析:其实分代不是最开始就有的,而是大家观察到了分代假说的两个现象之后,才有了分代的设计。那么分代究竟是为什么引入呢?很简单,就是既然新对象很容易就死掉,老对象很难死掉,那我们就分开着两个,然后新对象朝生夕死,这样就可以每次都回收大量的空间。而老对象待着的地方,我就可以少回收,反正也回收不到东西,只在确实没空间了我再回收。所以分代,核心就是为了提高 GC 效率。

回答这种问题,我们可以从如果不分代会有什么问题来引出,提高 GC 效率这么一个说法。但是还有一个难点,就是为啥有的 GC 实现是不分代的。
回答这种问题,我们可以从如果不分代会有什么问题来引出,提高 GC 效率这么一个说法。但是还有一个难点,就是为啥有的 GC 实现是不分代的。这个问题的答案可以作为我们回答的亮点。

答:(首先回答分代假说,这是从理论上直接回答了这个问题)分代是基于强分代假说和弱分代假说,即新对象很容易死,老对象不容易死。(下面点出核心,就是为了效率)因此如果我们采用分代,依据存活时间来将对象放到不同的内存区域,那么在回收的时候,就可以只回收年轻代,或者只回收老年代。这样一来,回收效率高,(效率高的两个方面)一方面是停顿时间短(也可以说是资源消耗低),一方面是能够回收更多的内存。

(下面我们指出并不是所有的 GC 实现都是分代的,作为一个亮点)但是并不是所有的 GC 都是分代,例如Java 的 ZGC,golang GC 都不是分代的。绝大部分情况下,分代都要比不分代效率高,但是分代带来的了额外的问题:
1. 实现难度高,分代 GC 的实现难度,要比非分代高一个量级;
2. 较难配置和优化,所有的分代 GC 都面临一个问题,就是各个分代的大小该如何确定;

0 comments on commit b09a997

Please sign in to comment.