×

JVM:这是一份全面 & 详细的 (GC)垃圾收集算法 讲解攻略

96
Carson_Ho 595a1b60 08f6 4beb 998f 2bf55e230555
2018.10.10 09:40 字数 2566

前言

  • 不同的内存区域采用不同的垃圾收集算法
  • 而不同垃圾收集算法决定了垃圾收集的效率 & 效果
  • 本文将全面讲解垃圾收集算法,包括标记-清除、复制、标记-整理等,希望你们会喜欢

在接下来的日子,我会推出一系列讲解JVM的文章,具体如下;感兴趣可持续关注Carson_Ho的安卓开发笔记

示意图

目录

示意图

1. 垃圾收集算法 类型

  • 垃圾收集算法 类型 分为4类,如下图:
垃圾收集算法类型
  • 下面我将对每个进行详细讲解。

2. 标记-清除 算法

这是 垃圾收集算法中 最最基础的算法。

2.1 算法思想

算法分为两个阶段:

  1. 标记阶段:标记出所有需要回收的对象;
  2. 清除阶段:统一清除(回收)所有被标记的对象。

下面主要讲解标记阶段。标记阶段主要分为:(先进行可达性分析)

  1. 第一次标记 & 筛选
  2. 第二次标记 & 筛选

a. 可达性分析

阅读前请看文章

b. 第一次标记 & 筛选

i. 方式描述

对象 在 可达性分析中 被判断为不可达后,会被第一次标记 & 筛选

a. 不筛选 = 继续留在 ”即将回收“的集合里,准备被回收
b. 筛选 = 从 ”即将回收“的集合取出

ii. 筛选的标准

该对象是否有必要执行 finalize()方法

  1. 若有必要执行(人为设置),则筛选出来,进入下一阶段:第二次标记 & 筛选;
  2. 若没必要执行,判断该对象死亡,不筛选 并等待回收

当对象无 finalize()方法 或 finalize()已被虚拟机调用过,则视为“没必要执行”


c. 第二次标记 & 筛选

当对象经过了第一次的标记 & 筛选,会被进行第二次标记,并被进行 筛选

i. 方式描述

该对象会被放到一个 F-Queue 队列中,并由 虚拟机自动建立、优先级低的Finalizer 线程去执行 队列中该对象的finalize()

  1. finalize()只会被执行一次
  2. 但并不承诺等待finalize()运行结束。这是为了防止 finalize()执行缓慢 / 停止 使得 F-Queue队列其他对象永久等待。

ii. 判断标准

在执行finalize()过程中,若对象依然没与引用链上的GC Roots 直接关联 或 间接关联(即关联上与GC Roots 关联的对象),那么该对象将被判断死亡,不筛选(留在”即将回收“集合里) 并 等待回收


总结

总结图

2.2 优点

算法简单、实现简单

2.3 缺点

  • 效率问题:即 标记和清除 两个过程效率不高
  • 空间问题:标记 - 清除后,会产生大量不连续的内存碎片。
示意图

这导致 以后程序 需要分配较大空间对象时 无法找到足够大的连续内存 而被迫 触发另外一次垃圾收集行为,这导致非常浪费资源。

下面继续介绍的算法就是为了解决上述两个问题的。

2.4 应用场景

对象存活率较低 & 垃圾回收行为频率低 的场景

如老年代区域,因为老年代区域回收频次少、回收数量少,所以对于效率问题 & 空间问题不会很明显。


3. 复制算法

该算法的出现是为了解决 标记-清除算法中 效率 & 空间问题的。

3.1 算法思想

  • 将内存分为大小相等的两块,每次使用其中一块;
  • 当 使用的这块内存 用完,就将 这块内存上还存活的对象 复制到另一块还没试用过的内存上
  • 最终将使用的那块内存一次清理掉。

示意图如下:

示意图

3.2 优点

  1. 解决了标记-清除算法中 清除效率低的问题

每次仅回收内存的一半区域

  1. 解决了标记-清除算法中 空间产生不连续内存碎片的问题

将已使用内存上的存活对象 移动到栈顶的指针,按顺序分配内存即可。

3.3 缺点

  1. 每次使用的内存缩小为原来的一半。
  2. 当对象存活率较高的情况下需要做很多复制操作,即效率会变低

3.4 应用场景

对象存活率较低 & 需要频繁进行垃圾回收 的区域

如新生代区域

3.5 特别注意

a. 背景
新生代区域在进行垃圾回收时,98%对象都必须得回收

