目录
  1. 1. 一、GC 要解决什么问题
  2. 2. 二、可达性分析:谁是垃圾?
    1. 2.1. 2.1 引用计数法(为什么 JVM 不用)
    2. 2.2. 2.2 根搜索算法(GC Roots Tracing)
    3. 2.3. 2.3 引用类型与 GC 行为
  3. 3. 三、垃圾回收算法
    1. 3.1. 3.1 标记-清除(Mark-Sweep)
    2. 3.2. 3.2 标记-整理(Mark-Compact)
    3. 3.3. 3.3 复制算法(Copying Collection)
    4. 3.4. 3.4 分代回收(Generational Collection)
  4. 4. 四、ART 中的 GC 实现
    1. 4.1. 4.1 ART GC 的类型
    2. 4.2. 4.2 CMS(Concurrent Mark-Sweep)—— ART 早期默认 GC
    3. 4.3. 4.3 CC(Concurrent Copying)—— Android 10 引入
    4. 4.4. 4.4 ART GC 的堆结构
    5. 4.5. 4.5 GC Pause(暂停)与 Android 性能
  5. 5. 五、GC 根与内存泄漏
    1. 5.1. 5.1 常见内存泄漏模式
    2. 5.2. 5.2 使用 Memory Profiler 检测泄漏
    3. 5.3. 5.3 LeakCanary 原理
  6. 6. 六、避免 GC 性能问题的编程实践
    1. 6.1. 6.1 减少对象分配
    2. 6.2. 6.2 使用 Object Pool
    3. 6.3. 6.3 使用合适的集合类
  7. 7. 七、源码路径汇总
  8. 8. 八、常见面试题
Java进阶之深入理解GC机制

一、GC 要解决什么问题

在 C/C++ 中,开发者需要手动管理内存:malloc 分配、free 释放。一旦忘记释放或释放时机错误,就会导致内存泄漏或悬垂指针(dangling pointer)——这是 C/C++ 程序中最难以排查的一类 bug。Java 通过 GC(Garbage Collection,垃圾回收)从根本上解决了这个问题:开发者只管分配,JVM 负责回收

但”自动”并不意味着”免费”。GC 本身需要消耗 CPU 资源,其运行期间往往会暂停应用线程(Stop-The-World,简称 STW),导致应用出现卡顿。理解 GC 的工作原理,可以帮助我们编写 GC 友好的代码,减少 GC 频率和停顿时间。

本章从 JVM GC 的经典理论出发,深入 ART 的实现细节,系统性地剖析 GC 的方方面面。

二、可达性分析:谁是垃圾?

2.1 引用计数法(为什么 JVM 不用)

引用计数法的思想很简单:每个对象维护一个计数器,记录有多少个引用指向它。计数器为 0 时,对象可以被回收。Python 的 CPython 实现就使用这种方式。

// 引用计数的伪代码
class RefCounted {
int refCount = 0;
void addRef() { refCount++; }
void release() {
if (--refCount == 0) delete this;
}
};

引用计数法有两个致命缺陷,导致 JVM/ART 不采用它:

  1. 循环引用:A 引用 B,B 引用 A,两者计数器永远不为 0。虽然 Python 有辅助的标记-清除来处理循环引用,但 JVM 设计者选择了更彻底的方案。
  2. 性能开销:每次引用赋值都需要更新计数器。在多线程环境下,还需要原子操作(CAS),性能代价大。

2.2 根搜索算法(GC Roots Tracing)

JVM/ART 使用可达性分析(Reachability Analysis):从一组称为 GC Roots 的根对象出发,沿着引用链追踪,能被追踪到的对象就是”存活”的,其余都是”可回收”的。

GC Roots 包括:

