Appearance
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) 或其分代变体,针对移动端特点:
- 使用 Moving GC:将存活对象从从 From 空间拷贝到 To 空间,彻底消除内存碎片。
- 读屏障 (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 停顿日志分析
复习检查题
为什么可达性分析算法中“不可达的对象”不一定会被立即回收?
答:不可达只表示对象已经不再从 GC Roots 可达,不等于内存会立刻归还。GC 还要等待合适的收集阶段、处理引用队列和整理等工作。历史上的 Java
finalize()还可能让对象在回收前“复活”,但 finalization 已被现代 Java 弃用,Android/ART 的具体行为也随版本变化,工程代码不应依赖它;应使用显式关闭、生命周期清理或Cleaner等明确机制。Android ART 的 CC (Concurrent Copying) 收集器是如何做到在并发拷贝对象时不卡顿主线程的?
答:在采用 Concurrent Copying 的 ART 路径中,读屏障 (Read Barrier) 会在应用线程读取对象引用时协助发现并修正正在迁移的对象引用,使 GC 线程和应用线程可以更多地并发运行、减少长暂停;具体行为和暂停时长仍取决于 Android/ART 版本、设备和本次 GC 压力。
速记
- 判定根基:GC Roots 可达性分析,彻底解决引用计数循环引用难题。
- 三色标记:白/灰/黑三色,读写屏障解决多标与漏标(SATB)。
- ART 优化:部分版本的并发拷贝/读屏障路径可减少碎片和长暂停,但具体暂停仍需结合版本、设备和 GC 压力观察。