9. 垃圾收集器与内存分配策略

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

整体思路

先考虑3个问题

  1. 哪些内存需要收集

    • 堆和方法区需要收集程序计数器、虚拟机栈、本地方法栈都不需要做垃圾回收按照其功能很容易理解
  2. 什么时候收集

    • 对象已死。引申出另一个问题怎么判断对象已死呢
    • 当程序内存不足时
  3. 怎么收集

    • 分代收集理论

    • 垃圾回收算法

一、对象已死

1.1 引用计数法

定义在对象中添加一个引用计数器每当有一个地方引用它时计数器值就加一;当引用失效时计数器值就减一任何时刻计数器为零的对象就是不可能再被使用的。

**优点**原理简单判定效率也很高在大多数情况下它都是一个不错的算法。

**缺点**循环引用问题。两个对象再无任何引用实际上这两个对象已 经不可能再被访问但是它们因为互相引用着对方导致它们的引用计数都不为零引用计数算法也 就无法回收它们。

Java虚拟机用的不是引用计数法

1.2 可达性分析

**定义**通过一系列成为 “GC Roots” 的根对象作为起始节点集从这些节点开始根据引用关系向下搜索搜索过所走的路径成为引用链如果某个对象到 GC Roots 间没有任何引用链相连则证明此对象是不可达的。

可作为 GC Roots 的对象

  • 虚拟机栈栈帧中的本地变量表中引用的对象如方法参数、局部变量、临时变量等
  • 方法区中类静态属性引用的对象如 Java 类的应用类型的静态变量
  • 方法区中常量引用的对象如字符串常量池里的引用
  • 本地方法栈中 JNI Native 方法引用的对象
  • Java虚拟机内部的引用如基本数据类型对应的Class对象一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等还有系统类加载器
  • 所有被同步锁(synchronized关键字)持有的对象
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等

在这里插入图片描述

1.3 引用详细分类

**背景**在 JDK1.2 版以前Java 里的引用是很传统的定义即 reference 类型的数据中存储的数值代表另一块内存的起始地址就称该 reference 数据代表某块内存、某个内存的引用。在这种定义下一个对象只有 “被引用” 或者 “未被引用” 两种状态对于描述一些“ 食之无味弃之可惜”的对象就显 得无能为力。譬如我们希望能庙述一类对象当内存空间还足够时能保留在内存之中如果内存空间在进行垃圾收集后仍然非常紧张那就可以抛弃这些对象——很多系统的缓存功能都符合这样的应 用场景。

所以在 JDK 1.2 版之后Java 对引用的概念进行了扩充将引用分为强引用、软引用、弱引用和虚引用四种。

**强引用**指最传统的引用定义即 Object obj = new Object() 这种引用关系。只要强引用关系还在对象永远不会被回收。

**软引用**描述一些还有用但非必须的对象。只被软引用关联着的对象

**弱引用**来描述那些非必须对象但是它的强度比软引用更弱一些。
**虚引用**是最弱的一种引用关系。一个对象是否有虚引用的 存在完全不会对其生存时间构成影响也无法通过虚引用来取得一个对象实例。为一个对象设置虚 引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。

二、垃圾回收算法

2.1 分代收集理论

2.1.1 背景

当前商业虚拟机的垃圾收集器大多遵循了 “分代收集” 的理论进行设计它建立在两个分代假说之上

  1. 弱分代假说绝大多数对象都是朝生夕灭的。
  2. 强分代假说熬过越多次垃圾收集过程的对象就越难以消亡。

这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将Java堆划分 出不同的区域然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。

好处将大多数朝生夕灭的对象集中放在一起每次回收时只需要关注如何保留少量存活而不是去标注那些大量要被回收的对象就能以较低代价回收大量空间将剩下的难以消亡的对象集中放在一块虚拟机便可以较低频率回收这个区域这就同时兼顾了垃圾回收时间和内存空间的有效利用。

2.1.2 应用

回收类型的划分根据回收区域的位置不同划分出 Minor GC、Major GC、Full GC

回收算法根据不同区域中对象存活特征的不同发展出标记-复制算法、标记-清除算法、标记-整理算法

2.1.3 存在的难点——跨代引用

**问题**分代收集并非只是简单划分一下内存区域那么容易它至少存在一个明显的困难:对象不 是孤立的对象之间会存在跨代引用假如要现在进行一次只局限于新生代区域内的收集(Minor GC)但新生代中的对象是完全有可 能被老年代所引用的为了找出该区域中的存活对象不得不在固定的GC Roots之外再额外遍历整个老年代中所有对象来确保可达性分析结果的正确性。