GC Root 类型 说明 示例
虚拟机栈中引用的对象 当前执行方法的局部变量表 方法内的局部变量 Object obj = new Object()
静态变量引用的对象 类的 static 字段 private static final Singleton INSTANCE = ...
JNI 全局引用 Native 代码中的 NewGlobalRef JNI 中缓存的全局对象
活跃线程对象 所有存活的 Thread 对象 Thread 实例本身
同步监视器对象 synchronized 持有的对象 synchronized(lock) {} 中的 lock
ART 内部引用 Class 对象、ClassLoader、String 常量池 类加载时注册到 Runtime 的引用

2.3 引用类型与 GC 行为

Java 定义了四种引用类型,它们在 GC 时行为不同:

// 强引用(Strong Reference)—— 永远不会被 GC 回收
Object strongRef = new Object();

// 软引用(Soft Reference)—— 内存不足时回收
SoftReference<Bitmap> softRef = new SoftReference<>(bitmap);
// 适用于缓存场景:内存紧张时自动释放

// 弱引用(Weak Reference)—— 下次 GC 时立即回收
WeakReference<Activity> weakRef = new WeakReference<>(activity);
// 适用于避免内存泄漏:如 Handler 持有 Activity 的弱引用

// 虚引用(Phantom Reference)—— 无法通过 get() 获取对象,仅用于跟踪回收时机
PhantomReference<Object> phantomRef =
new PhantomReference<>(obj, referenceQueue);
// 适用于对象销毁后的资源清理

// 终结器引用(Finalizer Reference)—— JDK 内部使用,调用 finalize()

软引用在 Android 中的应用:Android 的 LruCacheDiskLruCache 就是基于强引用 + 内存阈值控制。早期 Android 开发中常用 SoftReference 做图片缓存,但在 ART 中软引用的回收时机较为激进(基本在 GC 时就会被回收),实际上还不如使用 LruCache 手动控制缓存大小来得高效。

三、垃圾回收算法

3.1 标记-清除(Mark-Sweep)

这是最基础也最直观的算法,分为两个阶段:

标记阶段:从 GC Roots 出发,遍历所有可达对象,将它们标记为”存活”。
清除阶段:遍历整个堆,将未被标记的对象回收,并将标记清除以备下次 GC。

优点:无需移动对象,实现简单。
缺点:产生内存碎片。长期运行后,堆中可能有很多小空隙,
无法分配大对象(即使总空闲内存足够)。

标记-清除在 ART 中的应用:ART 的 CMS(Concurrent Mark-Sweep)GC 就是基于此算法,但通过并发标记和清扫阶段来减少 STW 时间。

3.2 标记-整理(Mark-Compact)

在标记-清除的基础上增加了整理阶段:将所有存活对象向堆的一端移动,消除碎片。

优点:消除内存碎片,可以连续分配大对象。
缺点:需要移动对象,更新所有指向这些对象的引用,STW 时间较长。

适用场景:老年代(Old Generation)中对象存活率高,复制算法效率低,更适合使用标记-整理。

3.3 复制算法(Copying Collection)

将堆分为两个大小相等的半区:From Space 和 To Space。内存只在 From Space 中分配。GC 时将 From Space 中的存活对象复制到 To Space,然后交换两个半区的角色。

Cheney 算法是复制算法的一种高效实现,使用广度优先遍历将对象从 From Space 复制到 To Space,同时修正所有引用。

优点:分配速度快(只需要指针碰撞),无碎片。
缺点:只能使用一半堆内存,复制大对象成本高。

适用场景:新生代(Young Generation)中大部分对象朝生夕死,存活率低,复制算法效率最高。

3.4 分代回收(Generational Collection)

JVM 将堆划分为新生代(Young Generation)老年代(Old Generation)

堆内存结构(HotSpot JVM):
┌────────────────────────────────────────┐
│ Young Generation │
│ ┌───────┬──────────┬──────────┐ │
│ │ Eden │ Survivor │ Survivor │ │
│ │ │ S0 │ S1 │ │
│ └───────┴──────────┴──────────┘ │
├────────────────────────────────────────┤
│ Old Generation (Tenured) │
└────────────────────────────────────────┘

