【读书笔记】《深入理解Java虚拟机》垃圾回收

476 阅读11分钟

《深入理解Java虚拟机》第3章总结

垃圾收集范围

Java运行时内存区域中程序计数器、虚拟机栈和本地方法栈随线程而生随线程而灭,栈帧随着方法的进入和退出执行着对应的入栈和出栈操作,因此这3个区域的内存分配和回收都具有确定性。

Java运行时内存区域中堆和方法区的内存分配和回收都是动态的,这也是垃圾收集器关注的内存。

方法区内存回收

方法区可以不实现垃圾回收,因为方法区的垃圾收集性价比通常比较低。

方法区的垃圾回收主要回收:废弃的常量、不再使用的类型。

识别类型不再使用必须同时满足3个条件:

  1. 该类所有的实例已经被回收;
  2. 加载该类的类加载器已经被回收(除非特殊设计,否则很难达成);
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

对象已死?

引用计数法

主流的Java虚拟机都没有采用该算法。该算法有很多例外情况需要考虑,必须配合大量的额外处理才能保证正确的工作,比如解决对象的循环引用问题。

可达性分析算法

如果从根对象("GC Roots")到某个对象不可达时,说明该对象不可能再被使用(对象处于可恢复状态,执行对象的finalize()方法后依然没有被引用,则进入不可达状态,可以被垃圾收集器回收)。1个对象的finalize()方法最多只会被执行1次。

固定作为GC Roots的对象包括:

  1. 虚拟机栈(栈帧中本地变量表)中引用的对象
  2. 本地方法栈中JNI引用的对象
  3. 方法区中类静态属性引用的对象
  4. 方法区中常量引用的对象
  5. 所有被同步锁(synchronized关键字)持有的对象
  6. 反映Java虚拟机内部情况的JMXBean、本地代码缓存等

临时性加入:分代收集和局部回收方法中,回收新生代的内存时,老年代的对象可能引用新生代的对象,因此老年的关联对象也要加到GC Roots中。

垃圾收集算法

分代收集理论

商用虚拟机大多遵循“分代收集”的理论进行设计。分代收集理论的两个假说:

  • 弱分代假说:绝大多数对象都是朝生夕灭。
  • 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。
  • 跨代应用假说:跨代引用相对同代引用来说仅占极少数。

根据分代收集理论,将Java堆分为新生代和老年代两个区域,对不同的区域采用不同的收集算法和收集频率。

由于存在跨代应用,因此在回收新生代时,需要把整个老年代所有对象加入GC Roots进行可达性分析,这无疑会降低性能。根据跨代引用假说,在新生代建立记忆集(Remembered Set),该结构将老年代划分为若干个小块,标识出老年代哪块内存有跨代引用,以后发生新生代收集时,只需要把这块内存区域的老年代对象键入GC Roots即可。

垃圾收集方式

  • 部分收集(Partial GC):目标不是整个Java堆。

    1. 新生代收集(Minor GC/Young GC)
    2. 老年代收集(Major GC/Old GC):目前只有CMS有单独收集老年代的行为。
    3. 混合收集(Mixed GC):回收新生代和部分老年代,目前只有G1收集器有这种行为。
  • 整堆收集(Full GC):收集整个堆和方法区。

标记-清除算法

两个缺点:

  1. 执行效率不稳定,面对大量可回收对象时。
  2. 内存空间碎片化问题

标记-复制算法

解决标记-清除算法执行效率不稳定的问题。

Appel式回收:新生代分为1个Eden空间和2个Survivor空间,每次内存分配只使用Eden和1个Survivor空间。垃圾收集时,将Eden和已用过的那块Survivor空间中仍然存活的对象复制到另一块Survivor空间,然后直接清理掉Eden和已用过的那块Survivor空间。

Appel式回收的分配担保:当Survivor空间不足与容纳一次Minor GC之后存货的对象时,需要依赖其他内存区域(老年代)进行垃圾收集。具体方式:发生Minor GC前,只要老年代的连续空间大小大于新生代对象总大小或者历次回收晋升到老年代的平均大小,就进行Minor GC,否则进行Full GC。

标记-整理算法

标记-复制算法在对象存活率较高时需要进行较多次的复制,效率会降低。Appel式回收还需要分配担保。

老年代一般不使用标记-复制算法。

标记-整理和标记-清理算法的区别在于前者时移动式垃圾收集,后者是非移动式。

移动对象(标记-整理算法):内存回收时更加复杂;移动时必须全程暂停应用程序(Stop The World),垃圾回收时需要停顿时间,即延迟。

不移动对象(标记-清除算法):存在内存碎片化问题,内存分配时更加负责;垃圾回收时停顿时间短甚至不停顿

关注吞吐量采用标记-整理算法,关注延迟采用标记-清除算法。

注:吞吐量=运行用户代码时间/(运行用户代码时间+运行垃圾收集时间)。由于内存分配更加频繁,因此标记-整理算法的吞吐量更大。

HotSpot的实现细节

记忆集与卡表

记忆集:记录从非收集区域执行收集区域的指针集合的抽象数据结构。

卡表:记忆集的具体实现方式。

垃圾收集时,更具卡表中脏元素可以快速定位那些卡页包含跨代引用,将这些卡页内对象加入GC Roots一般扫描