为了解决这个问题添加第三条经验法则

  1. 跨代引用假说: 跨代引用相对于同代引用来说仅占极少数。

解决方式依据这条假说我们不必扫描整个老年代也不必浪费空间专门记录每一个对象是否存在以及存在哪些跨代引用只需在新生代上建立一个全局的数据结构(该结构被称 为**“记忆集”**Remembered Set)这个结构把老年代划分成若干小块标识出老年代的哪一块内存会 存在跨代引用。此后当发生Minor GC时只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。

2.2 标记-清除法

**算法思路**算法分为 “标记” 和 “清除” 两个阶段首先标记出所有需要回收的对象在标记完成后统一回收掉所有被标记的对象。也可以反过来。

**优点**简单

缺点

  1. 执行效率不稳定。果Java堆中包含大量对 象而且其中大部分是需要被回收的这时必须进行大量标记和清除的动作导致标记和清除两个过 程的执行效率都随对象数量增长而降低

  2. 内存空间碎片化的问题标记、清除之后会产生大量不连续的内存碎片导致大对象无法找到足够的内存分配从而会触发另一次垃圾回收

在这里插入图片描述

2.3 标记-复制法

**算法思路**为了解决标记-清除算法面对大量可回收对象时执行效率低 的问题1969年Fenichel提出了一种称为“半区复制”(Semispace Copying)的垃圾收集算法它将可用 内存按容量划分为大小相等的两块每次只使用其中的一块。当这一块的内存用完了就将还存活着 的对象复制到另外一块上面然后再把已使用过的内存空间一次清理掉。

**优点**实现简单运行高效。每次都针对整个半区进行清理不用考虑内存碎片的问题而且内存中对象大多数都是可回收的所以需要复制的对象并不多。

**缺点**这种复制回收法将可用内存缩小为原来的一半空间浪费太多。

在这里插入图片描述

**改进**IBM对新生代 “朝生夕灭” 的特点做了更量化的诠释——新生代中的对象有98%熬不过第一轮收集。因此并不需要按照1∶1的比例来划分新生代的内存空间。Andrew Appel 提出了一种更优化的半区复制分代策略将新生代分为一块较大的Eden空间和两块较小的Survivor空间每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时将Eden和Survivor中仍 然存活的对象一次性复制到另外一块Survivor空间上然后直接清理掉Eden和已用过的那块Survivor空。间。HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1也即每次新生代中可用内存空间为整个新 生代容量的90%(Eden的80%加上一个Survivor的10%)只有一个Survivor空间即10%的新生代是会 被“浪费”的。

**内存分配担保**上述改进是基于98%的对象可被回收的理论任何人都没有办法百分百 保证每次回收都只有不多于10%的对象存活因此Appel式回收还有一个充当罕见情况的“逃生门”的安全设计当Survivor空间不足以容纳一次Minor GC之后存活的对象时就需要依赖其他内存区域(实际上大多就是老年代)进行分配担保(Handle Promotion)。

**缺点**老年代没有可用的内存为其作担保

2.4 标记-整理法

**定义**老年代没有可用的内存为其作担保所以不可用标记-复制算法。标记-整理法与标记-清除算法一样但后续步骤不是直接对可 回收对象进行清理而是让所有存活的对象都向内存空间一端移动然后直接清理掉边界以外的内存。

在这里插入图片描述

**优点**可以避免空间碎片化问题

**缺点**移动存活对象并更新所有引用这些对象的地方是一种极为负重的操作这种对象移动操作必须全程暂停用户应用程序才能进行。ZGC和Shenandoah收集器使用读屏障(Read Barrier)技术实现了整理过程与用户线程的并发 执行

三、Hotspot虚拟机的实现

第一章和第二章从原理上讲了常见的对象存活判定算法和垃圾收集算法Java虚拟机实现这些算法时必须对算法的执行效率有严格的考量才能保证虚拟机的高效运行。本章介绍 Hotspot 虚拟机是如何高效实现上述算法的。

下面这些技术都是针对可达性分析遇到的问题的解决方案

3.1 根节点枚举

**问题**在做可达性分析时首先需要查找 GC Roots查找过程实现高效不是一件容易的事情光是光是方法区的大小就常有数百上千兆里面的类、常量等更是恒河沙数若要逐个检 查以这里为起源的引用肯定得消耗不少时间。