对象分配与晋升流程:

  1. 新对象在 Eden 区分配(TLAB 机制优化)
  2. Eden 区满时触发 Minor GC(Young GC):
    • 将 Eden 和一个 Survivor 区中的存活对象复制到另一个 Survivor 区
    • 清除 Eden 和被复制的 Survivor 区
    • 存活对象年龄 +1
  3. 当对象年龄达到阈值(默认 15,-XX:MaxTenuringThreshold),晋升到老年代
  4. 老年代满时触发 Major GC / Full GC(Stop-The-World)
Minor GC vs Major GC vs Full GC:
- Minor GC: 只回收新生代,速度快,STW 时间短(通常 < 100ms)
- Major GC: 只回收老年代(通常伴随 Minor GC)
- Full GC: 回收整个堆 + 方法区,速度最慢,STW 时间长(可达数秒)

四、ART 中的 GC 实现

Android Runtime (ART) 对 GC 做了大量针对移动设备的优化。相关源码位于 AOSP 的 art/runtime/gc/ 目录。

4.1 ART GC 的类型

ART 定义了多种 GC 类型,根据触发原因和回收范围分类:

GC 类型 说明 触发条件
kGcCauseForAlloc 分配内存时堆空间不足 应用分配新对象失败
kGcCauseBackground 后台 GC ART 判定为合适的时机
kGcCauseExplicit 显式 GC System.gc() 调用
kGcCauseForNativeAlloc Native 内存分配压力 JNI 的 NewGlobalRef
kGcCauseCollectorTransition GC 算法切换 ART 选择切换 GC 策略

源码中的定义在 art/runtime/gc/gc_cause.h

enum GcCause {
kGcCauseForAlloc,
kGcCauseBackground,
kGcCauseExplicit,
kGcCauseForNativeAlloc,
kGcCauseCollectorTransition,
// ...
};

4.2 CMS(Concurrent Mark-Sweep)—— ART 早期默认 GC

从 Android 5.0 (API 21) 到 Android 9 (API 28),ART 默认使用 CMS(Concurrent Mark-Sweep)GC。CMS 的核心思想是将大部分 GC 工作放在后台线程中并发执行,只在少量关键阶段才需要 STW。

CMS 的各个阶段:

阶段 1: Initial Mark (STW)       — 标记 GC Roots 直接可达的对象(暂停 < 1ms)
阶段 2: Concurrent Mark — 从 GC Roots 并发追踪所有可达对象
阶段 3: Pre-Clean — 处理并发标记阶段新产生的引用变化
阶段 4: Remark (STW) — 修正并发标记阶段的遗漏(暂停数 ms)
阶段 5: Concurrent Sweep — 并发回收垃圾对象
阶段 6: Concurrent Clear — 并发清理标记信息

CMS 在 ART 中的实现

  • 源码路径:art/runtime/gc/collector/concurrent_copying.cc
  • 核心类:ConcurrentMarkSweep (早期), ConcurrentCopying (Android 10+)
  • CMS 使用一个 card table 来记录并发标记期间被修改的引用(write barrier)
  • 相关机制:Baker Read Barrier(读屏障,用于并发复制 GC)

4.3 CC(Concurrent Copying)—— Android 10 引入

Android 10 (API 29) 引入了 Concurrent Copying (CC) GC,显著改善了 GC 的性能。CC GC 的核心创新:使用 Baker 风格的读屏障(Read Barrier) 实现并发对象复制。

CMS GC 问题: Mark-Sweep 产生内存碎片,长时间运行后需要整理
CC GC 优势: 使用 Copying 算法,自带整理功能,无碎片问题

Read Barrier 的工作原理

当一个对象被 GC 线程移动到新的内存位置后,旧位置会留下一个”转发指针”(forwarding pointer)。应用线程在读取对象引用时,读屏障检查引用是否指向旧位置——如果是,则自动跳转到新位置。

// art/runtime/gc/collector/concurrent_copying.cc (简化逻辑)
inline mirror::Object* ConcurrentCopying::ReadBarrier(mirror::Object* ref) {
if (UNLIKELY(ref->IsForwarded())) {
return ref->GetForwardingAddress();
}
return ref;
}

