Java GC 基础

80 阅读5分钟

引用计数

基本原理

为每个对象维护一个引用计数器,当创建一个对象的实例并在堆上申请内存时, 对象的引用计数器初始化为1,当一个引用指向该对象时,引用计数加1;当一个引用不在执行该对象时,引用计数减1。当引用计数为0时,说明没有任何引用指向该对象。此时该对象可被垃圾回收器回收。

当计数器为0是,对象内存会被立即释放么? 不一定,1. 频繁的内存操作会影响内存性能。2. 多线程安全

优点

  1. 尽快的回收不被使用的对象「嵌入式系统或小型脚本语言,立刻释放内存」
  2. 回收过程中,不会导致长时间停顿
  3. 清晰标明每个对象的生命周期

缺点

  1. 原始引用计数无法解决循环依赖问题「两个或者多个对象相互引用」
  2. 频繁更新计数器,开销显著增加
  3. 无法统一进行对象压缩,整理碎片,内存脆皮更难控制

Tracing GC

基本原理

通过可达性分析,判断哪些对象依然被程序使用,不可达对象识别为垃圾

可达性分析

可达性分析算法不仅能解决引用计数算法中循环引用导致的内存泄漏问题,还具备实现简单和执行高效的特点。

  • 可达性分析算法是以根对象集合(GCRoots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
  • 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)。

三色标记算法

  • 白色对象:初始状态下的所有对象,表示尚未被访问。回收完成后仍为白色的对象将被视为垃圾并被回收。
  • 灰色对象:已被访问但其引用的对象尚未全部处理的对象。灰色对象需要进一步扫描其引用的对象。
  • 黑色对象:已被访问且其引用的对象也已处理完毕的对象。黑色对象被认为是存活的,不会被回收。

四个阶段

  • 初始标记:标记GCRoots直接引用的节点,将他们标记为灰色,需要「STW」
  • 并发标记:从灰色节点开始,去扫描整个引用链,将其标记为灰色
  • 再标记:校准并发标记阶段的错误,需要「STW」
  • 并发清除:将已经确定为垃圾的对象清除

优缺点

  • 优点
    • 自动管理内存,避免手动释放内存引起的错误
    • 高效处理对象之间的引用关系
  • 缺点
    • 引入的STW

常见的Tracing GC 算法

标记-清除(Mark-Sweep)

  • 标记阶段:标记所有可达对象。
  • 清除阶段:遍历内存,将未被标记的对象释放掉。 优点:实现简单。
    缺点:容易产生碎片,回收后内存空间不连续。

标记-压缩(Mark-Compact)

  • 和标记-清除类似,但在清除后会把存活对象压缩到一起,避免碎片化。 优点:解决内存碎片问题。
    缺点:压缩过程耗时较多。

复制算法(Copying GC)

  • 将内存分为两个区(From、To),一次只在一个区中分配对象。
  • 回收时,将所有存活对象复制到另一个区,其余直接清除。 优点:避免碎片,回收速度快。
    缺点:内存浪费(只用了一半)。

分代收集(Generational GC)

  • 根据对象“生命周期长短”将内存分为年轻代(Young Generation)和老年代(Old Generation)。
  • 年轻代使用复制算法;老年代用标记-压缩。 优点:优化性能,大部分对象是“朝生暮死”的,回收代价小。

基础知识

  • Stop The Word 停止所有的工作线程,不创建新的对象

STW不创建新对象的目的是什么?

  1. 确保堆的稳定性,防止出现对象被扫描后,马上被创建,GC无法确定其引用关系。防止出现漏标或误标
  2. 提升 GC 执行效率 STW 的设计初衷是为 GC 提供一个稳定的环境:
  • 没有新对象创建可以让 GC 更快地完成标记、清理或复制;
  • 避免在 GC 运行期间管理并发的分配请求,降低复杂度和 CPU 压力。
  1. 简化算法的实现
  • Safe Point 一个线程可以安全停留在这里的代码点。 Safe Point 理解成代码执行过程中的一些特殊位置,当线程执行到这个位置时,线程可以暂停。 Safe Point 处保存了其他位置没有的一些当前线程信息,可以提供给其他线程读取,这些信息包括:线程上下文信息,对象的内部指针等。 而 Stop the World 就是所有线程同时进入 Safe Point 并停留在那里,等待 JVM 进行内存分析扫描,接着进行内存垃圾回收的时间。
  • 通常的GC Roots
    • 虚拟机栈中的引用 每一个线程在执行方法是,会在虚拟机中创建一个栈,栈中的局部变量(如方法的参数和局部变量)可能引用对象。只要方法还未执行完,这些引用的对象被视为存活。
    • 方法区中类静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈中 JNI(Java Native Interface)引用的对象
    • 活跃线程
    • 系统类加载器加载的类
    • 同步锁持有的对象
    • JVM 内部的特殊引用:
      例如,一些常驻的异常对象(如 NullPointerException、OutOfMemoryError)和基本数据类型对应的 Class 对象等
  • 并行与并发
    • 并行(Parallel): 多个 GC 线程同时工作以提高吞吐量
    • 并发(Concurrent): GC 线程与应用线程同时运行以减少停顿时间(Pause Time)