解决方式 虚拟机记录哪些地方时存放着对象引用的。在HotSpot虚拟机里使用一组称为 OopMap 的数据结构来达到这个目的。一旦类加载完成的时候HotSpot 就会把对象内什么偏移量上是什么类型的数据计算出来在即时编译过程中也 会在特定的位置记录下栈里和寄存器里哪些位置是引用。这样收集器在扫描时就可以直接得知这些信 息了并不需要真正一个不漏地从方法区等GC Roots开始查找。

**注意事项**迄今为止所有收集器在根节点枚举这一步骤时都是必须暂停用户线程的。现在可达性分析算法耗时 最长的查找引用链的过程已经可以做到与用户线程一起并发具体见3.6但根节点枚举始终还 是必须在一个能保障一致性的快照中才得以进行。

3.2 安全点

**问题**在OopMap的协作下HotSpot可以快速准确地完成 GC Roots 枚举但是导致 OopMap 内容变化的指令非常多如果为每一条指令都生成对应的 OopMap那将需要大量的额外存储空间。

解决方式在 “特定的位置” 生成 OopMap这些位置成为 安全点。有了安全点的设定也就决定了用户程序执行时并非在指令流的任意位置都能停顿下来开始垃圾收集而是必须达到安全点后才能暂停

**安全点的选取**以 “是否具有让程序长时间执行的特征” 为标准。“长时间执行” 的明显特征是指令序列的复用如方法调用、循环跳转、异常跳转等。

**注意**当用户的所有线程到达安全点后才可以进行垃圾收集。

3.3 安全区域

**问题**如果存在用户线程处于不执行如线程处于 Sleep 或 Blocked状态那就到达不了安全点就没办法进入垃圾回收了。对于这种情况必须引入安全区域来解决。

解决方式安全区域是指能够确保在某一段代码片段之中引用关系不会发生变化因此在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸了的安全点。当用户线程进入到安全区域里面的代码时首先会标识自己已经进入了安全区域。当用户离开安全区域时只有在完成根节点枚举的情况下才能离开。

3.4 记忆集和卡表

问题 在 GC Roots 扫描时有些对象存在跨代引用的情况为了避免将整个老年代加入扫描范围垃圾收集器在新生代中建立了名为记忆集的数据结构。

**记忆集**记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。实现记忆集可以有不同的精度

  • **字长精度**每个记录精确到一个机器字长该字包含跨代指针。
  • **对象精度**每个记录精确到一个对象该对象里有字段含有跨代指针。
  • **卡精度**每个记录精确到一块内存区域该区域内有对象含有跨代指针。可以用 “卡表” 的方式实现记忆集这也是目前最常用的一种记忆集实现形式。

**卡表**卡表最简单的形式可以只是一个字节数组[2]而HotSpot虚拟机确实也是这样做的。以下这行代 码是HotSpot默认的卡表标记逻辑[3]:

CARD_TABLE [this address >> 9] = 0;

字节数组CARD_TABLE的每一个元素都对应着其标识的内存区域中一块特定大小的内存块这个 内存块被称作“卡页”(Card Page)。一般来说卡页大小都是以2的N次幂的字节数通过上面代码可 以看出HotSpot中使用的卡页是2的9次幂即512字节(地址右移9位相当于用地址除以512)。

一个卡页的内存中通常包含不止一个对象只要卡页内有一个(或更多)对象的字段存在着跨代 指针那就将对应卡表的数组元素的值标识为1称为这个元素变脏(Dirty)没有则标识为0。

在这里插入图片描述

3.5 写屏障

**问题**有其他分代区域中对象引用了本区域对象时其对应的卡表元素就应该变脏变脏时间点原则上应该发生在引用类型字段赋值的那一刻。但问题是如何变脏即如何在对象赋值的那一刻去更新维护卡表呢? 在编译执行的场景中经过即时编译后的代码已经是纯粹的机器指令流了这就必须找到一个在机器码层面的手段把维护卡表的动作放到每一个赋值操作之中。

**写屏障**HotSpot虚拟机里是通过写屏障(Write Barrier)技术维护卡表状态的。写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切面在引用对象赋值时会产生一个环形(Around)通知供程序执行额外的动作也就是说赋值的前后都在写屏障的覆盖范畴内。

应用写屏障后虚拟机就会为所有赋值操作生成相应的指令一旦收集器在写屏障中增加了更新卡表操作无论更新的是不是老年代对新生代对象的引用每次只要对引用进行更新就会产生额外 的开销不过这个开销与Minor GC时扫描整个老年代的代价相比还是低得多的。

3.6 并发的可达性分析

看书3.4.6

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6