『深入学习Java』(二) JVM 垃圾回收

200 阅读6分钟

『深入学习Java』(二) JVM 垃圾回收

前言

上一篇,学习了 JVM 内存区域。我们对 JVM 内存布局有一定了解了。这一篇,我们继续来学习 JVM GC 机制,即 JVM 垃圾回收机制。

何如判断对象"已死"?

引用计数算法

在对象中添加一个引用计数器。有一个地方引用它,计数器加一;引用失效时,计数器减一。计数器为零的对象就是不可再使用的。

弊端:循环引用时,两个对象的计数都为1,导致两个对象都无法被释放。

可达性分析算法

扫描堆中的对象,看能否沿着 GC Root 对象为起点的引用链找到该对象,如果找不到,则表示可以回收。

Java 中即是通过可达性分析来判定对象是否存活。

可以作为 GC Roots 的对象:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中 JNI(即一般说的Native方法)引用的对象。
  • Java 虚拟机内部的引用,如基本数据类型对应的 Class 对象。
  • 所有被同步锁(synchronized) 持有的对象。

Java 中的引用类型

  • 强引用 Strongly Reference

  普通程序中Object o = new Object(),这种引用关系。

  无论任何情况下,只要有强引用关系,该对象永远不会被回收。

  • 软引用 Soft Reference

  用于描述还有用,但非必须的对象。

  系统将要发生内存溢出前,会把这些对象列进回收范围之中进行第二次回收。如果这种回收还没有足够的内存,将会抛出内存溢出异常。

  • 弱引用  Weak Reference

  也用于非必须对象,比软引用更弱一些。

  弱引用关联的对象,只能生存到下一次 GC 为止。

  • 虚引用 Phantom Reference

  最弱的一种引用关系。该引用关系不会对生存时间造成影响。

  设置弱引用的唯一目的是,该对象在被 GC 时收到一个系统通知。

垃圾回收算法

标记-清除算法

算法描述

最基础的垃圾收集算法。

  1. 标记

  标记出所有需要回收的对象。如上图:对象B、D、F。

  这个标记的过程,实际上也就是使用上述的可达性分析算法,来判定的。

  1. 删除

  删除被标记的对象。

缺点
  1. 执行效率不稳定。受对象数量影响。
  1. 产生内存碎片。

标记-复制算法

算法描述

现在的商用 Java 虚拟机新生代,大多使用标记-复制算法。

把内存分为大小相等的两块。一块使用完了,就把还存活的对象复制到另外一块儿上。然后把使用的内存块一次性清除掉。

HotSpot 虚拟机的 Serial、ParNew 等收集器均采用了更优化的标记复制策略--Appel式回收。

我们上一节学习到的新生代内存布局,就是依据这种策略设计的。

GC 时,把 Eden 区和 Survivor(0) 区中存活的对象,一次性复制到另一块 Survivor(1) 区中。然后直接清除掉 Eden 和 Survivor(0) 区。

当 Survivor(1) 区,不足以容纳一次 Minor GC 后存活的对象时,就直接分配对象至老年代,称之为担保分配机制。

缺点

  1. 对象存活率高时,进行较多复制操作,降低效率。
  1. 浪费内存空间。

标记-整理算法

算法描述

针对老年代对象的存亡特征,提出了标记-整理算法。

标记过程与"标记-清除"算法一致。后续步骤为让所有存活的对象,向一端移动,随后清除存活边界以外的内存空间。

缺点

  1. 移动存活对象,并更新所有引用对象,需要暂停用户线程。称之为 "Stop The World"。

垃圾回收器

上图展示了用于不同分代的收集器。

Serial 收集器

  • 初代收集器。
  • 单线程工作,进行垃圾收集时,会 STW。

ParNew 收集器

  • Serial 收集器的多线程并行版本,JDK 7 前新生代首选收集器。
  • 基本已经退出了历史舞台。

Parallel Scavenge 收集器

  • 一款新生代收集器,关注达到可控制的吞吐量。

Serial Old 收集器

  • Serial 的老年代版本。供客户端模式下的 HotSpot 虚拟机使用。

Parallel Old 收集器

  • Parallel Scavenge 的老年代版本,支持多线程并行收集。

CMS 收集器

  • 一种以获取最短回收停顿时间为目标的处理器。

  • 回收过程较为复杂:

  1. 初始标记 "STW"

 标记 GC Roots 能关联到的对象,速度较快。需要 STW。

  1. 并发标记

 从 GC Roots 的直接关联对象开始,遍历整个对象图。不需要 STW。

  1. 重新标记 "STW"

 修正并发标记期间,由于用户线程运作产生的标记变动。需要 STW。

  1. 并发清除

 清除已标记死亡的对象。不需要 STW。

Garbage First 收集器

  • 里程碑式收集器,开创面向局部收集设计思路和基于 Region 的内存布局形式。

  • G1 收集器,不划分固定大小与数量的分代,而是把连续的 Java 堆,划分为多个大小相等的独立区域(Region)。

  每个 Region 动态扮演新生代的 Eden 空间、Survivor 空间或者老年代。

  • Region 有一类特殊的 Humongous 区域,专门用于存储大对象。G1 基本将其看作老年代的一部分。

  • 运行过程大致分为四个步骤:

  1. 初始标记。

 标记 CG Roots 能关联到的对象,修改 TAMS 指针。

  1. 并发标记。

 进行可达性分析,扫描要回收的对象。

 扫描完后,重新处理 SATB 记录下的有引用变动的对象。

  1. 最终标记。

 暂停用户线程,处理并发阶段后遗留的少量 SATB 记录。

  1. 筛选回收。

 更新 Region 的统计数据,对 Region 的回收价值和成本做排序,根据用户期望来执行回收。

 把回收 Region 存活对象复制到空 Region 中,并清除掉回收 Region。这个过程中需要 STW。

Shenandoah 收集器

  • Shenandoah 是由 RedHat 独立发展的新型收集项目,后贡献至 OpenJDK。
  • 这是一款目标为低延迟的垃圾收集器。

ZGC 收集器

  • 与 Shenandoah 相似,一款官方出品的低延迟的垃圾收集器。
  • 一款基于 Region 内存布局,不设分代,使用了读屏障、染色指针和内存多重映射来实现可并发标记-整理算法的,以低延时为首要目标的垃圾收集器。