CC GC 的各个阶段

阶段 1: InitializePhase — 初始化,设置 From/To Space
阶段 2: MarkingPhase — 并发标记存活对象(无 STW)
阶段 3: CopyingPhase — 并发复制对象到新空间(极短 STW 用于开始)
阶段 4: ReclaimPhase — 回收旧空间

4.4 ART GC 的堆结构

ART 的堆与 HotSpot JVM 不同,没有严格的”分代”划分。但 ART 使用 Space 的概念来管理不同的内存区域:

ART 堆结构:
├── Image Space — 来自 boot.art 的预编译系统类(只读,共享)
├── Zygote Space — 从 Zygote 进程 fork 时继承的预加载类
├── Allocation Space — 应用分配的常规对象
├── Large Object Space — 大对象(数组、Bitmap 等,防止碎片化)
└── Non-moving Space — 不能被移动的对象(JNI 直接引用等)

源码路径:art/runtime/gc/space/ 下的各个 space 实现。

4.5 GC Pause(暂停)与 Android 性能

GC 暂停是导致 Android 应用卡顿的重要原因之一。在 ART 中,GC 的暂停监控可以通过 logcat:

# 查看 GC 日志
adb logcat | grep -i "GC_"

# 典型输出:
# I/art: Explicit concurrent mark sweep GC freed 12523(1088KB)
# AllocSpace objects, 0(0B) LOS objects, 25% free,
# 23MB/31MB, paused 2.543ms total 69.543ms

关键指标解读:

  • paused 2.543ms:STW 暂停时间(越短越好)
  • total 69.543ms:GC 总耗时(包括并发阶段)
  • 25% free:当前空闲堆比例
  • 23MB/31MB:已使用 / 总计

五、GC 根与内存泄漏

5.1 常见内存泄漏模式

(1)静态变量持有 Activity 引用

public class MyActivity extends Activity {
private static Context sContext; // 泄漏源!

@Override
protected void onCreate(Bundle savedInstanceState) {
sContext = this; // Activity 实例被静态变量持有
}
}

(2)非静态内部类持有外部引用

public class MyActivity extends Activity {
// Handler 是匿名内部类,持有外部 Activity 的隐式引用
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// 如果消息延迟 10 分钟发送,Handler 持有 Activity 引用 10 分钟
}
};
}

(3)单例模式持有 Context

public class DataManager {
private static DataManager sInstance;
private Context mContext;

public static DataManager getInstance(Context context) {
if (sInstance == null) {
// 如果传入的是 Activity Context,将导致 Activity 泄漏
sInstance = new DataManager(context);
}
return sInstance;
}
// 修正:使用 context.getApplicationContext()
}

5.2 使用 Memory Profiler 检测泄漏

Android Studio 的 Memory Profiler 是检测内存泄漏的利器:

  1. 打开 Profiler → 选择 Memory
  2. 反复执行怀疑泄漏的操作(如打开/关闭 Activity)
  3. 手动触发 GC(点击垃圾桶图标)
  4. 如果内存曲线呈”锯齿状”上升且不回落,说明有泄漏
  5. Dump Java heap → 查看 Object 列表 → 搜索怀疑泄漏的类名

5.3 LeakCanary 原理

LeakCanary 是 Square 开源的内存泄漏检测库。其工作原理:

  1. 监听 Activity/Fragment 的 onDestroy()
  2. 延迟 5 秒后,创建一个弱引用指向已销毁的对象
  3. 触发 GC
  4. 检查弱引用是否被清除——如果未被清除,说明存在泄漏
  5. Dump heap → 分析引用链 → 报告最短泄漏路径

六、避免 GC 性能问题的编程实践

6.1 减少对象分配

// 差:在循环中创建对象
for (int i = 0; i < 10000; i++) {
String s = new String("hello"); // 每次循环分配新对象
}

