文章作用

  • 垃圾回收基础:什么是垃圾、如何判断是垃圾、如何回收
  • 可达性分析算法(根搜索算法)、GC 类型、垃圾收集类型
  • 垃圾回收算法
  • 垃圾收集器:串行收集器、并行收集器、新生代 ParallelScavenge 收集器、CMS、G1、ZGC

垃圾回收概述

什么是垃圾

简单说就是内存中已经不再被使用到的内存空间就是垃圾

如何判断是垃圾

引用计数法

给对象添加一个引用计数器,有访问就加 1,引用失效就减 1

  • 优点:实现简单、效率高
  • 缺点:不能解决对象之间循环引用的问题

可达性分析算法(根搜索算法)

从根(GC Roots)节点向下搜索对象节点,搜索走过的路径成为引用链,当一个对象到根之间没有连通的话,则该对象不可用

可以作为 GC Roots 的对象

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中 JNI 引用的对象

判断是垃圾的步骤

  • 通过可达性分析算法判断为不可达
  • 看是否有必要执行 finalize 方法,没有必要两种情况:
    • 要回收的对象没有覆盖 finalize 方法
    • 要回收的对象 finalize 已经被虚拟机调用过
  • 两个步骤走完后对象仍然没有人使用,那就属于垃圾

判断类无用的条件

刚刚上边说的是类的实例是如何被判定为垃圾的,现在说下类被卸载的条件

  • JVM 中该类的所有实例都已经被回收
  • 加载该类的 ClassLoader 已经被回收
  • 没有任何地方引用该类的 Class 对象
  • 无法在任何地方通过反射访问这个类

GC 类型

  • MinorGC/YoungGC:发生在新生代的动作
  • MajorGC/OldGC:发生在老年代的 GC,目前只有 CMS 收集器会有单独收集老年代的行为
  • MixedGC:收集整个新生代以及部分老年代,目前只有 G1 收集器会有这种行为
  • FullGC:收集整个 Java 堆和方法区的 GC

垃圾回收类型

  • 串行收集:GC 单线程内存回收、会暂停所有的用户线程,如:Serial
  • 并行收集:多个 GC 线程并发工作,此时用户线程是暂停的,如:Parallel
  • 并发收集:用户线程和 GC 线程同时执行(不一定是并行,可能交替执行),不需要停顿用户线程,如:CMS

垃圾回收算法

标记清除法(Mark-Sweep)

算法分为标记和清除两个阶段,先标记出要回收的对象,然后统一回收这些对象

  • 优点:简单
  • 缺点:
    • 效率不高,标记和清除的效率都不高
    • 标记清除后会产生大量不连续的内存碎片,从而导致在分配大对象时触发 GC

复制算法(Copying)

把内存分成两块完全相同的区域,每次使用其中一块,当一块使用完了,就把这块上还存活的对象拷贝到另外一块,然后把这块清除掉

  • 优点:实现简单,运行高效,不用考虑内存碎片问题
  • 缺点:内存浪费一半

JVM实际实现中,是将内存分为一块较大的 Eden 区和两块较小的 Survivor 空间,每次使用 Eden 和一块 Survivor,回收时,把存活的对象复制到另一块 Survivor

HotSpot 默认的 Eden 和 Survivor 比是 8:1,也就是每次能用 90% 的新生代空间

如果 Survivor 空间不够,就要依赖老年代进行分配担保,把放不下的对象直接进入老年代

分配担保

当新生代进行垃圾回收后,新生代的存活区放置不下,那么需要把这些对象放置到老年代去的策略,也就是老年代为新生代的 GC 做空间分配担保,步骤如下:

  1. 在发生 MinorGC 前,JVM 会检查老年代的最大可用的连续空间,是否大于新生代所有对象的总空间,如果大于,可以确保 MinorGC 是安全的
  2. 如果小于,那么 JVM 会检查是否设置了允许分配担保,如果设置了,则继续检查老年代最大可用的连续空间,是否大于历次晋升到老年代对象的平均大小
  3. 如果大于,则尝试进行一次 MinorGC
  4. 如果不大于,则改做一次 FullGC

标记整理法(Mark-Compact)

由于复制算法在存活对象比较多的时候,效率较低,且有空间浪费,因此老年代一般不会选用复制算法,老年代多选用标记整理算法

