1. 概述

1.1 什么是垃圾

所谓垃圾就是内存中已经没有用的对象。 既然是”垃圾回收",那就必须知道哪些对象是垃圾。Java 虚拟机中使用一种叫作可达性分析的算法来决定对象是否可以被回收。

1.2 可达性分析

​JVM​​​ 把内存中所有的对象之间的引用关系看作一张图,通过一组名为 ​​GC Root​​ 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,最后通过判断对象的引用链是否可达来决定对象是否可以被回收。如下图所示:

GC 回收机制_java

注意:上图中圆形图标虽然标记的是对象,但实际上代表的是此对象在内存中的引用。包括 GC Root 也是一组引用而并非对象。

1.3 GC Root 对象

在 Java 中,有以下几种对象可以作为 GC Root:

  • Java 虚拟机栈(局部变量表)中的引用的对象
  • 方法区中静态引用指向的对象
  • 仍处于存活状态中的线程对象
  • Native 方法中 JNI 引用的对象。

注意:全局变量同静态变量不同,它不会被当作 GC Root。

1.4 什么时候回收

不同的虚拟机实现有着不同的 GC 实现机制,但是一般情况下每一种 GC 实现都会在以下两种情况下触发垃圾回收:

  • ​Allocation Failure​​:在堆内存中分配时,如果因为可用剩余空间不足导致对象内存分配失败,这时系统会触发一次 GC。
  • ​System.gc()​​:在应用层,Java 开发工程师可以主动调用此 API 来请求一次 GC。

2. 垃圾标记算法

2.1 引用计数算法

GC 回收机制_老年代_02


循环引用的问题不好解决

2.2 根搜索算法

GC 回收机制_java_03


eg: 开启一个线程后,线程对象作为一个 GC Root,一旦该线程对象被销毁之后,引用它的 Object 都变为不可达。

3. 清除算法

3.1 标记-清除算法

GC 回收机制_垃圾回收_04

从 ​​GC Roots​​​ 集合开始,将内存整个遍历一次,保留所有可以被 ​​GC Roots​​ 直接或间接引用到的对象,而剩下的对象都当作垃圾对待并回收,过程分两步;

  • ​Mark 标记阶段​​:找到内存中的所有 GC Root 对象,只要是和 GC Root 对象直接或者间接相连则标记为灰色(也就是存活对象),否则标记为黑色(也就是垃圾对象)
  • ​Sweep 清除阶段​​:当遍历完所有的 GC Root 之后,则将标记为垃圾的对象直接清除

GC 回收机制_老年代_05

3.1.1 小结

  • 优点:实现简单,不需要将对象进行移动。
  • 缺点:
  • 在进行 GC 的时候,需要暂停虚拟机的工作 -->​​stop the world​
  • 而且需要遍历整个堆内存,效率低,清除时会产生大量内存碎片, 提高了垃圾回收的频率。

3.2 复制算法

GC 回收机制_老年代_06

将现有的内存空间分为两快,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中。之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。

  • 复制算法之前,内存分为 A/B 两块,并且当前只使用内存 A,内存的状况如下图所示:

GC 回收机制_老年代_07


标记完之后,所有可达对象都被按次序复制到内存 B 中,并设置 B 为当前使用中的内存。内存状况如下图所示:

GC 回收机制_java_08

3.2.2 小结

  • 优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。
  • 缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。

3.3 标记-压缩算法

GC 回收机制_老年代_09

需要先从根节点开始对所有可达对象做一次标记,之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。最后,清理边界外所有的空间。因此标记压缩也分两步完成:

  • ​Mark 标记阶段​​:找到内存中的所有 GC Root 对象,只要是和 GC Root 对象直接或者间接相连则标记为灰色(也就是存活对象),否则标记为黑色(也就是垃圾对象)。
  • ​Compact 压缩阶段​​:将剩余存活对象按顺序压缩到内存的某一端。

