1. 判断对象是否可被回收

1.1 引用计数法

通过对象的引用计数器来判断该对象是否被引用,因为会存在循环引用问题,所以 JVM 不使用

1.2 可达性分析算法

通过根对象(GC Roots)作为起始点进行搜索,走过的路径被称为引用链(Reference Chain),如果某个对象到根对象没有引用链相连时,就认为这个对象是不可达的,可以回收

Snipaste_20211222_224019.png

GC Roots 包含的对象

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

注意:一个对象即使不可达,也不一定会被回收

Snipaste_20211222_224539.png

2. 引用类型

2.1 强引用

如 Object obj = new Object() 的引用
只要强引用在,永远不会回收被引用的对象

2.2 软引用

如 SoftReference sr = new SoftReference<>("hello")
是用来描述一些有用但非必需的对象
软引用关联的对象,只有在内存不足的时候才会回收

2.3 弱引用

如 WeakReference sr = new WeakReference<>("hello")
弱引用也是用来描述非必需对象的
无论内存是否充足,都会回收被弱引用关联的对象

2.4 虚引用

如 ReferenceQueue queue = new ReferenceQueue<>();
PhantomReference pr = new PhantomReference<>(hello", queue);

不影响对象的生命周期,如果一个对象只有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收
虚引用主要用来跟踪対象被垃圾回收器回收的活动,必须和引用队列(ReferenceQueue)配合使用
当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内之前,把这个虚引用加入到与之关联的引用队列中
程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收
如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存回收之前采取必要的行动

3. 垃圾回收算法

3.1 标记-清除

  • 介绍:算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收
  • 优点:实现简单
  • 缺点:标记和清除两个过程的效率都不高,会产生碎片,碎片太多会导致提前 GC

Snipaste_20211114_223843.png

3.2 标记-整理

  • 介绍:标记过程仍然与标记清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
  • 优点:没有了内存碎片
  • 缺点:整理内存比较耗时

Snipaste_20211114_224222.png

3.3 复制

  • 介绍:它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉
  • 优点:实现简单,运行高效,无碎片
  • 缺点:空间利用率低,只能使用一半的空间

Snipaste_20211114_224633.png

3.4 分代收集算法

  • 介绍:根据对象的存活周期,把内存分成多个区域,不同区域使用不同的垃圾回收算法回收对象
  • 优点:更有效的清除不再使用的对象,提升了垃圾回收的效率
  • 调优原则
    • 合理设置 Survivor 区域的大小,避免内存浪费
    • 让 GC 尽量发生在新生代,尽量减少 Full GC 的发生

Snipaste_20211115_201904.png

4. 垃圾收集器

Snipaste_20211115_204634.png

4.1 术语介绍

  • STW(Stop The World):也叫全局停顿,发生 STW 时 Java 代码停止运行,native 代码继续运行,但不能与 JVM 进行交互原因,原因多半是由于垃圾回收导致,也可能由 Dump 线程、死锁检查、Dump 堆等导致,可能导致服务停止,没有响应;主从切换,危害生产环境
  • 并行收集:多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态,适合科学计算、后台处理等弱交互场景
  • 并发收集:用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),垃圾收集线程在执行的时候不会停顿用户程序的运行,适合对响应时间有要求的场景,比如 Web
  • 吞吐量:CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值,公式:运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
  • 停顿时间:垃圾收集器做垃圾回收中断应用执行的时间,-XX:MaxGCPauseMillis

4.2 新生代收集器

4.2.1 Serial

串行收集器,采用复制算法
适用场景:单核机器和客户端应用程序,应用以 -client 模式运行时,默认使用的就是 Serial
特点:

  • 单线程,简单高效
  • 收集过程全程 STW

Snipaste_20211222_225111.png

4.2.2 ParNew