b. 问题
复制算法中 每次使用的内存缩小为原来的一半 利用率低 & 代价太高

c. 解决方案

  • 不 按 1:1的比例 划分内存,而是 按8:1:1比例 将内存划分为一块较大的 Eden 和两块较小的 Survivor 区域(From SurvivorTo Survivor
示意图
  • 每次使用EdenFrom Survivor区域;
  • 用完后就 将上述两块区域存活的对象 复制到To Survivor区域上
  • 最终一次清理掉EdenFrom Survivor区域

使用逻辑 同 改进前


很多同学会问,假如 EdenFrom Survivor区域上存活对象所需内存大小 > To Survivor区域怎么办?

解决方案:依赖老年代内存区域 做 内存分配担保。

To Survivor区域 存不下来的对象 会通过 内存分配担保机制 暂时保存在老年代


4. 标记 - 整理 算法

此算法类似于第一种标记 - 清除 算法,只是在中间加多了一步:整理内存。

4.1 算法思路

算法分为三个阶段:

  1. 标记阶段:标记出所有需要回收的对象;
  2. 整理阶段:让所有存活的对象都向一端移动
  3. 清除阶段:统一清除(回收)端以外的对象。

示意图如下:

示意图

4.2 优点

  • 解决了标记-清除算法中 清除效率低的问题:一次清楚端外区域
  • 解决了标记-清除算法中 空间产生不连续内存碎片的问题:将已使用内存上的存活对象 移动到栈顶的指针,按顺序分配内存即可。

4.3 应用场景

对象存活率较低 & 垃圾回收行为频率低 的场景

如老年代区域,因为老年代区域回收频次少、回收数量少,所以对于效率问题 & 空间问题不会很明显。


5. 分代收集算法

主流的虚拟机基本都采用该算法,下面会着重讲解。

5.1 算法思路

  • 根据 对象存活周期的不同Java堆内存 分为:新生代 & 老年代 。分配比例如下:
示意图
  • 根据 两块区域特点 选择 对应的垃圾收集算法(即上面介绍的算法),具体细节请看下图
示意图

5.2 具体存储过程

  1. 新建的对象 一般会被优先分配到新生代的Eden区、From Survivor
  • 大对象(如很长的字符串以及数组)会直接分配到老年代,这是为了避免在 Eden 区 和 Survivor区之间发生大量的内存复制(因为新生代会采用复制算法进行垃圾收集)
  1. 这些对象经过第一次 Minor GC后,若仍然存活,将会被移到To Survivor

一次清理掉EdenFrom Survivor区域

  1. To Survivor 区每经过一轮 Minor GC ,该对象的年龄就+1
  2. 当对象年龄达到一定时(阈值默认=15),就会被移动到老年代。
  • 即新生代的对象在存活一定时间后,会被移动存储到老年代区域。
  • 还有一种 新生代对象被移懂到老年代区域 的情况是:动态对象年龄判定。即如果在Survivor区中 所有相同年龄对象的大小总和 大于
    Survivor区内存大小一半时,所有大于或等于该年龄的对象都会直接进入老年代。

特别注意

From SurvivorTo Survivor之间会经常互换角色。

每次发生GC时,把Eden区和 From Survivor区中 存活且没超过年龄阈值的对象 复制到To Survivor区中(此时To Survivor变成了From Survivor),然后From Survivor清空(此时From Survivor变成了To Survivor

5.2 优点

效率高、空间利用率高

根据不同区域特点 选择 不同的垃圾收集算法

5.3 应用场景

现在主流的虚拟机基本都采用 分代收集算法 ,即根据不同区域特点选择不同垃圾收集算法。

  1. 新生代 区域:采用 复制算法
  2. 老年代 区域:采用 标记-清除 算法、标记 - 整理 算法

6. 总结

  • 用一张图总结上述4个垃圾收集算法
总结
示意图

请点赞!因为你的鼓励是我写作的最大动力!

相关文章阅读
Android开发:最全面、最易懂的Android屏幕适配解决方案
Android事件分发机制详解:史上最全面、最易懂
Android开发:史上最全的Android消息推送解决方案
Android开发:最全面、最易懂的Webview详解
Android开发:JSON简介及最全面解析方法!
Android四大组件:Service服务史上最全面解析
Android四大组件:BroadcastReceiver史上最全面解析


欢迎关注Carson_Ho的简书!

不定期分享关于安卓开发的干货,追求短、平、快,但却不缺深度

JVM
Web note ad 1