Skip to content

JVM / ART 垃圾回收算法与 GC 演进深度剖析 ​

回到总览:内存、生命周期、系统约束
相关模块:JVM / ART / Dart / Flutter 内存模型总览 · Android / Java 内存泄漏定位、LeakCanary 原理与 Profiler 实战

一句话定义 ​

JVM 与 Android ART 虚拟机的垃圾回收(GC)机制都是从 GC Roots 出发,通过可达性分析判断对象是否仍然存活;具体的分代策略、移动/非移动收集器、屏障和 STW 时机则取决于 JVM/ART 版本、设备和运行时配置。本页重点放在 Android 工程判断,CMS、G1、ZGC 只作必要对照。

代码索引 ​

计划补齐的实验:

  • labs/java/runtime-concurrency/gc-log-analysis-demo/ — GC 瓶颈模拟与并发 GC 停顿日志分析

为什么需要 ​

  • 为什么“引用计数算法”无法作为 JVM 和 ART 堆内存回收的核心算法?
    • 一句话答:引用计数无法解决对象之间的循环引用回收问题(A 引用 B、B 引用 A,但外部已经没有入口);JVM 和 ART 主要以 GC Roots 为起点做可达性分析判断对象是否仍然存活。(详见下文 §1及复习题 1)
  • 为什么 10 年 Android 工程师理解 JVM / ART GC 演进对性能优化至关重要?
    • 一句话答:Android 早期运行时和高分配压力场景可能出现较长暂停并造成丢帧;理解 ART 收集器、版本差异和大对象/Bitmap 分配路径,才能把 GC 日志、堆压力和 UI 卡顿联系起来。(详见下文 §2、§3及复习题 2)

底层机制 ​

1. 可达性分析与 GC Roots ​

GC 判定对象存活依据:从 GC Roots 对象作为起始点,沿引用链向下搜索(Tracing)。无法从 GC Roots 到达的对象判定为可回收。

text
[GC Roots]
   ├── 线程栈帧中的局部变量 (Local Variables)
   ├── 类静态变量 (Static Variables)
   ├── JNI 全局/局部引用 (JNI References)
   └── 系统类加载器 (System ClassLoader)
         |
         v
     [存活对象 A] ----> [存活对象 B]
     
     [不可达孤岛对象 C] <---> [不可达孤岛对象 D]  (两者循环引用,但从 Roots 无法到达 -> 释放!)

2. 经典三色标记法 (Three-Color Marking) 与 STW 解决 ​

现代并发 GC(如 G1、ZGC、ART CC)使用三色标记算法实现 GC 线程与应用线程并发运行:

  • 白色:未被 GC 访问过的对象(初始状态,标记结束时白色即为垃圾对象)。
  • 灰色:已经被 GC 访问过,但该对象引用的其他对象尚未全部标记。
  • 黑色:已经访问过,且该对象引用的所有其他对象均已完成标记(黑色不可再指向白色)。

多标与漏标(SATB 与 Incremental Update) ​

当应用线程在 GC 标记期间修改了引用,可能发生“漏标”(将存活对象误认为垃圾而误杀):

  • SATB (Snapshot-At-The-Beginning):G1 等采用 SATB 思路的收集器会在引用被破坏时记录旧引用快照,保证本轮快照中仍存活的对象不会被漏标;不能把这条机制无条件等同于所有 ART 收集器。
  • 增量更新 (Incremental Update):CMS 等收集器可在新增黑色指向白色引用时记录跨代/跨区域关系,具体屏障实现由收集器决定。

3. JVM GC 算法与 Android ART GC 对照演进 ​

text
JVM 时代:   CMS (Mark-Sweep) ----> G1 (Region/Pause Target) ----> ZGC (Low Latency)
Android:   Dalvik/早期 ART ----> ART 的并发/分代收集器演进(具体设备实现需查版本)

Android ART 的 Concurrent Copying (CC) 收集器 ​

部分 Android ART 版本和运行时配置会使用 CC (Concurrent Copying) 或其分代变体,针对移动端特点:

  1. 使用 Moving GC:将存活对象从从 From 空间拷贝到 To 空间,彻底消除内存碎片。
  2. 读屏障 (Read Barrier):在支持读屏障的并发拷贝路径中,应用线程读取对象引用时可以协助修正对象地址,减少长时间暂停的需要;实际暂停时间仍取决于堆大小、分配压力、设备和收集阶段,不能承诺固定为微秒级。

Android GC Roots 与进程内存边界 ​

ART 的可达性分析主要处理托管对象。常见 GC Root 包括:

  • 活线程栈中的局部引用;
  • 静态字段和类加载器相关引用;
  • JNI 全局引用、系统或框架保留的引用;
  • ART/运行时自身维护的根集合。
text
GC Root: AppSingleton
      │
      ├── Activity
      │     └── ViewTree ──> Bitmap / Adapter / Context
      │
      └── Repository ──> Callback / Handler / Coroutine