4. JVM分代回收策略

GC 回收机制_老年代_10


Java 虚拟机根据对象存活的周期不同,把堆内存划分为几块,一般分为 ​​新生代​​​、​​老年代​​,这就是 JVM 的内存分代策略。

注意: 在 HotSpot 中除了新生代和老年代,还有​​永久代​​。

4.1 新生代

新生成的对象优先存放在新生代中,新生代对象存活率很低,在新生代中,常规应用进行一次垃圾收集一般可以回收 70%~95% 的空间,回收效率很高。新生代中因为要进行一些复制操作,所以一般采用的 GC 回收算法是​​复制算法​​。

新生代又可以继续细分为 3 部分:​​Eden​​​、​​Survivor0(简称 S0)​​​、​​Survivor1(简称S1)​​​。这 3 部分按照 ​​8:1:1​​ 的比例来划分新生代。

GC 回收机制_垃圾回收_11

  • 绝大多数刚刚被创建的对象会存放在​​Eden​​ 区
  • 当​​Eden​​​ 区第一次满的时候,会进行垃圾回收。首先将​​Eden​​​ 区的垃圾对象回收清除,并将存活的对象复制到​​S0​​​,此时​​S1​​ 是空的
  • 下一次​​Eden​​​ 区满时,再执行一次垃圾回收。此次会将​​Eden​​​ 和​​S0​​​ 区中所有垃圾对象清除,并将存活对象复制到​​S1​​​,此时​​S0​​ 变为空
  • 如此反复在​​S0​​​ 和​​S1​​​ 之间切换几次(​​默认 15 次​​)之后,如果还有存活对象。说明这些对象的生命周期较长,则将它们转移到老年代中。

4.2 老年代

一个对象如果在新生代存活了足够长的时间而没有被清理掉,则会被复制到老年代。老年代的内存大小一般比新生代大,能存放更多的对象。如果对象比较大(比如长字符串或者大数组),并且新生代的剩余空间不足,则这个大对象会直接被分配到老年代上。

我们可以使用 ​​-XX:PretenureSizeThreshold​​ 来控制直接升入老年代的对象大小,大于这个值的对象会直接分配在老年代上。老年代因为对象的生命周期较长,不需要过多的复制操作,所以一般采用标记压缩的回收算法

注意:对于老年代可能存在这么一种情况,老年代中的对象有时候会引用到新生代对象。这时如果要执行新生代 GC,则可能需要查询整个老年代上可能存在引用新生代的情况,这显然是低效的。所以,老年代中维护了一个 512 byte 的 card table,所有老年代对象引用新生代对象的信息都记录在这里。每当新生代发生 GC 时,只需要检查这个 card table 即可,大大提高了性能。

5. GC Log 分析

新生代和老年代所打印的日志是有区别的:

  • ​新生代 GC​​:这一区域的 GC 叫作 Minor GC。因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
  • ​老年代 GC​​:发生在这一区域的 GC 也叫作 Major GC 或者 Full GC。当出现了 Major GC,经常会伴随至少一次的 Minor GC。

注意:在有些虚拟机实现中,​​Major GC​​​ 和 ​​Full GC​​​ 还是有一些区别的。​​Major GC​​​只是代表回收老年代的内存,而 ​​Full GC​​ 则代表回收整个堆中的内存,也就是新生代 + 老年代。

GC 回收机制_java_12


GC 回收机制_java_13

6. Java 四种引用

GC 回收机制_垃圾回收_14


需要注意的是,被软引用对象关联的对象会自动被垃圾回收器回收,但是软引用对象本身也是一个对象,这些创建的软引用并不会自动被垃圾回收器回收掉。

GC 回收机制_垃圾回收_15

Android官方在对 ​​SoftReference​​ 的介绍中,也已经不建议使用它来实现缓存功能。

7. Android 中的内存抖动

GC 回收机制_老年代_16

8. 参考链接