标记过程跟标记清除一样,但是后续不是直接清除可回收对象,而是让所有存活对象都向一端移动,然后直接清除边界以外的内存

垃圾收集器(HotSpot 中的收集器)

垃圾收集器是具体实现前面列举的那些算法并实现内存回收

垃圾回收基础(TODO) - 图1

串行收集器(Serial/Serial Old)

是一个单线程的收集器,在垃圾收集时,会暂停所有用户线程

垃圾回收基础(TODO) - 图2

  • 优点:简单,对于单 CPU,由于没有多线程的交互开销,可能更高效,是默认的 Client 模式下的新生代收集器
  • 使用:通过-XX:+UseSerialGC来开启,会使用 Serial+Serial Old 的收集器组合
  • 新生代使用复制算法,老年代使用标记-整理算法

开启使用的例子如下:

  1. // GC 测试代码
  2. fun main() {
  3. val list = mutableListOf<GCUnit>()
  4. try {
  5. while (true) {
  6. list.add(GCUnit())
  7. }
  8. } catch (e: Exception) {
  9. e.printStackTrace()
  10. }
  11. }
  12. class GCUnit {
  13. val byteArray: ByteArray = ByteArray(1024 * 1024)
  14. }

VM options

  1. -XX:+UseSerialGC -XX:+PrintGCDetails

执行 log 如下:

垃圾回收基础(TODO) - 图3

Serial 收集器新生代表示为DefNew( Default New Generation 缩写 ),老年代为Tenured

并行收集器

ParNew

只用在新生代,使用多线程进行垃圾回收,在垃圾收集时,会暂停所有用户线程

垃圾回收基础(TODO) - 图4

在并发能力好的 CPU 环境里,它停顿的时间要比串行收集器端;但是对于单 CPU 或并发能力较弱的 CPU,由于多线程的交互开销,可能比串行回收期更差

是 Server 模式下首选的新生代收集器,且能和(只能和) CMS 收集器配合使用

不在使用-XX:+UseParNewGC开启,改使用 CMS(-XX:+UseConcMarkSweepGC),CMS 新生代默认使用的就是 ParNew

例子:

运行代码同串行收集器一样,就不重复贴了

VM options:

  1. -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails

执行 log 如下:

垃圾回收基础(TODO) - 图5

Parallel Scavenge / Parallel Old

Parallel Scavenge 是一个应用于新生代的、使用复制算法的、并行的收集器,Parallel Old是它的老年代版本,不过使用的是标记清除算法

跟 ParNew 很类似,但是更关注吞吐量,能最高效率的利用 CPU,适合运行后台应用

使用-XX:+UseParallelGC(或者使用-XX:+UseParallelOldGC)来开启

例子:

运行代码同串行收集器一样,就不重复贴了

VM options:

  1. -XX:+UseParallelGC -XX:+PrintGCDetails

垃圾回收基础(TODO) - 图6

Parallel Scavenge 收集器(新生代)表示为PSYoungGen,Parallel Old(老年代)为ParOldGen

CMS收集器(Concurrent Mark and Sweep)

使用标记清除算法的多线程并发收集的垃圾收集器,分为以下几步

  1. 初始标记:只标记 GC Roos 能直接关联到的对象,会发生 STW(Stop-The-World)
  2. 并发标记:进行 GC Roots Tracing 的过程
  3. 重新标记:修正并发标记期间,因程序运行导致标记发生变化的那一部分对象,会发生 STW
  4. 并发清除:并发回收垃圾对象
  5. 重置线程:清空跟收集相关的数据并重置,为下一次收集做准备

垃圾回收基础(TODO) - 图7

  • 优点:低停顿(停顿的点只做标记处理,没有执行清理所以会快很多),并发执行
  • 缺点:
    • 并发执行,对 CPU 资源压力大
    • 无法处理在处理过程中产生的垃圾,可能导致 FullGC
    • 采用的标记清除算法会导致大量碎片,从而在分配大对象时可能触发 FullGC

使用-XX:UseConcMarkSweepGC来开启,使用的是 ParNew+CMS+Serial Old 的收集器组合,Serial Old 将作为 CMS 出错的后配收集器

例子与ParNew相同,就不重复了

G1 收集器(Garbage-First)

ZGC 收集器