Serial 收集器的多线程版本,除了使用多线程以外,其他和 Serial 收集器一样,包括:JVM 参数,STW 的表现和垃圾回收算法
适用场景:主要用来和 CMS 收集器配合使用
特点:

  • 多线程
  • 可使用 -XX:ParallelGCThreads 设置垃圾收集的线程数

Snipaste_20211222_225448.png

4.2.3 Parallel Scavenge

也叫吞吐量优先收集器,采用的也是复制算法,也是并行的多线程收集器
适用场景:注重吞吐量的场景
特点:

  • 可以达到一个可控制的吞吐量,使用 -XX:MaxGCPauseMillis 来尽力控制最大的垃圾收集停顿时间
  • 使用 -XX:GCTimeRatio 来设置吞吐量的大小,取值 0-100,系统花费不超过 1/(1+n) 的时间用于垃圾收集
  • 自适应 GC 策略:使用 -XX:+UseAdptiveSizePolicy 打开自适应策略后,无需手动设置新生代的大小(-Xmn)、Eden 与 Survivor 区的比例(-XX:SurvivorRatio)等参数,虚拟机会自动根据系统的运行状况收集性能监控信息,动态地调整这些参数,从而达到最优的停顿时间以及最高的吞吐量

Snipaste_20211222_225739.png

4.3 老年代收集器

4.3.1 Serial Old

Serial 收集器的老年代版本,采用标记-整理算法
适用场景:可以和 Serial/ParNew/Parallel Scavenge 这三个新生代的垃圾收集器配合使用,当使用 CMS 收集器出现故障的时候,会用 Servial Old 作为后备

4.3.2 Parallel Old

Parallel Scavenge 收集器的老年代版本,采用标记-整理算法
适用场景:关注吞吐量的场景

4.3.3 CMS

Snipaste_20211115_214609.png

适用场景:希望系统停顿时间短,响应速度快的场景,比如各种服务器应用程序

优点:

  • Stop The World 的时间比较短
  • 大多过程并发执行

缺点:

  • CPU资源比较敏感
    • 并发阶段可能导致应用吞吐量的降低
  • 无法处理浮动垃圾
  • 不能等到老年代几乎满了才开始收集
    • 预留的内存不够-> Concurrent Mode Failure -> Serial Old 作为后备
  • 可使用 CMSInitiatingOccupancyFraction 设置老年代占比达到多少就触发垃圾收集,默认 68%
  • 内存碎片
    • 标记-清除导致碎片的产生
    • UseCMSCompactAtFullCollection:在完成 Full GC 后是否要进行内存碎片整理,默认开启
    • CMSFulIGCsBeforeCompaction:进行几次 Full GC 后就进行一次内存碎片整理,默认 0
4.3.4 CMS 执行过程
  • 初始标记
    • 标记 GC Roots 能直接关联到的对象
    • Stop The World
  • 并发标记
    • 找出所有 GC Roots 能直接关联到的对象
    • 并发执行,没有 Stop The World
  • 并发预清理(略)
    • 重新标记那些在并发标记阶段,引用被更新的对象,从而减少后面重新标记阶段的工作量
    • 并发执行,没有 Stop The World
    • 可使用 -XX:-CMSPrecleaningEnabled 关闭并发预清理阶段,默认打开
  • 并发可中止的预清理阶段(略)
    • 和并发预清理做的事情一样,并发执行,无 Stop The World
    • 当 Eden 的使用量大于 CMSScheduleRemarkEdenSizeThreshold的阈值(默认 2M)时,才会执行该阶段
    • 主要作用:允许我们能够控制预清理阶段的结束时机,比如扫描多长时间(CMSMaxAbortablePrecleanTime 默认 5秒)或者 Eden 区使用占比达到一定阈值(CMSScheduleRemarkEdenPenetration 默认 50%)就结束本阶段
  • 重新标记
    • 修正并发标记期间,因为用户程序继续运行,导致标记发生变动的那些对象的标记
    • 一般来说,重新标记花费的时间会比初始标记阶段长一些,但比并发标记的时间短
  • 并发清除
    • 基于标记结果,清除掉要清除前面标记出来的垃圾
    • 并发执行,没有 Stop The World
  • 并发重置
    • 清理本次 CMS GC 的上下文信息,为下一次 GC 做准备