如果 AppSingleton 仍然可达,下面的 Activity 和 ViewTree 就可能继续存活。反过来,Native heap、GPU 纹理和部分图形缓冲并不是简单地“等 Java GC 扫完就释放”,需要结合 Native/Engine/Graphics 生命周期判断。Android 图像内存的具体统计和采样见 ART / Bitmap 内存专题。

Android / Flutter / Web / Backend 对照 ​

维JVM服务端 (G1 / ZGC)Android (ART 收集器随版本/配置变化)Dart VM(常见分代收集路径)
内存堆模型服务器常见为较大堆,具体取决于 JVM 配置受设备、系统版本、应用配置和 largeHeap 等因素影响Isolate 独立堆(普通 Dart 对象不跨 isolate 共享)
GC 核心目标高吞吐量或低延迟尽量不丢帧、控制 STW 与碎片针对短命 Dart 对象优化
分代收集传统/Region 动态分代移动端新生代 (Sticky-bit) 分代新生代 (Young Space) 两空间复制
STW 敏感度中等(取决于服务延迟目标)较高:如果暂停发生在负责当前帧的 UI 线程或关键渲染路径,超过一帧预算就可能丢帧高(在 UI isolate 上的暂停或高分配压力可能影响帧)

常见场景 ​

1. 大对象直接进入老年代 / Large Object Space ​

在部分 Android ART 运行时中,大对象可能进入独立的 Large Object Space (LOS) 或采用特殊的大对象分配路径。LOS 属于 ART 管理的托管堆范围,不应直接等同于 Native heap;Bitmap、Native buffer 和 GPU 纹理仍要按各自的内存归属单独排查。

2. 避免在 onDraw 或高频 Loop 中分配内存 ​

java
// 错误案例:每秒触发 60 次,瞬间产生大量年轻代垃圾
@Override
protected void onDraw(Canvas canvas) {
    Rect rect = new Rect(); // 高频分配触发 Young GC / Sticky CC!
}

常见误配、事故后果与排障 ​

1. 事故:分配压力触发同步回收并造成卡顿 ​

  • 排障手段:结合 Logcat、Android Studio Memory Profiler 和帧时间线观察 GC 原因、暂停时长、分配速率与堆变化。GC_FOR_ALLOC、Explicit concurrent mark sweep 等名称主要见于旧版 Dalvik / ART 日志,不能作为所有 Android 版本的固定日志格式。
  • 原因:当应用请求内存但当前托管堆空间不足时,运行时可能触发更同步或更昂贵的回收路径;如果暂停发生在主线程,可能阻塞帧处理。
  • 排障与修法:使用 Glide / Fresco 进行图片降采样与 Bitmap 内存复用 (inBitmap);及时释放不再使用的 Activity/Context。Compose 侧可改用 Landscapist 这类统一封装,并借 Compose 约束驱动采样。

与相近概念对比 ​

垃圾回收器核心算法内存碎片STW 停顿时间适用的场景
CMS标记-清除 (Mark-Sweep)有 (需 Periodical Compact)较低 (两次短 STW)早期 Java Web 服务
G1标记-整理 (Region-based)无 (Region 整理)可预期 (可设 MaxGCPauseMillis)中大堆 Java 服务端
ZGC染色指针 + 读屏障无目标是低暂停,但不应视为固定保证大堆低延迟服务
ART CC读屏障辅助的并发拷贝路径通常减少碎片取决于版本、堆压力和设备Android 移动端设备

对应实验 ​

计划补齐的实验:

  • labs/java/runtime-concurrency/gc-log-analysis-demo/ — GC 瓶颈模拟与并发 GC 停顿日志分析

复习检查题 ​

  1. 为什么可达性分析算法中“不可达的对象”不一定会被立即回收?

    答:不可达只表示对象已经不再从 GC Roots 可达,不等于内存会立刻归还。GC 还要等待合适的收集阶段、处理引用队列和整理等工作。历史上的 Java finalize() 还可能让对象在回收前“复活”,但 finalization 已被现代 Java 弃用,Android/ART 的具体行为也随版本变化,工程代码不应依赖它;应使用显式关闭、生命周期清理或 Cleaner 等明确机制。

  2. Android ART 的 CC (Concurrent Copying) 收集器是如何做到在并发拷贝对象时不卡顿主线程的?

    答:在采用 Concurrent Copying 的 ART 路径中,读屏障 (Read Barrier) 会在应用线程读取对象引用时协助发现并修正正在迁移的对象引用,使 GC 线程和应用线程可以更多地并发运行、减少长暂停;具体行为和暂停时长仍取决于 Android/ART 版本、设备和本次 GC 压力。

速记 ​

  • 判定根基:GC Roots 可达性分析,彻底解决引用计数循环引用难题。
  • 三色标记:白/灰/黑三色,读写屏障解决多标与漏标(SATB)。
  • ART 优化:部分版本的并发拷贝/读屏障路径可减少碎片和长暂停,但具体暂停仍需结合版本、设备和 GC 压力观察。

站点构建时间:2026/8/24 23:43:17