02. GC 基础知识


1. 什么是垃圾

C 语言申请内存:malloc;释放内存:free

C++ 申请内存:new;释放内存:delete

Java 申请内存: new;释放内存:自动回收

自动内存回收,编程上简单,系统不容易出错;手动释放内存,容易出两种类型的问题:

  • 忘记回收
  • 多次回收(delete 可能封装在各种条件语句中,可能多次 delete)

什么是垃圾?

没有任何引用指向的一个对象或者多个对象。

2. 如何定位垃圾?

2.1 引用计数法(Reference Count)

给每个对象分配一个计数器,当有一个引用指向这个对象时,计数器加 1;当指向该对象的一个引用失效时,计数器减 1。当该对象的计数器为 0 时,该对象就会被回收。

缺点:不能解决循环引用的垃圾回收问题。

2.2 根可达算法(Root Searching)

以根对象(GC Roots)作为起点,向下搜索根对象所连接的目标对象,称这些对象是从根可达的(这些对象就是内存中存活的对象)。剩下的那些不可达的对象就是垃圾,要被回收。

根对象:线程栈变量、静态变量、常量池、JNI指针(调用C、C++本地方法创建的变量)

如下图,o1、o2、o3、o4 是根可达的,o5、o6、o7 不是根可达的,o5、o6、o7 将会被回收。

02. GC 基础知识

3. 垃圾回收算法

3.1 标记-清除算法(Mark-Sweep)

两个阶段:

  • 标记
  • 清除

先对需要回收的对象进行标记,然后在下一阶段把这些对象清除。

存在的问题:位置不连续,容易产生碎片。

3.2 拷贝算法(Copying)

将内存一分为二(假设分为 s0 和 s1),每次只使用其中的一块。当 s1 的内存用完之后,就把 s1 中存活的对象复制到 s2,然后对 s1 进行回收;当 s2 的内存用完之后,再把 s2 中存活的对象复制到 s1,对 s2 进行回收… …

不会产生碎片,并且简单高效,但是浪费了内存空间。

3.3 标记压缩(Mark-Compact)

在标记清除的过程中做了一次压缩整理(把存活的对象向前复制,挪到一起)。如下图:

02. GC 基础知识

一方面,这样不会产生内存碎片;另一方面,把空闲空间整理在一起,能够为大对象分配足够的内存。当然,效率要比前两种低一些。

4. Java 堆内存模型(Java Heap Memory Model)

Java 将堆内存分为三部分:新生代(new 或者 young)+ 老年代(old)+ 永久代(permanent),其中新生代又进一步划分为 Eden、S0(Survivor0)、S1(Survivor1) 三个区。如下图:

02. GC 基础知识

新生代和老年代的内存比例为 1 : 2 或者 1 : 3, Eden、S0、S1 三个区的比例为 8 : 1 : 1。结构如下图所示:

02. GC 基础知识

其中,survivor 区使用 Copying 算法进行垃圾回收,old 区使用 Mark-Compact 进行垃圾回收。

新生代垃圾回收过程:

  • 第一次 YGC(Young GC)回收之后,大多数的对象会被回收,活着的对象进入 s0;
  • 再次 YGC 之后,eden 区和 s0 里活着的对象拷贝到 s1;
  • 再次 YGC 之后,eden 区和 s1 里活着的对象拷贝到 s0;
  • 上面两步循环着来;
  • 每拷贝一次,年龄增长一岁。年龄足够时,对象进入老年代(年龄取决于不同的 GC,PS/PO 15 岁,CMS 6 岁,G1 15 岁);
  • s 区装不下了,直接放到 old 区。

老年代:

  • 装的是存活时间较长的对象;
  • 老年代满了,触发一次 FGC(FGC,即 Full GC,整个堆内存的一次回收)。

5. 常见的垃圾收集器(十种)