// 好:复用对象
String s = "hello"; // 字符串常量池复用
for (int i = 0; i < 10000; i++) {
// 使用已存在的字符串对象
}

// 差:自动装箱
Integer sum = 0;
for (int i = 0; i < 1000; i++) {
sum += i; // 每次 + 都创建新的 Integer 对象
}

// 好:使用基本类型
int sum = 0;
for (int i = 0; i < 1000; i++) {
sum += i; // 只操作栈上的 int
}

6.2 使用 Object Pool

// 使用 Android 内置的对象池
Message msg = Message.obtain(); // 从 Message 池获取
// ... 使用 msg ...
msg.recycle(); // 归还到池中

// 自定义对象池
private static class BitmapPool {
private static final List<Bitmap> sPool = new ArrayList<>();

static Bitmap obtain(int w, int h, Bitmap.Config config) {
synchronized (sPool) {
for (int i = 0; i < sPool.size(); i++) {
Bitmap b = sPool.get(i);
if (b.getWidth() == w && b.getHeight() == h
&& b.getConfig() == config) {
sPool.remove(i);
return b;
}
}
}
return Bitmap.createBitmap(w, h, config);
}

static void recycle(Bitmap bitmap) {
synchronized (sPool) {
if (sPool.size() < 10) {
sPool.add(bitmap);
}
}
}
}

6.3 使用合适的集合类

// HashMap vs ArrayMap (Android 特有)
// ArrayMap 在元素数量少(< 1000)时比 HashMap 更省内存
// 因为 HashMap 使用 Node 数组 + 链表,ArrayMap 使用两个平行的 int 和 Object 数组

ArrayMap<String, Object> map = new ArrayMap<>(); // 内存友好

// SparseArray 替代 HashMap<Integer, Object>
// 避免了 int → Integer 的自动装箱
SparseArray<Object> sparseArray = new SparseArray<>(); // 推荐

七、源码路径汇总

组件 路径 说明
GC 核心接口 art/runtime/gc/gc_cause.h GC 触发原因枚举
Concurrent Copying GC art/runtime/gc/collector/concurrent_copying.cc CC GC 实现(Android 10+)
Mark Sweep GC art/runtime/gc/collector/mark_sweep.cc MS GC 实现
Semi-Space GC art/runtime/gc/collector/semi_space.cc 半空间复制 GC
Heap art/runtime/gc/heap.cc 堆管理主类
Space 实现 art/runtime/gc/space/ 各内存空间实现
Baker Read Barrier art/runtime/gc/collector/concurrent_copying.h 读屏障声明
GC 性能监控 art/runtime/gc/accounting/ 堆统计信息
Reference 处理 art/runtime/gc/reference_processor.cc 软/弱/虚引用处理

八、常见面试题

Q1: 简述 GC Roots 有哪些?为什么它们能作为根?

A: GC Roots 包括:(1) 当前正在执行的方法的栈帧中的局部变量和参数;(2) 类的静态变量(包括常量池中的引用);(3) JNI 全局引用(GlobalRef);(4) 活跃的 Thread 对象;(5) 被 synchronized 持有的对象(锁对象);(6) ART/JVM 内部的系统类引用(如 ClassLoader、Class 对象)。它们能作为根的共同特征是:这些引用是”活的”——即程序执行所必需、不可能被回收的引用。从这些根出发,沿着引用链走的每一步都证明对象是被需要的。如果没有任何 GC Root 到达某个对象,那么这个对象对程序来说已经”死”了,可以安全回收。

Q2: Minor GC 和 Full GC 有什么区别?什么情况下会触发 Full GC?

