『深入学习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 时收到一个系统通知。
垃圾回收算法
标记-清除算法
算法描述
最基础的垃圾收集算法。
- 标记
标记出所有需要回收的对象。如上图:对象B、D、F。
这个标记的过程,实际上也就是使用上述的可达性分析算法,来判定的。
- 删除
删除被标记的对象。
缺点
- 执行效率不稳定。受对象数量影响。
- 产生内存碎片。
标记-复制算法
算法描述
现在的商用 Java 虚拟机新生代,大多使用标记-复制算法。
把内存分为大小相等的两块。一块使用完了,就把还存活的对象复制到另外一块儿上。然后把使用的内存块一次性清除掉。
HotSpot 虚拟机的 Serial、ParNew 等收集器均采用了更优化的标记复制策略--Appel式回收。
我们上一节学习到的新生代内存布局,就是依据这种策略设计的。
GC 时,把 Eden 区和 Survivor(0) 区中存活的对象,一次性复制到另一块 Survivor(1) 区中。然后直接清除掉 Eden 和 Survivor(0) 区。
当 Survivor(1) 区,不足以容纳一次 Minor GC 后存活的对象时,就直接分配对象至老年代,称之为担保分配机制。
缺点
- 对象存活率高时,进行较多复制操作,降低效率。
- 浪费内存空间。
标记-整理算法
算法描述
针对老年代对象的存亡特征,提出了标记-整理算法。
标记过程与"标记-清除"算法一致。后续步骤为让所有存活的对象,向一端移动,随后清除存活边界以外的内存空间。
缺点
- 移动存活对象,并更新所有引用对象,需要暂停用户线程。称之为 "Stop The World"。
垃圾回收器
上图展示了用于不同分代的收集器。
Serial 收集器
- 初代收集器。
- 单线程工作,进行垃圾收集时,会 STW。
ParNew 收集器
- Serial 收集器的多线程并行版本,JDK 7 前新生代首选收集器。
- 基本已经退出了历史舞台。
Parallel Scavenge 收集器
- 一款新生代收集器,关注达到可控制的吞吐量。
Serial Old 收集器
- Serial 的老年代版本。供客户端模式下的 HotSpot 虚拟机使用。
Parallel Old 收集器
- Parallel Scavenge 的老年代版本,支持多线程并行收集。
CMS 收集器
-
一种以获取最短回收停顿时间为目标的处理器。
-
回收过程较为复杂:
- 初始标记 "STW"
标记 GC Roots 能关联到的对象,速度较快。需要 STW。
- 并发标记
从 GC Roots 的直接关联对象开始,遍历整个对象图。不需要 STW。
- 重新标记 "STW"
修正并发标记期间,由于用户线程运作产生的标记变动。需要 STW。
- 并发清除
清除已标记死亡的对象。不需要 STW。
Garbage First 收集器
-
里程碑式收集器,开创面向局部收集设计思路和基于 Region 的内存布局形式。
-
G1 收集器,不划分固定大小与数量的分代,而是把连续的 Java 堆,划分为多个大小相等的独立区域(Region)。
每个 Region 动态扮演新生代的 Eden 空间、Survivor 空间或者老年代。
-
Region 有一类特殊的 Humongous 区域,专门用于存储大对象。G1 基本将其看作老年代的一部分。
-
运行过程大致分为四个步骤:
- 初始标记。
标记 CG Roots 能关联到的对象,修改 TAMS 指针。
- 并发标记。
进行可达性分析,扫描要回收的对象。
扫描完后,重新处理 SATB 记录下的有引用变动的对象。
- 最终标记。
暂停用户线程,处理并发阶段后遗留的少量 SATB 记录。
- 筛选回收。
更新 Region 的统计数据,对 Region 的回收价值和成本做排序,根据用户期望来执行回收。
把回收 Region 存活对象复制到空 Region 中,并清除掉回收 Region。这个过程中需要 STW。
Shenandoah 收集器
- Shenandoah 是由 RedHat 独立发展的新型收集项目,后贡献至 OpenJDK。
- 这是一款目标为低延迟的垃圾收集器。
ZGC 收集器
- 与 Shenandoah 相似,一款官方出品的低延迟的垃圾收集器。
- 一款基于 Region 内存布局,不设分代,使用了读屏障、染色指针和内存多重映射来实现可并发标记-整理算法的,以低延时为首要目标的垃圾收集器。