垃圾回收器的发展路线,是随着内存越来越大的过程而演进,从分代算法演化到不分代算法。

  • Serial 算法 几十兆
  • Parallel 算法 几个 G
  • CMS 算法 几十个 G
  • G1 上百个 G
  • ZGC - Shenandoah 4T

下图列出了常见的十种垃圾收集器。其中新生代垃圾收集器有 Serial、ParNew 和 Parallel Scavenge 收集器,老年代垃圾收集器有 Serial Old、Parallel Old 和 CMS 收集器,从 G1 开始往后的收集器是目前最新的收集器,已经开始不分代。

02. GC 基础知识

收集器常用的三种组合如下图:

02. GC 基础知识

JDK 1.8 默认使用的是 Parallel Scavenge + Parallel Old,简称 PS + PO,又称 ParallelGC。

5.1 新生代垃圾收集器

(1)Serial 收集器(串行回收)

Serial 收集器作用于新生代,是一个单线程收集器,基于 Copying 算法实现。在进行垃圾回收时仅使用单条线程回收并且在回收过程中会暂停所有的用户线程(Stop-the-World,即 STW)。过程如下图:

02. GC 基础知识

简单描述:

程序正常运行——》一定程度后,eden 区满,触发 YGC——》这时候,停下所有的用户线程(stop-the-world)——》进行单线程垃圾回收——》回收完成之后,用户线程继续执行

(2)Parallel Scavenge 收集器(并行回收)

Parallel Scavenge 收集器同样作用于新生代。相比于 Serial 收集器,它在垃圾回收时也会暂停所有的用户线程,但在垃圾回收时使用多个线程同时进行垃圾回收。

(3)ParNew 收集器

ParNew 收集器和 Parallel Scavenge 收集器一样,也是一个多线程收集器,也存在 STW。区别在于,ParNew 可以配合 CMS 使用。

5.2 老年代垃圾收集器

(1)Serial Old 收集器

Serial Old 收集器作用于老年代,采用单线程和 Mark-Compact 算法实现垃圾回收。在垃圾回收时同样会暂停所有的用户线程,造成应用的卡顿。

一般来说,老年代的容量都比新生代的大,所以当老年代进行垃圾回收时,STW 所用的时间会比新生代所用的时间长得多。

(2)Parallel Old 收集器

Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本,采用多线程和 Mark-Compact 算法来实现垃圾回收。这个收集器主要是为了配合 Parallel Scavenge 收集器的使用,即当新生代选择 Parallel Scavenge 收集器时,老年代就选择 Parallel Old 收集器(这就是 JDK 1.8 默认使用的垃圾回收器组合,即 PS + PO)。

(3)CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一款承上启下的老年代垃圾收集器,实现了垃圾回收和用户线程的并发进行,大大降低了 STW 的时间。

CMS 只在初始化标记和重新标记阶段需要 STW,其它阶段中垃圾回收线程和用户线程可以并发进行,不必暂停用户线程,也就不会造成应用的停顿。

CMS 垃圾回收主要分四个阶段,图示如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jFgU9DCv-1597407728069)(https://note.youdao.com/yws/public/resource/f224f2f39f38801a0ea1eef352e08912/xmlnote/BB972A89800642B3B847F3C2684BD3C1/7929)]

  • 初始标记: 这个阶段只标记 GC Roots 直接关联的对象,速度很快,所以 STW 的时间很短。
  • 并发标记: 这个阶段的任务是,从初始标记收集到的对象引用开始,遍历所有其它的对象引用,并标记所有需要回收的对象。这个过程中,垃圾收集线程和用户线程并发执行。
  • 重新标记: 由于上一阶段垃圾收集线程和用户线程是并发执行的,可能会有对象被重新引用或者有新对象可以被回收。所以,这一阶段就先 Stop-the-World,然后对这些对象重新进行标记。
  • 并发清理: 这一阶段把之前标记的垃圾进行回收,这个过程是与用户线程并发进行的。