4.4 G1

Snipaste_20211115_222412.png

G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离,Region 的大小是一致的,数值是在 1M 到 32M 字节之间的一个 2 的幂值数,JVM 会尽量划分 2048 个左右、同等大小的 Region,通过参数 -XX:G1HeapRegionSize 指定 Region 的大小

通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收,这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能,通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region

G1 会将超过 Region 50% 大小的对象(在应用中,通常是 byte 或 char 数组)归类为 Humongous 对象,并放置在相应的 region 中。逻辑上,Humongous region 算是老年代的一部分,因为复制这样的大对象是很昂贵的操作,并不适合新生代 GC 的复制算法

每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region,通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描

适用场景

  • 占用内存较大的应用(6G 以上)
  • 替换 CMS 垃圾收集器

特点

  • 可以作用在整个堆
  • 可控的停顿,MaxGCPauseMillis=200
  • 无内存碎片
4.4.1 垃圾收集机制
  • Young GC
    所有 Eden Region 都满了的时候,就会触发 Young GC
    伊甸园里面的对象会转移到 Survivor Region 里面去
    原先 Survivor Region 中的对象转移到新的 Survivor Region 中,或者晋升到 Old Region
    空闲 Region 会被放入空闲列表中,等待下次被使用

  • Mixed GC
    老年代大小占整个堆的百分比达到一定阈值(可用 -XX:InitiatingHeapOccupancyPercent 指定,默认 45%),就触发
    Mixed GC 会回收所有 Young Region,同时回收部分 Old Region

  • Full GC
    复制对象内存不够,或者无法分配足够内存(比如巨型对象没有足够的连续分区分配)时,会触发 Full GC,Full GC 模式下,使用 Serial Old 模式

4.4.2 Mixed GC 执行过程
  • 初始标记
    • 标记 GC Roots 能直接关联到的对象
    • 和 CMS 类似,存在 Stop The World
  • 并发标记
    • 同 CMS 的并发标记
    • 并发执行,没有 Stop The World
  • 最终标记
    • 修正在并发标记期间引起的变动
    • 存在Stop The World
  • 筛选回收
    • 对各个 Region 的回收价值和成本进行排序
    • 根据用户所期望的停顿时间(MaxGCPauseMillis)来制定回收计划,并选择一些 Region 回收
    • 回收过程
      • 选择一系列 Region 构成一个回收集
      • 把决定回收的 Region 中的存活对象复制到空的Region中
      • 删除掉需回收的 Region -> 无内存碎片
      • 存在 Stop The World
4.4.3 如何减少 Full GC
  • 增加预留内存(增大 -XX:G1ReservePercent,默认为堆的 10%)
  • 更早地回收垃圾(减少 -XX:InitiatingHeapOccupancyPercent,老年代达到该值就触发 Mixed GC,默认 45%)
  • 增加并发阶段使用的线程数(增大 -XX:ConcGCThreads

4.5 是否需要切换到 G1

  • 50% 以上的堆被存活对象占用
  • 对象分配和晋升的速度变化非常大
  • 垃圾回收时间特别长,超过了 1 秒
  • 如果内存 <= 6G,建议用CMS,如果内存 > 6G,考虑使用 G1

4.6 如何选择垃圾收集器

  • 优先调整堆的大小让服务器自己来选择
  • 如果内存小于 100M,使用串行收集器
  • 如果是单核,并且没有停顿时间的要求,串行或者 JVM 自己选
  • 如果允许停顿时间超过 1 秒,选择并行或者 JVM 自己选
  • 如果响应时间最重要,并且不能超过 1 秒,使用并发收集器

Q.E.D.


盛年不重来,一日难再晨。