并发的可达性分析

可达性分析分为两步:识别和GC Roots直接相连的对象(根节点标记)、从GC Roots直接关联的对象开始遍历整个对象图。为保证可达性分析的正确性,这两步必须全程冻结用户线程的进行。第一步由于GC Roots较少,Stop The World的时间较短,可以接受。但第二步和堆容量成正比,堆越大停顿时间越长。

为减少可达性分析的停顿时间,在第二步采用并发的方式进行,即用户线程同时也在执行。这时候要保证可达性分析的正确性,将可达性分析分为三步:初始标记(根节点标记、Stop The World)、并发标记、重新标记(Stop The World)。

并非标记中同时满足以下两个条件时间存在对象消失问题:

  1. 赋值器插入一条或多条从黑色对象到白色对象的新引用。
  2. 赋值器删除了全部灰色对象到该白色对象的直接或间接引用。

并发标记阶段要解决对象消失的问题,解决方式:

  1. 增量更新:破坏导致出现对象消失的条件1。当黑色对象插入新的指向白色对象的引用关系时,记录该引用关系,在并发标记结束后的重新标记阶段以该引用关系的黑色对象为根从新扫描一次。CMS收集器使用。
  2. 原始快照:破坏导致出现对象消失的条件2。当灰色对象删除指向白色对象的引用关系时,记录该引用关系,在并发标记结束后的重新标记阶段以该引用关系的灰色对象为根从新扫描一次。G1/Shenandoah收集器使用

垃圾收集器简介

HotSpot虚拟机的垃圾收集器:

Serial收集器

采用标记-复制算法。用于收集堆的新生代区域。

单线程工作的收集器,进行垃圾收集时必须暂停其他所有工作线程。

用途:运行在客户端模式的虚拟机,搭配老年代收集器CMS使用(JDK9开始取消支持该组合)。 ,()

Serial Old收集器

采用标记-整理算法。用于收集堆的老年代区域(Serial收集器的老年代版本)

单线程工作的收集器,进行垃圾收集时必须暂停其他所有工作线程。

用途:JDK 5之前与Parallel Scavenge收集器搭配使用;CMS收集器发生失败时的后备方案。

ParNew收集器

基于标记-复制算法。用于收集堆的新生代区域。

Serial收集器的多线程版本,进行垃圾收集时必须暂停其他所有工作线程。

用途:与CMS收集器搭配使用(JDK 9后只能和CMS搭配使用了)。

Parallel Scavenge收集器

基于标记-复制算法。用于收集堆的新生代区域。

并行的多线程收集器,进行垃圾收集时必须暂停其他所有工作线程。

关注吞吐量,支持自适应调整垃圾收集的停顿时间。

用途:与Serial Old搭配使用(JDK 6以前);与Parallel Old收集器搭配使用(JDK 6以后)。

Parallel Old收集器

基于标记-整理算法。用于收集堆的老年代区域(Parallel Scavenge收集器的老年代版本)

并行的多线程收集器,进行垃圾收集时必须暂停其他所有工作线程。

用途:与Parallel Scavenge收集器搭配(吞吐量优先的全套收集器)

CMS收集器

基于标记-清除算法。用于收集堆的老年代区域。

并发的多线程收集器,只有在初始标记和重新标记阶段需要暂停其他所有工作线程。

分为4个阶段:初始标记、并发标记、重新标记、并发清除。

以获取最短回收停顿时间为目标。

用途:与新生代收集器Serial或者ParNew收集器搭配使用。JDK 9以后CMS已被声明为Deprecate。

缺点:

  1. 对处理器的资源非常敏感。在并发标记和并发清除阶段会占用线程资源,导致应用程序变慢。
  2. 基于标记-清除算法会出现空间碎片,碎片过多导致给大对象的分配内存时找不到连续的内存空间,进而触发Full GC,同时进行空间碎片的整理(会带来停顿)。

G1收集器

面向服务端应用的收集器。开创了面向局部收集的设计思路和基于Region的内存布局形式。

将Java堆划分为多个大小相等的独立Region,每个Region可以根据需要扮演新生代的Eden、Survivor或者老年代空间。收集器可以对扮演不同角色的Region采用不同的策略处理。

G1垃圾收集的4个阶段:初始标记(Stop The World)、并发标记、重新标记(Stop The World)、筛选回收(Stop The World)。

筛选回收阶段负责更新Region的统计数据,对给Region的回收价值和成本进行排序,根据用户期望的停顿时间选择回收哪些Region,然后把回收Region的存活对象复制到空的Region中,在清理掉回收Region。这个阶段需要暂定其他所有用户线程

Region间存在跨Region引用的问题,因此每个Region都要维护一份记忆集。因此G1收集器比传统收集器有更大的内存消耗(至少要耗费整个堆内存的10%~20%)。

可以由用户指定期望的停顿时间是G1收集器很强大的一个功能,设置不同的期望停顿时间,可使得G1在不同应用场景中取得关注吞吐量和关注延迟之间的最佳平衡。

从G1开始,收集器都开始变为追求能够应付应用内存分配速率,而不追求一次把整个Java堆全部清理干净。

G1收集器整体上使用标记-整理算法,局部上(Region间)使用标记-复制算法。

用途:JDK 9开始取代Parallel Scavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器。