7. 垃圾回收理论
1. 研究方法
- 明确需要回收的区域在运行期内存结构模型中的位置
- 堆
- 明确何为垃圾(或垃圾有何特点)
- 明确如何定位垃圾(或如何标记垃圾)
- 明确如何回收垃圾(或垃圾回收算法)
- 明确垃圾回收的时机
- 垃圾回收需要考虑的问题
- GC 类型
2. 回收区域在运行期内存结构模型中的位置
- 栈管运行、堆管存储
- 线程私有区域 : 可以完全交由负责运行的线程来管理,线程运行结束,所占用的内存空间自然就应该被回收
- 线程共享区域 : 生命周期与 JVM 的生命周期一致,需要额外的收集器进行收集
- 方法区
- 永久区/元空间
- 基本不收集,回收条件苛刻,判断一个类是否可以被回收,需同时满足
- 该类的所有实例都已经被回收
- 加载该类的 ClassLoader 已经被回收
- 该类对应的 java.lang.Class 对象没有任何地方引用,无法在任何地方通过反射访问该类的方法
- 基本不收集,回收条件苛刻,判断一个类是否可以被回收,需同时满足
- 永久区/元空间
- Java 堆
- 新生代
- 频繁收集
- 老年代
- 较少收集
- 新生代
- 方法区
3. 垃圾对象定位
- 引用计数法
- 原理
- 给对象引用次数设置一个变量,每引用一次,就对这个变量+1,使用之后变量-1,变量为 0 即代表对象没有被引用,可以被回收,此对象属于垃圾对象
- 缺点
- 需要额外的空间来存储引用次数,增加了空间的开销
- 每次删减引用后,都需要操作这块空间,增加了时间的开销
- 无法处理循环引用的问题
- 优点
- 实现简单、回收效率高
- 原理
- 可达性分析(Hotspot 采用的方法)
- 基本原理
- 在某一时刻的快照里,以 GCRoot 作为起始点,向下收集,收集的路径称为引用链,引用链上的所有对象都是存活对象;不在引用链上的对象为不可达对象,不可达对象就是要回收的垃圾对象
- 可作为 GCRoot 的对象元素
- 虚拟机栈所引用的对象
- 本地方法栈所引用的对象
- 方法区静态变量引用的对象
- 方法区常量引用的对象
- 同步锁持有的对象
- 虚拟机的内部引用,如常驻的异常对象、class 对象、类加载器对象
- 虚拟机监控对象、本地代码缓存对象、JVMTI 中注册的回调等
- 临时性加入的对象
- 可作为 GCRoot 的对象元素
- 我们希望有这样一种情况:如果内存空间足够时,就把对象保留到内存中;如果内存空间不够时,就清理掉对象。
- 对引用分等级,视内存紧张程度来决定是否回收
- 强引用,StrongReference : 只要存在强引用,就不会被回收
- 软引用,SoftReference : 下次 GC 一定会回收软引用的对象
- 弱引用,WeakReference : 下次 GC 前,不管空间是否够用,一律回收
- 虚引用,PhantomReference : 虚引用的对象在回收时收到系统的一个通知
- 终结引用,FinalReference : 供 Finalizer 线程找到被引用对象,并调用其 finalization()方法,第二次 GC 时才能回收被引用对象
- 对引用分等级,视内存紧张程度来决定是否回收
- 在某一时刻的快照里,以 GCRoot 作为起始点,向下收集,收集的路径称为引用链,引用链上的所有对象都是存活对象;不在引用链上的对象为不可达对象,不可达对象就是要回收的垃圾对象
- 详细细节
- 对象的 finalization() 方法 : Java 语言提供的一种机制,为了让程序员控制一些类在销毁前做一些自定义操作
- 特性
- 可以被子类重写
- 程序员提供销毁前的自定义处理逻辑,实际调用过程则由 JVM 完成
- 垃圾回收器在销毁对象之前,总是会先调用一下对象的 finalization()方法
- 垃圾收集器调用 finalization() 方法可能会导致对象“复活”,极端情况下垃圾收集器不会调用 finalization() 方法
- 特性
- 两次标记过程
- 第一次标记:筛选没有被 GCRoot 引用的对象 : 看 GCRoot 引用链上是否有此对象
- 第二次标记:触发没有被 GCRoot 引用的对象的 finalization()方法
- finalization() 方法已经被调用过。如果此对象没有重写 finalization() 方法且 finalization() 方法已经被虚拟机调用过了,或者此对象重写了 finalization() 方法,也已经被虚拟机调用过了,则直接标记这个对象为不可触及对象
- finalization() 方法还没有被调用过。链接如果此对象没有被调用过 finalization() 方法,虚拟机就会创建一个 F-queue 的队列,并把这个对象放到这个队列里面,之后由一个低优先级的 Finalizer 线程依次触发队列里面对象的 finalization() 方法。如果 finalization() 方法执行完成之后,对象依然没有与引用链上的其他对象建立引用,就标记此对象为不可触及对象,并剔除对列里面与引用链上的某一个对象建立了引用关系的对象
- 判断一个对象能否被回收
- 对象的 finalization() 方法 : Java 语言提供的一种机制,为了让程序员控制一些类在销毁前做一些自定义操作
- 基本原理
- 统计出所有需要回收的对象【Java 堆上保存的就是 Java 对象,要回收当然就要统计出所有需要回收的对象】
- 枚举根节点
- 迄今为止,所有收集器在枚举根节点这一步骤都是必须暂停用户线程的(jvm 中在垃圾回收的时候单独开辟一个线程去收集垃圾的,在收集的过程中,用户线程在不断产生新的垃圾,因此需要暂停用户线程),如果根节点过多,那么收集就会耗费很长时间,也就是暂停时间过长,那么势必会影响整个 jvm 的吞吐量,因此一定会有一个地方存放着;
- HotSpot 的解决方案中,会使用一组称为 OopMap 的数据结构来达到这个目的,在类加载完成的时候,HotSpot 就会把对象内什么偏移量上是什么类型的数据计算出来,在即使编译过程中,也会在特定的位置记录下栈里和寄存器里哪些位置是引用
- 更新记忆集
- 所有涉及到部分区域收集行为的垃圾收集器,都会
面临着非收集区域指向收集区域的跨区域引用的问题
; - 额外开辟一块内存区域,然后创建一种数据结构,就记录非收集区域指向收集区域的指针或引用,这种数据结构就叫记忆集;每次垃圾回收时,只需要关注记忆集和根节点就行,免去了每次都要扫描全部区域的麻烦
- 简单来说,就是对收集区域进行划分,然后进行编号,之后就会形成一个数组,如果此区域包含了跨代引用或跨区域引用的对象,那么这个位置的值就设置为 1,这样每次进行回收的时候,碰到为 1 的位置就不进行回收了,这样形成的数组就是卡表;
- 卡表由很多中具体的实现方式,可以卡对象的精度,也可以卡内存块的精度,还可以卡字长的精度。所谓卡对象的精度,就是对对象进行编号,存在跨代引用就把这个编号的值置为 1,卡字长的精度,就是根据 jvm 的字长进行编号;
- 更新记忆集的时候也是会产生并发一致性的问题,即在垃圾回收需要读取记忆集信息的时候,会同时产生新的引用信息;HotSpot 是使用写屏障的技术手段来解决这个问题的;
- 使用 AOP 的思想,在对象赋值操作时,对卡表进行更新,即在对象赋值操作时建立切面,然后生成更新卡表的代码语句;
- 所有涉及到部分区域收集行为的垃圾收集器,都会
- 三色标记
- 收集线程所产生的开销会随着收集区域的扩大而扩大,这会造成 jvm 吞吐量的下降;
- 收集线程在进行第二次标记时,用户线程也在重新建立引用或删除引用,即第二次标记时,对象引用关系发生了改变,采用三色标记方式进行解决;
- 黑色:第一次标记时,被垃圾回收器访问过,且这个对象的所有引用都被扫描过,那么把这个对象标记为黑色;
- 灰色:第一次标记时,被垃圾回收器访问过,但是至少有一个引用还没有被扫描到,那么就把这个对象标记为灰色;
- 白色:第一次标记时,没有被垃圾回收器访问过,那么就把这个对象标记为白色;
- 第二次标记完成后,所有的灰色对象要么变成白色、要么变成灰色,之后垃圾回收器只需要回收白色对象,下次垃圾回收会再次忽略黑色对象,直接从白色对象进行第二次标记;
- 枚举根节点
4. 垃圾回收算法
4.1. 垃圾对象特点
- 绝大多数对象“朝生夕死”
- 熬过多次 GC 后的对象更难消亡
- 跨代引用相对于同代引用只占少数比例
基于上述三条实践原理,把对象分为年轻代、老年代。
- 划分
- 分区
- 如何划分区域
- 划分为等大小的小区域,化整为零
- 如何回收
- 部分回收
- 如何划分区域
- 分代(如何划分及各区域如何回收(或回收的算法))
- 新生代
- 老年代
- 方法区
- 混合
- 分区+分代
- 分区
- 清除垃圾
- 已有的理论成果——垃圾收集算法
- 标记整理: 让所有存活的对象向内存空间一端移动,然后清理掉边界以外的内存
- 原理
- 标记阶段: Collector 从引用根节点开始遍历,标记所有被引用的对象
- 清除阶段: 把可以被回收的对象的地址放置到一个空闲列表中,下次进行分配时,先判断垃圾空间是否够用,如果够用就存放
- 缺点
- 效率不高
- 清理出来的空闲空间不是连续的,产生内存碎片
- 需要额外空间来保存被清理的对象的地址
- 使用场景
- 老年代对象收集过程
- 原理
- 标记清除
- 原理
- 标记阶段: Collector 从引用根节点开始遍历,标记所有被引用的对象
- 清除阶段:清除掉没有被引用的对象
- 压缩阶段:把剩余没有被清理的对象压缩到内存的一端
- 优缺点
- 解决了内存碎片问题
- 但是如果对象被其他对象引用,还需要调整引用的地址
- 使用场景
- 老年代对象收集过程
- 腾出来的空间再次分配时,可以使用“指针碰撞”的方式进行分配
- 原理
- 复制算法
- 原理
- 准备两块相同大小的内存空间,把应该保留的对象都复制到另外一块内存空间上,然后再把原来的内存空间全部清空即可
- 优缺点
- 没有标记和清除阶段,运行效率较高
- 不会出现内存碎片问题
- 但是需要两倍内存空间
- 对于分区域收集的垃圾回收器,需要维护跨区域引用的关系,会产生额外的性能开销
- 使用场景
- 适合垃圾对象较多,存活对象较少的场景。因此,是现在大多数商用虚拟机新生代所采用的收集算法
- 原理
- 增量收集算法
- 原理
- 通过妥善处理“垃圾清理线程”与“用户工作线程”的切换,让垃圾清理线程每次只清理一小块内存区域。
- 优缺点
- 减少每次垃圾回收的停顿时间
- 需要妥善处理好“垃圾清理线程”与“用户工作线程”的切换问题,并且线程切换会带来损耗,造成系统吞吐量下降
- 原理
- 分区收集算法
- 原理
- 把整块内存区域“化整为零”,每个小块空间独立使用,独立回收
- 优缺点
- 可以控制每次回收多少个小区间,达到停顿时间可控的目的
- 原理
- 都会产生 STW
- GC 发生过程中,会导致所有用户线程都暂停,像是整个世界都停止了一样
- 特性
- 所有的 GC 回收器都会产生
- 是由 JVM 主动发起的
- 调优的主要目的就是降低 STW 的停顿时间
- 原因 : 收集垃圾时需要在一个一致性快照中进行,不能一边清理垃圾一边产生垃圾
- 停顿的时间点
- 安全点 : 在某一时刻,所有的用户线程都会停下来,让 jvm 标识垃圾,这个时刻就是安全点。
- 所有的用户线程主动轮询一个 GC 标志位并采用主动中断的方式来挂起自身线程
- 安全区 : 如果单单是安全点,那么所有的线程都会在这个点进行主动中断,这就会出现有些线程等待时间过长的问题,因此可以延长这个“时刻”,把“时刻”变成“时间段”,这个时间段就是安全区,在这个安全区内,代码引用关系不会发生改变,换言之在这个区域内任何地方开始 GC 操作,都是等价的。
- 安全点 : 在某一时刻,所有的用户线程都会停下来,让 jvm 标识垃圾,这个时刻就是安全点。
- 标记整理: 让所有存活的对象向内存空间一端移动,然后清理掉边界以外的内存
- 最佳实践——分代收集算法
- 新生代
- 特点
- 区域小、对象生命周期短、存活率低、回收频繁
- 最佳实践
- 复制算法
- 特点
- 老年代
- 特点
- 区域大、对象生命周期长、存活率高、回收不是很频繁
- 最佳实践
- “标记清除”与“标记整理”混合使用
- 特点
- 新生代
- 已有的理论成果——垃圾收集算法
5. 垃圾回收的时机
- 不能一边打扫房间一边丢垃圾,必须在某一时刻停止制造垃圾的用户线程。所以这个 【停止用户工作线程的时刻】 和 【用户工作线程的停止时间间隔】 就比较重要,不可能随时随地停止,也不可能停止无限长的时间。
- 用户工作线程停止的时刻
- 选择标准
- 是否具有让程序长时间执行的特征
- 分类
- 安全点: 虚拟机会选取一些能够长时间执行的指令作为安全点,并在安全点处采用主动中断的方式挂起所有用户线程(主动中断就是用户线程主动去轮询一个 GC 标志位,如果标志位为真,表示要进行 GC 回收,此时用户线程就会主动中断)
- 安全区域
- 只有安全点的概念,会使主动中断等待的时间过长(GC 线程等待所有用户线程都到达安全点才主动中断),试想在一个区域内才更加合理,这个区间就是安全区域
- 停顿之后主线程的工作方式
- 选择标准
- 用户工作线程停止的方式
- 主动中断 : 主动中断就是用户线程主动去轮询一个 GC 标志位,如果标志位为真,表示要进行 GC 回收,此时用户线程就会主动中断
- 被动阻塞 : 被垃圾回收线程阻塞;
- 用户工作线程停止的时长
- 全局停顿时间
- 在新生代进行的 GC 叫做 minor GC,在老年代进行的 GC 都叫 major GC,Full GC 同时作用于新生代和老年代。在垃圾回收过程中经常涉及到对对象的挪动(比如上文提到的对象在 Survivor 0 和 Survivor 1 之间的复制),进而导致需要对对象引用进行更新。为了保证引用更新的正确性,Java 将暂停所有其他的线程,这种情况被称为“Stop-The-World”,导致系统全局停顿。Stop-The-World 对系统性能存在影响,因此垃圾回收的一个原则是尽量减少“Stop-The-World”的时间
- 全局停顿时间
6. 垃圾回收需要考虑的问题
- 要回收垃圾,就要暂停 JVM 中所有应用程序,理论上回收的区域越大、暂停的时间也就越多,用户线程等待时间也就越长;用户线程等待的时间越长,意味着相同时间内的吞吐量就越小;
- 性能指标
- 吞吐量 : 系统运行时间占总时间的比例
- 停顿时间
- 二者是相互竞争的
- 调优的目标 : 在最大吞吐量优先的前提下,尽量缩短停顿时间
- 收集频率
- 内存占用
- 垃圾收集的开销
- 服务器性能指标
- JVM 性能指标
- 应用性能指标
7. GC 类型
- 按照【部分回收 or 全部回收】分为
- PartitionGC
- 对整个堆空间的部分回收
- FullGC
- 对整个堆空间进行整体回收,包括 Java 堆和方法区
- PartitionGC
- 按照【针对老年代进行回收 or 针对新生代进行回收】分为
- MinorGC(有时也称 YoungGC)
- 对 Eden 区、S0 区、S1 区进行回收,回收过程会产生 STW
- MajorGC
- 只对 Tenured 区(养老区,也称老年代区)进行回收,回收过程也会产生 STW,但是是新生代耗时的好几倍
- MinorGC(有时也称 YoungGC)
- 事实上,我们对 JVM 调优,根本目的在于如何减少 MajorGC 是造成的 STW 的时间
7.0.1. Minor GC、Major GC、Full GC
JVM 在进行 GC 时,并非每次都对堆内存(新生代、老年代;方法区)区域一起回收的,大部分时候回收的都是指新生代。 针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大类:部分收集(Partial GC),整堆收集(Full GC)
- 部分收集:不是完整收集整个 Java 堆的垃圾收集。其中又分为:
- 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集
- 老年代收集(Major GC/Old GC):只是老年代的垃圾收集
- 目前,只有 CMS GC 会有单独收集老年代的行为
- 很多时候 Major GC 会和 Full GC 混合使用,需要具体分辨是老年代回收还是整堆回收
- 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集
- 目前只有 G1 GC 会有这种行为
- 整堆收集(Full GC):收集整个 Java 堆和方法区的垃圾
8. 垃圾回收器分类
- 按照线程工作模式
- 串行垃圾回收器
- 并行垃圾回收器
- 按照碎片处理方式
- 压缩式垃圾回收器
- 非压缩式垃圾回收器
- 按照工作的内存区间
- 新生代垃圾回收器
- 老年代垃圾回收器