JVM垃圾回收机制

如何判断一个对象可以被回收

  1. 引用计数法
    每个对象维护一个引用计数器
    有一处引用指向该对象,引用计数器+1
    引用失效,则引用计数器-1
    引用计数器为0,判断无任何引用,可以回收

  2. 可达性分析法(jvm的实际使用)

  3. 选定一系列起点gc roots

  4. 从gc roots遍历所有的引用链路,能遍历到的对象全部存活

  5. 没有走到的对象,没有任何引用,可以被回收

引用计数法的缺陷

  1. 循环引用(致命问题)
    两个/多个对象互相引用,没有外部引用指向它们,导致内存泄漏,拥有不会被回收
  2. 额外开销大
    对象创建、赋值、消耗都需要修改计数器,高并发场景下有大量原子操作损耗性能

常见gc roots清单

  1. 虚拟机栈(栈帧本地变量表)
    线程每调用一个方法就创建一块栈帧,栈帧的本地变量表里存着引用变量,从这些变量往下找对象,能找到,就认为存活
  2. 本地方法栈
    专门给native本地方法服务,C/C++通过JNI持有java对象引用,C代码拿着java对象的指针,这个指针存在本地方法栈,gc扫描本地方法栈,顺着C的引用找到java对象,没有的话判定为不可达
  3. JDK8 元空间(原方法区)的静态变量、常量引用
    静态变量属于类,类存在元空间,生命周期和类一致,只要类没被卸载,静态变量永远是 GC Roots。静态变量属于类,类存在元空间,生命周期和类一致,只要类没被卸载,静态变量永远是 GC Roots。
    运行时常量池里的引用同理:常量引用指向堆内对象,这条链路让对象可达。

为什么大部分JVM都采用分代收集?

分代收集的根本原因:

  1. 绝大多数对象朝生夕灭
  2. 熬过多次 GC 的老对象,很难再被回收

Minor GC / Mixed GC / Full GC 触发条件

  1. Minor GC
    Eden区内存耗尽,只回收新生代
  2. Mixed Gc
    仅限G1
    老年代占用率达到 IHOP 默认 45%,并发标记结束后触发;回收全部新生代 + 收益最高的部分老年代分区。
  3. Full GC(全堆 + 元空间回收)
    老年代空间不足,放不下晋升对象;
    元空间触及最大限制
    主动调用 System.gc
    ......

从 Serial → Parallel → CMS → G1 → ZGC,每一代的演进解决了什么问题

Serial

  1. 架构
    单线程gc,全程STW,新生代复制、老年代标记整理
  2. 解决
    最基础的垃圾回收,适配早年单核小内存机器
  3. 缺点
    多核cpu完全闲置,堆越大STW停顿越长,无法用于线上服务

Parallel

  1. 改进
    多线程并行gc,多核cpu所有gc线程一起干活
  2. 解决
    利用多核减少stw的耗时,主打高吞吐量,降低gc耗时
  3. 定位
    吞吐量优先
  4. 遗留问题
    依旧全局stw,业务全程卡死,大堆停顿依然很高,无法做低延迟业务

CMS

  1. 核心
    gc大部分阶段核用户线程并发执行,不再全程卡死,追求低停顿
  2. 解决Parallel 最大痛点
    解决长时间stw,只有初始标记、重新标记两段短暂暂停
  3. 底层
    增量更新解决三色标记漏标
  4. 遗留问题
    4.1. 标记清除算法,产生大量内存碎片;分配大对象容易 Full GC;
    4.2. 并发模式失败:并发期间老年代被占满,直接退化为单线程 Full GC;
    4.3. 无法管控停顿时长,堆越大风险越高。

CMS垃圾回收流程

  1. 初始标记
    标记核gc root直接关联的对象,有stw
  2. 并发标记
    gc线程和业务线程同时运行
  3. 重新标记
    在并发标记中业务线程可以修改了一些对象的引用关系,因此重新标记。
    是通过增量更新,即,在黑色对象指向白色对象的情况,将黑色对象标记为灰色对象
  4. 并发清除
    gc回收所有的白色对象,业务线程正常跑

G1(Garbage-First)

解决:CMS三大痛点

  1. 内存碎片不可控
  2. 停顿不可预测
  3. 老年代回收一刀切

底层内存布局
将java堆切分成上千个等大的Region,逻辑区分新生代、老年代,无物理连续分区

核心优势

  1. 可预测停顿,可设置-XX:MaxGCPauseMillis,GC 优先回收垃圾占比最高的 Region,把停顿卡死在预期范围
  2. 解决碎片:回收 Region 时使用标记复制 / 整理,回收完该 Region 整块空闲,无内存碎片
  3. 支持 Mixed GC,不用整堆回收老年代。

G1 缺点
超大堆(32G+)场景,STW 依旧会拉长;并发整理耗时高,极致低延迟不如 ZGC。

gc过程

  1. 初始标记
    标记所有 GC Roots 直接引用的对象

  2. 根区扫描(STW 结束,用户线程运行)
    顺着 Root Region,遍历老年代指向新生代的引用。

  3. 并发标记(GC 线程与业务线程并发执行,无 STW)
    GC 顺着引用链遍历全堆对象

  4. 最终重新标记(STW)
    修正并发标记期间引用变动产生的标记误差;SATB 把所有删除的旧引用全部补标记,保证快照内存活对象全部标记到位。

  5. 筛选回收(可带轻微 STW)