A: Minor GC 只回收新生代(Young Gen),发生在 Eden 区满时。由于新生代对象存活率低,Minor GC 通常很快(几毫秒到几十毫秒)。Full GC 回收整个堆(新生代 + 老年代 + 方法区/元空间),通常在以下情况触发:(1) 老年代空间不足;(2) 方法区(Metaspace)空间不足;(3) 调用 System.gc() 显式触发(取决于 JVM 实现);(4) Minor GC 后 Survivor 区放不下存活对象,需要晋升到老年代但老年代也放不下;(5) CMS GC 的 Concurrent Mode Failure(并发回收速度跟不上分配速度)。Full GC 的 STW 时间可达数秒,对应用响应时间影响极大,因此 JVM 调优的核心目标之一就是降低 Full GC 频率。

Q3: ART 的 GC 与 HotSpot JVM 的 GC 有什么不同?为什么 ART 不做严格的分代?

A: 主要区别:(1) ART 使用 Concurrent Copying (CC) GC,HotSpot 使用 G1/Shenandoah/ZGC 等分代或分区 GC;(2) ART 没有严格的”分代”(Eden/Survivor/Old),而是使用 Image Space + Zygote Space + Allocation Space 的划分;(3) 不做严格分代的原因:Android 应用的特点——大部分类在 Zygote fork 时已经预加载(Zygote Space 中的对象是只读的、共享的),应用层新分配的对象生命周期短且量小,因此分代的收益不大。CC GC 通过复制算法已经能很好地处理碎片问题,不需要独立的”老年代整理”。

Q4: 什么是 Write Barrier(写屏障)和 Read Barrier(读屏障)?它们在 GC 中分别起什么作用?

A: Write Barrier 是在对象引用被修改时插入的一段代码,用于通知 GC”这个引用变了”。在 CMS GC 中,并发标记阶段应用线程可能修改已经被标记过的对象的引用,Write Barrier 记录这些修改到 card table(一个记录”脏”内存块的位图),在 Remark 阶段重新扫描。Read Barrier 是在读取对象引用时插入的检查代码。在 CC GC 中,由于对象可能在并发复制阶段被移动,Read Barrier 确保应用线程读取到的始终是对象的最新位置——如果读到的是旧地址(带有转发指针),就自动跳转到新地址。Android 10 的 CC GC 使用 Baker Read Barrier(以发明者 Henry Baker 命名),这是 CC GC 能实现低 STW 的关键技术。

Q5: 如何通过代码降低 GC 压力?给出具体的编程建议。

A: (1) 避免在循环中创建对象——将对象创建移到循环外或使用对象池;(2) 优先使用基本类型(int、long)而非包装类型(Integer、Long),避免自动装箱;(3) 对于小数据量的 Map,使用 ArrayMap 替代 HashMap(Android 平台);(4) 使用 SparseArray 替代 HashMap<Integer, Object>;(5) 及时释放大对象:Bitmap 不用时调用 recycle()(API < 10)或置 null 让 GC 回收;(6) 使用 StringBuilder 而非 + 拼接字符串;(7) 设置合理的缓存上限,避免无限增长(如 LruCache 的 maxSize);(8) 避免在 onDraw() 中分配对象——onDraw() 每帧调用一次,60fps 下就是每秒 60 次;(9) 对于频繁创建销毁的 Activity,注意及时清理静态引用和监听器注册。

Q6: 为什么 System.gc() 不建议频繁调用?ART 是如何处理它的?

A: System.gc() 是一个请求(不是命令),JVM/ART 可以选择忽略它。在 HotSpot JVM 中,-XX:+DisableExplicitGC 可以完全禁用 System.gc()。在 ART 中,System.gc() 通常会导致一次完整的 GC 暂停,浪费 CPU 资源,且回收效果不一定好(因为刚回收完,马上又有新对象分配)。更重要的是,频繁的 Full GC 会导致应用卡顿,用户感知明显。ART 的处理策略:当应用调用 System.gc() 时,如果 ART 认为当前没有 GC 的必要(如堆空间充足),它可能只执行一次轻量级的 GC 甚至不做任何操作。最好的做法是:信任 GC 的自动调度,只在非性能关键路径的特定场景(如进入后台前)调用一次 System.gc() 来建议清理。


参考文档:

打赏
  • 微信
  • 支付宝

评论