第一部分:垃圾回收基础
1.1 什么是垃圾回收
定义: 垃圾回收(Garbage Collection,GC)是自动管理内存的机制,能自动识别并回收不再使用的对象,释放其占用的内存。
作用:
| 作用 | 说明 |
|---|---|
| 自动释放内存 | 无需手动释放,降低忘记释放导致泄漏的风险 |
| 避免内存泄漏 | 回收不可达对象,减缓内存被无效占用 |
| 简化开发 | 开发者更关注业务,少关心释放细节 |
| 提升效率 | 减少因内存管理不当产生的 Bug |
在 Android 中的重要性:
设备内存有限、GC 影响流畅度、频繁 GC 易卡顿、内存不足时应用可能被系统回收。
public void createObjects() {
Object obj1 = new Object();
Object obj2 = new Object();
// 不再被引用后,GC 会自动回收,无需手动释放
}
1.2 如何判断对象已死
1.2.1 引用计数法(Android 未采用)
- 做法: 对象被引用时计数 +1,取消引用时 -1,计数为 0 则可回收。
- 问题: 无法处理循环引用,两个对象互相引用时计数永不为 0,导致泄漏。
- 结论: Android 使用可达性分析,不用引用计数。
1.2.2 可达性分析(Android 采用)
思路: 从一组「GC Roots」出发,能到达的对象视为存活,不可达的视为垃圾,可被回收。
flowchart LR
R1["GC Roots: 栈/静态/常量/锁"] --> R2["可达: 对象1-对象2"]
R3["不可达: 对象3-对象4"] -.->|垃圾| R4["可回收"]
常见 GC Roots:
| 类型 | 示例 |
|---|---|
| 虚拟机栈中的引用 | 局部变量、方法参数 |
| 方法区静态属性 | static 变量 |
| 方法区常量 | 字符串常量等 |
| 本地方法栈引用 | JNI 引用 |
| 同步锁持有对象 | synchronized(lock) 的 lock |
| 内部引用 | Class、异常对象等 |
循环引用在可达性分析下可被正确回收:只要从 GC Roots 不可达,即使对象之间互相引用也会被当作垃圾。
1.2.3 引用类型(简要)
| 类型 | 特点 | 典型用法 |
|---|---|---|
| 强引用 | 存在则不会被回收 | 普通对象、Activity 等 |
| 软引用 | 内存紧张时才回收 | 缓存(Android 更推荐 LruCache) |
| 弱引用 | 下次 GC 就可能回收 | Handler 持 Activity、WeakHashMap |
| 虚引用 | 最弱,多用于回收前清理 | 少见,如堆外内存清理 |
第二部分:垃圾回收算法
2.1 三种基础算法对比
| 算法 | 思路 | 优点 | 缺点 | Android 使用 |
|---|---|---|---|---|
| 标记-清除 | 先标记再清除未标记对象 | 实现简单、不移动对象 | 易产生碎片 | 非主流 |
| 标记-复制 | 存活对象复制到另一块,再清空当前块 | 无碎片、适合年轻代 | 占用双倍空间(可优化) | 年轻代主用 |
| 标记-整理 | 标记后把存活对象向一端紧凑移动,再清理边界外的内存 | 无碎片、不浪费整块空间 | 移动对象、更新引用成本高 | 老年代 |
2.1.1 标记-清除(Mark-Sweep)
两阶段: ① 从 GC Roots 遍历,标记所有存活对象 → ② 遍历整块内存,清除所有未标记的对象。
问题: 清除后存活对象之间会留下许多“空洞”,即内存碎片,后续分配大对象时可能找不到连续空间。
flowchart LR
A1["回收前: A活 B垃圾 C活 D垃圾 E活"] --> A2["标记: A留 B除 C留 D除 E留"] --> A3["清除后: A 空 C 空 E"]
小结: 存活对象位置不变,只把“垃圾”清掉 → 中间出现空洞,形成碎片。
2.1.2 标记-复制(Mark-Copy)
思路: 内存一分为二(From 区 / To 区),平时只在 From 区分配。GC 时:标记 From 区存活对象 → 把存活对象复制到 To 区 → 整块清空 From 区。下次 GC 时 From/To 角色互换。
优点: 复制后 To 区一侧全是存活对象,没有空洞,无碎片;分配时顺序往后挪即可。
flowchart LR
B1["From区: A活 B垃圾 C活 D垃圾"] --> B2["复制存活到To"]
B2 --> B3["To区: A C | From区: 清空"]
小结: 只复制存活对象到另一块,原块整块清空 → 新块连续无碎片,但需要预留一半空间(Appel 式用 Eden+2个 Survivor 可只浪费约 10%)。
2.1.3 标记-整理(Mark-Compact)
三阶段: ① 标记所有存活对象 → ② 紧凑移动:把存活对象往内存一端(如左侧)挨个挪动,中间不留空 → ③ 清边界:在“最后一个存活对象”后面画一条边界,边界外的整块内存统一回收。
优点: 存活对象集中在一端,另一侧是一大块连续空闲,无碎片,且不需要双倍内存。
flowchart LR
C1["回收前: A活 空 B活 空 空 C活 空"] --> C2["紧凑移动"]
C2 --> C3["一端存活ABC 一端可回收"]
“清边界”含义: 不逐个清每个空洞,而是把「最后一个存活对象」右边的整块区域视为空闲,一次性用于后续分配。
小结: 标记 → 紧凑移动(无空洞)→ 清边界 → 得到一端连续存活、一端连续空闲,无碎片。
2.2 分代收集(Appel 式):年轻代 + 老年代是一套设计
年轻代和老年代是同一套分代策略的两块区域:年轻代用 Appel 式(Eden + 两块 Survivor)+ 标记-复制,老年代用标记-整理,两者配合成一套。
2.2.1 为什么用分代
大部分对象创建后很快不用,少数长期存活;把新对象和长期存活对象分开存放、用不同算法回收,省时、少碎片。年轻代 + 老年代即这一思路下的同一套布局。
2.2.2 新老代总览(谁是谁、怎么收)
堆分两大块:
| 代 | 包含区域 | 存什么 | 回收算法 | 何时回收 |
|---|---|---|---|---|
| 年轻代 | Eden + Survivor0 + Survivor1(三块) | 新对象、上轮存活对象(S0 与 S1 轮流当 From/To) | 标记-复制 | Eden 满 → Minor GC |
| 老年代 | 独立一块(≠ Survivor,不是 S0/S1) | 从年轻代晋升来的对象、大对象 | 标记-整理 | 老年代满/堆不足等 → Full GC |
要点: 老年代 ≠ Survivor1,是堆里单独的一块。Minor GC 时,Eden 与当前 From 的存活对象都复制到当前 To(S0、S1 之一);To 放不下则多出的晋升。晋升条件详见 2.2.3。
2.2.3 年轻代:Eden + S0/S1 + 晋升条件(重要)
年轻代三块各干什么:
| 区域 | 作用 |
|---|---|
| Eden | 新对象都在这里分配;约占年轻代 80%(Appel 式 8:1:1)。 |
| Survivor0 / Survivor1(S0、S1) | 两块大小相同(各约 10%),轮流当 From / To,存上一轮 Minor GC 后仍存活的对象。 |
Minor GC 触发与“往哪放”:
- 何时触发:一般是 Eden 满了、新对象没地方分配时触发 Minor GC。只要发生了 Minor GC,就会回收年轻代(Eden + 当前 From),并把存活对象复制走。
- Eden 和 From 的存活对象往哪放:每次 Minor GC 时,S0、S1 里一块是 From(源)、一块是 To(目标)。Eden 的存活对象和当前 From 的存活对象,都复制到当前作为 To 的那一块里。下次 Minor GC 时 From 与 To 互换。能否全部放进 To,按下面表格判断:
| To 是否放得下 | 判断 | 结果 |
|---|---|---|
| 放得下 | Eden + From 的存活对象总量 ≤ To 可用空间 | 全部复制到 To,留在年轻代,年龄 +1 |
| 放不下 | Eden + From 的存活对象总量 > To 可用空间 | 能放进 To 的进 To;多出的直接晋升到老年代 |
从年轻代晋升到老年代的条件(重要): 老年代是独立的一块,不是 Survivor。满足任一即晋升:
| 条件 | 说明 |
|---|---|
| Survivor 放不下 | 即上表「To 放不下」:本次存活对象太多,To 装不下,多出的直接进老年代。 |
| 对象年龄达阈值 | 在年轻代经历多次 Minor GC 仍存活(如年龄 ≥ 15),晋升到老年代。 |
一次 Minor GC 的流程:
flowchart LR
D1["Eden满+S0+S1"] --> D2["标记"] --> D3["复制到To"] --> D4["清空From"] --> D5["互换角色"]
小结: 年轻代 = Eden(分配)+ 两块 Survivor(轮流 From/To);Minor GC 时 Eden + From 的存活对象都进 To,To 放不下则多出的晋升;晋升条件见上表。
2.2.4 老年代
- 存什么: 从年轻代晋升来的对象(晋升时可能在 S0 或 S1)、部分大对象。
- 算法: 标记-整理(无碎片、不双倍空间)。
- 何时回收: 仅在 Full GC 时(老年代满 / 堆不足 / 显式
System.gc());Full GC 时年轻代也参与,暂停明显长于 Minor GC。
2.2.5 新老代关系(一张图)
flowchart LR
Y1["年轻代 Eden+S0+S1"] -->|晋升| Y2["老年代"]
Y2 -.->|Full GC回收| Y2
对象在年轻代 S0/S1 间轮流复制,满足晋升条件(见 2.2.3)则进入老年代;老年代仅 Full GC 时回收。
2.3 分代收集策略小结
对象从创建到回收的流转与对比如下;晋升条件见 2.2.3。
flowchart LR
E1["新对象"] --> E2["Eden"] --> E3["Minor GC"]
E3 --> E4{"存活"}
E4 -->|否| E5["回收"]
E4 -->|是| E6["Survivor"] --> E7{"年龄"}
E7 -->|否| E6
E7 -->|是| E8["晋升"] --> E9["Full GC"]
| 代 | 存什么 | 算法 | 触发 | 回收类型 | 暂停 |
|---|---|---|---|---|---|
| 年轻代 | 新对象、短期对象 | 标记-复制(Eden+S0+S1) | Eden 满 | Minor GC | 短 |
| 老年代 | 晋升对象、大对象 | 标记-整理 | 老年代满/堆不足/System.gc() | Full GC | 长 |
第三部分:Android ART 垃圾回收器
ART(Android Runtime) 是 Android 5.0 起使用的运行时,替代了早期的 Dalvik。ART 的 GC 与 JVM 不同:只有一种收集器(并发复制收集器),面向低延迟、少卡顿,适合手机上的交互式应用。
3.1 ART 与 JVM 的差异
ART 不是 JVM:编译方式、GC 机制、适用场景都不同。
| 维度 | ART | JVM |
|---|---|---|
| 是什么 | Android 运行时,替代 Dalvik | Java 虚拟机,常见于服务端/桌面 |
| 编译 | AOT(安装或安装后编译为机器码) | 多为 JIT(运行时编译),部分支持 AOT |
| 收集器 | 仅一种:并发复制(CC),无需选择 | 多种可选(Serial、Parallel、CMS、G1、ZGC 等) |
| 目标 | 低延迟、减少卡顿、适合交互 | 吞吐量/延迟等可按场景配置 |
为什么 ART 只有一种收集器? 移动端场景统一(交互式、内存有限、对卡顿敏感),ART 针对这类场景做好一种即可,应用开发者无需、也无法选择收集器;JVM 则要兼顾服务端、批处理等多种场景,所以提供多种收集器。
3.2 并发复制收集器(CC)的特性
ART 的 GC 就是这一种:并发复制收集器(Concurrent Copying,CC)。
本质:还是分代回收,只不过加了并发。 CC 用的仍是 2 节的分代模型(年轻代 Eden+S0/S1 标记-复制,老年代标记-整理);区别在于执行方式:尽量在后台线程做标记、复制等耗时工作,减少主线程暂停。可理解为「分代回收 + 并发执行」。
下面都是 CC 的特性(不是多种回收方式,而是同一套收集器的不同侧面):
| 特性 | 含义 | 说明 |
|---|---|---|
| 分代回收 | 年轻代 + 老年代,不同代用不同算法 | 与 2 节一致:年轻代标记-复制,老年代标记-整理 |
| 并发回收 | 尽量在后台线程做标记/复制 | 在分代基础上把耗时操作放后台,减少主线程暂停 |
| 复制算法 | 年轻代用复制 | 存活对象复制到另一块,无碎片,即 2 节 Appel 式 Eden+S0/S1 |
| 增量回收 | 把一次大回收拆成多小步 | 单次暂停更短,多次短暂停代替一次长暂停 |
| 压缩回收 | 老年代整理内存 | 回收时移动存活对象、减少碎片,即 2 节老年代标记-整理 |
串行 vs 并发(直观对比): 串行回收时主线程要停下来等 GC;ART 的并发回收尽量在后台做,主线程可继续跑,从而减少卡顿。
flowchart LR
F1["串行: 主线程-暂停GC-主线程"] --> F2["并发: 主线程继续 + 后台GC"]
3.3 GC 触发条件
ART 根据堆使用情况自动触发不同范围的 GC,与 2 节的 Minor GC / Full GC 对应。三种类型如下。
| 类型(英文) | 中文名称 | 触发条件 | 回收范围 | 耗时与影响 |
|---|---|---|---|---|
| Partial GC | 部分回收(年轻代回收) | 年轻代空间不足,如 Eden 区满了 | 仅年轻代(Eden + 当前 Survivor),不碰老年代 | 暂停短(通常几毫秒),即 Minor GC,对流畅度影响小 |
| Full GC | 完全回收(整堆回收) | 堆整体紧张、老年代满了、或代码里调用了 System.gc() | 整堆(年轻代 + 老年代)一起回收 | 暂停时间长(可能几十到几百毫秒),容易造成卡顿,应尽量避免 |
| Sticky GC | 粘性回收(局部年轻代回收) | 频繁分配“刚用就丢”的对象,例如列表快速滑动、动画帧、临时对象大量创建 | 只清理最近新分配的那一小块区域(年轻代里刚分配不久的对象) | 暂停极短,几乎无感,专门为“分配很多、死得很快”的场景优化 |
简要对照:
- Partial GC:只收年轻代,即 Minor GC,最常见。
- Full GC:收整堆,老年代满或堆不足时触发,耗时长、易卡顿;避免主动调
System.gc()。 - Sticky GC:只收“刚分配不久”的区域,用于列表滑动、动画等高频分配,停顿极短。
建议: 不要主动调用 System.gc(),易引发 Full GC,增加卡顿风险。
3.4 内存分配策略(详细)
ART 的对象分配与 2 节的分代、晋升一致:新对象先进年轻代,大对象或熬过多次 GC 的会进老年代;堆有上限,分配失败可能触发 GC 或 OOM。
3.4.1 新对象:优先在年轻代(Eden)分配
- 规则:
new出来的普通对象,默认在年轻代的 Eden 区分配。 - 原因:大多数对象生命周期很短,放在年轻代便于用 Minor GC 快速回收,减少对老年代的压力。
- 流程:分配时先看 Eden 是否有足够连续空间;有则直接分配;没有则触发 Minor GC(回收年轻代),再尝试分配;若年轻代仍不足,可能再触发 Full GC;若 Full GC 后仍不足,则抛出 OutOfMemoryError。
- 小结:日常分配都发生在 Eden,Eden 满会触发 Minor GC,是应用中最常见的 GC 来源。
3.4.2 大对象:可能直接进老年代
- 什么是大对象:占用的内存超过一定阈值(具体由 ART 决定,可理解为“比一块 Survivor 还大”或类似量级),例如大数组、大 Bitmap。
- 为什么可能直接进老年代:若在年轻代分配,每次 Minor GC 都要在 Eden 与两块 Survivor 之间复制,成本高且可能装不下;直接放进老年代可避免在年轻代反复复制。
- 对开发的影响:大对象会占用老年代、增加 Full GC 压力;应尽量控制大小、及时释放(如 Bitmap 用后
recycle()),避免一次分配过多或长期不释放。
3.4.3 晋升:从年轻代进入老年代
- 规则:在年轻代(Eden、S0/S1)里经历多次 Minor GC 仍存活的对象,满足晋升条件时会进入老年代(详见 2.2.3:Survivor 放不下 或 对象年龄达阈值)。
- 年龄:ART 会为对象维护“在年轻代存活的次数”(年龄);超过阈值(如 15)即晋升。
- 意义:长期存活的对象集中在老年代,减少年轻代 GC 的扫描与复制开销;老年代只在 Full GC 时回收,频率低,适合“活很久”的对象。
3.4.4 堆上限与分配失败
- 堆有上限:每个进程的 Java 堆大小由系统限制,不同设备、不同 API 级别上限不同(几十 MB 到几百 MB 甚至更大)。应用无法超过该上限。
- 分配失败时:当堆中无法再划出足够连续空间时,会先触发 GC(Minor / Full)尝试回收;若回收后仍无法满足分配,则抛出 OutOfMemoryError。
- onLowMemory():系统整体内存紧张时,可能回调
ComponentCallbacks2.onLowMemory(),应用应在此释放非必要缓存、大对象等,减轻压力。 - largeHeap:在 AndroidManifest 中为 Application 设置
android:largeHeap="true"可申请更大堆上限,但只是缓解手段,仍应优化内存使用、避免泄漏与过度分配。
小结表(与 2 节对应):
| 规则 | 说明 |
|---|---|
| 新对象 | 优先在 Eden 分配;Eden 满 → Minor GC → 再分配;仍不足可能 Full GC 或 OOM |
| 大对象 | 可能直接进老年代,避免在年轻代复制;注意及时释放、控制大小 |
| 晋升 | 年轻代存活多次(年龄达阈值)或 Survivor 放不下 → 进老年代(见 2.2.3) |
| 堆上限 | 设备有堆上限;分配失败先 GC,再 OOM;可关注 onLowMemory()、合理使用 largeHeap |
3.5 与 2 节的关系(小结)
- 2 节:GC 的算法与分代模型(标记-清除/复制/整理,年轻代/老年代划分、晋升条件),通用原理,不限于 Android。
- 3 节:ART 上的具体实现——收集器(CC)、触发类型(Partial/Full/Sticky)、分配与晋升。
- 关系:2 节是“规则和模型”,3 节是“Android 里怎么用”;CC 仍用 2 节的分代回收,执行上加了并发。
第四部分:内存问题与优化
4.1 内存泄漏
4.1.1 定义与危害
定义: 对象在业务上已经不再使用,但仍有强引用指向它,GC 无法回收,占用的内存一直不释放,导致可用堆逐渐变小。
与内存溢出的区别:
| 内存泄漏 | 内存溢出(OOM) | |
|---|---|---|
| 本质 | 该回收的没回收,可用内存被无效占用 | 需要分配的内存超过堆上限,分配失败 |
| 过程 | 随时间累积,可用内存越来越少 | 某次分配时发现堆不够用 |
| 结果 | 最终可能引发 OOM、卡顿、ANR | 直接崩溃(OutOfMemoryError) |
危害: 可用堆变小 → Minor/Full GC 更频繁、暂停时间变长 → 卡顿;严重时老年代被占满 → 频繁 Full GC 或最终 OOM 崩溃;若主线程被阻塞还可能 ANR。
4.1.2 为什么会产生泄漏
- 对象要被 GC 回收,必须从 GC Roots 出发不可达。若某处一直持有它的强引用(如静态变量、单例、未取消的监听、未关闭的集合),它就始终可达,不会被回收。
- 典型情况:生命周期长的(Application、单例、静态)持有了生命周期短的(Activity、Fragment、临时对象),且没有在合适的时机(如 onDestroy)释放引用或取消注册。
4.1.3 常见场景与应对
| 场景 | 典型错误 | 为什么算泄漏 | 正确做法 |
|---|---|---|---|
| Activity/Context | 静态变量或单例持有 Activity/Context | 静态/单例与进程同寿,Activity 销毁后仍被引用,无法回收 | 用 Application 的 Context;或必须持有时用 WeakReference,并在 onDestroy 清空 |
| 内部类 / 匿名类 | 非静态内部类、匿名 Runnable/Listener 持有外部 Activity | 内部类隐式持有外部类引用,若被静态或长生命周期对象引用,Activity 无法回收 | 静态内部类 + WeakReference(Activity);或在 onDestroy 中移除回调、清空引用 |
| 监听器 / 广播 | EventBus、BroadcastReceiver、自定义 Listener 注册后不反注册 | 订阅表/系统持有对 Activity 的引用,Activity 销毁后仍被引用 | 在 onDestroy 中 unregister、unregisterReceiver、removeListener |
| 集合 | 全局 List/Map 只增不减,把 Activity 或大对象放进去了 | 集合被静态或长生命周期持有,其中的对象一直可达 | 及时 remove/clear;或用有界缓存(LruCache),在 onDestroy 或合适时机清理 |
| 线程 / 异步 | 匿名 Runnable、AsyncTask 内部持有 Activity,且任务长时间运行 | 线程/任务持有 Activity,若在后台一直不结束,Activity 无法回收 | 静态内部类 + 弱引用持 Activity;在 onDestroy 里 cancel 任务、中断线程 |
| 资源未关闭 | 文件流、Cursor、Socket、连接未 close | 资源对象可能间接持有 Context 或大块 native 内存,未关闭则一直占用 | try-with-resources 或 finally 中 close;Cursor 及时 close,避免在 Activity 里长期持有 |
Handler 示例(防泄漏): Handler 若由非静态内部类或匿名类实现,会持有外部 Activity;若消息延迟发送或消息队列未清空,Activity 销毁后仍被 Handler 引用。做法:静态内部类 + WeakReference(Activity),并在 onDestroy 中 removeCallbacksAndMessages(null)。
private static class MyHandler extends Handler {
private final WeakReference<Activity> ref;
MyHandler(Activity a) { ref = new WeakReference<>(a); }
@Override
public void handleMessage(Message msg) {
Activity a = ref.get();
if (a != null) { /* 使用 a */ }
}
}
// onDestroy 中:handler.removeCallbacksAndMessages(null);
4.1.4 如何检测与避免
- 检测:开发期用 LeakCanary 自动检测 Activity/Fragment 等泄漏;或通过 Profiler 看内存曲线只升不降、生成堆转储后用 MAT 看 Path to GC Roots,定位谁在持有。
- 避免:生命周期内成对“注册/反注册、持有/释放”;长生命周期不持短生命周期强引用,必要时用 WeakReference;集合、缓存有界并在合适时机清理;资源用 try-with-resources 或 finally 关闭。
4.2 内存溢出(OOM)
4.2.1 定义与危害
定义: 当次申请的内存超过堆上限(或当前无足够连续空间),无法完成分配,虚拟机抛出 OutOfMemoryError。(Android 上由 ART 抛出。)
危害: 应用崩溃,当前操作中断,用户体验差;若发生在启动或关键流程,可能造成数据未保存、流程无法完成。
4.2.2 常见原因
| 原因 | 说明 |
|---|---|
| 内存泄漏累积 | 泄漏导致可用堆越来越小,最终某次正常分配时触发 OOM(泄漏是 OOM 的常见诱因) |
| 单次分配过大 | 一次加载过大的图片、大数组、大列表到内存,超过堆剩余或堆上限 |
| 大对象 / 大数组过多 | 多个大 Bitmap、大数组同时存在,占满老年代或整堆 |
| 堆上限本身较小 | 低端设备或系统给应用的堆上限较低,同样代码更容易 OOM |
4.2.3 OOM 的常见类型
- java.lang.OutOfMemoryError: Java heap space:堆空间不足,无法分配新对象,最常见。
- java.lang.OutOfMemoryError: Failed to allocate a ... byte allocation:某次具体分配失败,可看出申请了多少字节、当前堆情况(若日志完整)。
4.2.4 解决与排查思路
解决思路:
| 方向 | 做法 |
|---|---|
| 查泄漏 | 用 LeakCanary、Profiler、MAT 排查是否存在泄漏,修复后可用堆恢复,减少 OOM |
| 控制单次分配 | 图片用 inSampleSize、合适宽高解码;大列表分页、按需加载;避免一次性把大批数据加载进内存 |
| 大对象与缓存 | 大 Bitmap 用后 recycle;缓存用 LruCache 等有界缓存,避免无限增长 |
| 分批与回收 | 大批数据分批处理;不再使用的对象及时置空、清集合,便于 GC 回收 |
| 堆与系统回调 | 必要时 android:largeHeap="true" 申请更大堆(仅缓解);在 onLowMemory() 中释放非必要缓存,减轻压力 |
排查思路: OOM 时若有堆转储(hprof),用 MAT 打开,看 Histogram / Dominator Tree 找出占用最大的对象,再用 Path to GC Roots 看是否被不该持有的引用链持有(即泄漏);若无转储,可在复现路径上提前用 Profiler 抓 heap dump,或加 try-catch 在 OOM 前后打日志、缩小范围。
4.2.5 与内存泄漏的关系
- 泄漏会让可用堆越来越小,更容易在后续某次分配时触发 OOM;很多 OOM 背后是长期泄漏。
- 因此:防泄漏(4.1)本身就是防 OOM 的重要手段;同时还要控制单次分配大小、大对象和缓存,避免“瞬时占用”或“无界缓存”直接撑爆堆。
4.3 Android 开发中的 GC 优化策略
优化目标: 降低 GC 暂停时间(尤其避免 Full GC)、减少内存占用与泄漏、提升流畅度并避免 ANR。
4.3.1 减少对象分配
- 对象复用 / 对象池:对频繁创建、生命周期短的对象(如列表项、临时 DTO)做池化,用完归还而非每次 new,减少年轻代压力和 Minor GC。
- 避免不必要的 new:循环里能用同一变量复用的不要每次 new;字符串拼接用
StringBuilder代替多次"a" + "b";能用基本类型int、boolean就不用包装类Integer、Boolean,减少装箱对象。 - 列表与 RecyclerView:依赖系统自带的 ViewHolder 复用,不在
onBindViewHolder里 new 新对象(如每次 new String、new 匿名 listener);数据用已有字段或轻量结构绑定。
4.3.2 合理使用缓存
- 有界缓存:用 LruCache 等带容量上限的缓存,避免无限增长的 Map 导致老年代膨胀和 Full GC;按业务设定合理 maxSize(如按条目数或总字节数)。
- 及时清理:在合适的生命周期(如
onDestroy、退出页面、内存紧张时)清理不再需要的缓存,避免长期占用堆。
4.3.3 典型场景优化
| 场景 | 常见问题 | 优化做法 |
|---|---|---|
| 列表滑动 | 频繁创建 ViewHolder 内对象、临时 String,触发 Partial GC 甚至卡顿 | ViewHolder 复用;onBindViewHolder 内不 new;大列表考虑分页、预加载 |
| 动画 | 每帧或每次动画 new 对象 | 复用 Animator、ValueAnimator 等,或使用静态/单例配置,避免在 onDraw/每帧里 new |
| 图片加载 | 大图、多图同时解码,占满年轻代/老年代 | 采样率 inSampleSize、合适尺寸;用 Glide/Coil 等库的缓存与生命周期绑定;及时 recycle 不再用的 Bitmap |
| 大对象 | 大数组、大 Bitmap 直接进老年代,加剧 Full GC | 控制单次分配大小;用完及时置空、recycle;必要时分批加载或流式处理 |
4.3.4 避免内存泄漏(减少 Full GC 与 OOM)
- 泄漏会导致可用堆越来越小,进而频繁 Full GC 或 OOM;防泄漏本身也是重要的“GC 优化”。
- 生命周期:在
onDestroy中取消监听、移除回调、清空对 Activity/Fragment 的引用、关闭 Cursor/流等。 - 引用方式:单例、静态变量不持 Activity/Context;若必须持有,用 Application 的 Context 或 WeakReference;Handler、异步任务用静态内部类 + 弱引用,并在 onDestroy 里 removeCallbacks。
- 结合 LeakCanary、Profiler 定期排查泄漏,避免“看不见的强引用”长期占用堆。
小结: GC 优化策略 = 少分配(对象池、复用、基本类型)+ 有界缓存与及时清理 + 列表/动画/图片/大对象等场景的具体写法 + 防泄漏(生命周期、弱引用、取消注册)。目标都是减少年轻代压力、避免老年代被占满,从而减少 GC 次数与暂停时间,提升流畅度。
第五部分:检测与诊断工具
常用于排查 GC、内存泄漏与 OOM 的工具:各自能看什么、怎么用、何时用。日常开发可优先用 Profiler + LeakCanary;精确定位引用链用 MAT;看 GC 频率与暂停用 logcat;分析卡顿是否与 GC 有关用 Systrace。
5.1 工具一览与选用
| 工具 | 主要用途 | 典型场景 |
|---|---|---|
| Android Studio Profiler | 实时看堆占用、GC 事件、生成堆转储、跟踪一段时间内的对象分配 | 看内存是否只升不降(泄漏嫌疑)、GC 是否频繁、谁在分配对象、导出 hprof 给 MAT |
| logcat | 看 GC 相关日志(类型、暂停时间、回收量) | 确认 Partial/Full GC 频率、pause 是否过长,配合优化前后对比 |
| Systrace | 看各线程时间线,含 GC 与主线程的关系 | 判断卡顿是否由 GC 引起、主线程在 GC 时是否被阻塞 |
| MAT(Memory Analyzer Tool) | 分析 hprof,看对象数量、占用、引用链 | 定位谁持有泄漏对象、大对象、OOM 时堆里什么占得多 |
| LeakCanary | 自动检测 Activity/Fragment 等泄漏并给出引用链 | 开发期自动发现泄漏,无需手动抓 dump,适合日常回归 |
何时用哪个: 怀疑泄漏 → LeakCanary 或 Profiler 看曲线 + 抓 dump;要查“谁持有了谁” → MAT 分析 hprof;看 GC 频率/暂停 → logcat;看卡顿是否和 GC 重叠 → Systrace。
5.2 Android Studio Profiler(内存与 GC)
5.2.1 打开与基本用法
- 菜单 View → Tool Windows → Profiler,或底部 Profiler 标签。
- 选择已运行的应用进程(Debug 构建),点击 Memory 行进入内存视图。
- 上方为堆占用曲线(可看 Java 堆、Native、Graphics 等),曲线下方会出现 GC 事件图标,点击可看该次 GC 的类型与耗时。
5.2.2 看什么、怎么判断
| 关注点 | 怎么看 | 说明 |
|---|---|---|
| 内存是否泄漏 | 曲线是否只升不降(操作后多次 GC 仍不回落) | 若反复操作同一流程后堆持续升高,有泄漏嫌疑 |
| GC 频率与类型 | 下方 GC 图标的密度与类型(Partial/Full 等) | Full GC 多或 GC 很密,需结合代码减少分配或查泄漏 |
| 单次 GC 耗时 | 点击 GC 图标看详情中的 paused / total 时间 | 暂停时间过长会直接导致卡顿 |
5.2.3 Heap Dump(堆转储)
- 在 Memory 视图中点击 Heap Dump(或工具栏对应按钮),等待生成完成。
- 可查看当前堆中按类/按实例的对象数量与大小,排查大对象、重复创建过多的类。
- Export 可导出为
.hprof文件,用 MAT 打开做引用链分析(需先用hprof-conv转换,见 5.4)。
5.2.4 Allocation Tracking(分配跟踪)
- 点击 Record allocations,操作应用一段时间(如滑动列表、进入某页),再 Stop。
- 可看到这段时间内哪些类被分配了多少次、调用栈在哪里,用于定位“谁在频繁 new 对象”、配合优化减少分配。
5.3 logcat 看 GC 日志
5.3.1 常用命令
# 过滤 GC 相关
adb logcat | grep -E "GC|dalvikvm|art"
# 仅当前应用(替换为你的包名)
adb logcat --pid=$(adb shell pidof -s 你的包名) | grep -E "GC|art"
在 Android Studio 的 Logcat 窗口里也可用过滤框输入 GC 或 art 查看。
5.3.2 日志里看什么
- GC 类型:Partial GC(年轻代)、Full GC(整堆)、Sticky GC 等,对应 3.3 节的三种类型。
- paused / total:本次 GC 的暂停时间与总耗时,paused 过长(如几十毫秒以上)容易造成卡顿。
- 回收量:freed 等字段表示本次回收了多少内存,可粗略判断回收是否有效。
若一段时间内 Full GC 非常频繁 或 paused 经常很大,需从泄漏、大对象、分配量等方面优化(见 4 节)。
5.4 MAT(Memory Analyzer Tool)分析 hprof
用于离线分析堆转储文件,精确定位大对象、引用链与泄漏路径。堆转储可从 Profiler 导出,或 OOM 时自动/手动抓取。
5.4.1 获取与转换 hprof
- 从 Profiler 导出:Memory 视图 → Heap Dump → Export 保存为
heap.hprof。 - Android 的 hprof 格式与标准 Java 略有不同,需用 SDK 自带的 hprof-conv 转换后 MAT 才能正确解析:
# 在 Android SDK 的 platform-tools 目录下执行
hprof-conv heap.hprof heap-converted.hprof
再用 MAT 打开 heap-converted.hprof。
5.4.2 常用视图与用法
| 视图 | 作用 | 典型用法 |
|---|---|---|
| Histogram | 按类统计实例数量与总占用(Retained Heap) | 按 Retained Heap 排序,找占用最大的类;双击类可看该类的所有实例 |
| Dominator Tree | 按对象看占用及“谁支配了谁” | 按 Retained Heap 排序,找占用最大的对象,展开看其引用的子对象 |
| Path to GC Roots | 从某对象反推到 GC Roots 的引用链 | 右键可疑对象 → Path to GC Roots → exclude weak/soft references,看是谁在强引用它(即泄漏路径) |
| Leak Suspects | MAT 自动生成的泄漏嫌疑报告 | 打开报告可快速看可能泄漏的点,再结合 Path to GC Roots 确认 |
5.4.3 分析泄漏的典型步骤
- 打开转换后的 hprof,等 MAT 解析完成。
- 看 Leak Suspects(若有),了解自动检测出的嫌疑。
- 在 Histogram 中搜索关心的类(如 Activity、Fragment、大业务对象),按 Retained Heap 排序,看实例数是否异常多(如 Activity 已销毁却仍存在多份)。
- 对可疑对象右键 → Path to GC Roots → exclude weak/soft references,得到从该对象到 GC Roots 的强引用链,即“谁持有了它”。
- 根据引用链回到代码:取消注册、清空静态/单例引用、改用 WeakReference 或在合适生命周期释放。
5.5 Systrace 看 GC 与卡顿
- 作用:录制各线程时间线(含主线程、GC 线程),看 GC 与主线程是否重叠、卡顿/掉帧是否与 GC 暂停重合。
- 用法:Android Studio Profile 或命令行抓取 System Trace,在 Perfetto/Chrome 中打开;关注主线程上的 GC 片段与 Choreographer/Frame 的对应关系。若卡顿帧与 GC 重叠,需减少 GC(少分配、避免 Full GC、查泄漏)。
5.6 LeakCanary 原理简述与使用
使用说明: 在模块的 build.gradle 中添加 debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12',仅 debug 构建会生效。无需额外代码,LeakCanary 会自动监控 Activity、Fragment 等;若检测到泄漏,会弹出通知并生成泄漏报告,报告中会给出从泄漏对象到 GC Roots 的引用链,便于直接定位到代码中的持有者。
核心思路
用 WeakReference(弱引用)+ ReferenceQueue(引用队列) 判断「本该被回收的对象是否真的被 GC 回收」。若没被回收,说明还有别的强引用在持有它 → 存在泄漏。
为什么弱引用能用来“测泄漏”
- 弱引用不阻止 GC:只要没有强引用指向对象,GC 时对象就会被回收。
- 对象被回收时,指向它的 WeakReference 会被 JVM 自动放进你传入的 ReferenceQueue。
- 因此:若一段时间后、且触发过 GC,发现这个 WeakReference 没进队列,且
ref.get()还不为 null → 说明对象没被回收 → 一定还有强引用在引用它 → 发生了泄漏。
引用队列(ReferenceQueue)是什么、怎么用
引用队列是什么
ReferenceQueue是 JDK 里的一个队列,专门和WeakReference、SoftReference等配合用。- 约定:创建
WeakReference(对象, queue)时绑上这个 queue,当那个对象被 GC 回收后,JVM 会把这个 WeakReference 本身(注意:不是被回收的对象)放进 queue。 - 也就是说:入队的是“引用对象”(WeakReference),不是 Activity。Activity 被回收后已经没了,我们只是通过「引用有没有入队」来推断「它指向的对象有没有被回收」。
谁入队、何时入队
- 入队的是 WeakReference 实例(我们创建的那个
ref)。 - 时机:在 GC 回收掉 Activity 之后,由 JVM 自动把
ref放入queue。我们不需要也不应该手动往 queue 里放东西。 - 因此:若某个
ref在 queue 里出现,说明 这个 ref 曾经指向的对象已经被回收了。
怎么用队列判断“有没有泄漏”
- 我们只创建一个
WeakReference(activity, queue),并记住这个ref。 - 触发 GC 后,用
queue.poll()(或轮询 queue)看队列里有没有出现我们这个 ref。- 出现了 → 说明 JVM 把我们的 ref 放进去了 → ref 指向的 Activity 已被回收 → 无泄漏。
- 没出现,并且
ref.get() != null→ Activity 还在 → 一定有强引用在别处持有它 → 泄漏。
简单代码理解
ReferenceQueue<Activity> queue = new ReferenceQueue<>();
WeakReference<Activity> ref = new WeakReference<>(activity, queue);
// 此后不再持有 activity 的强引用
// ... 等待几秒,触发 GC ...
// 若 activity 被回收了,ref 会被 JVM 放入 queue
Reference<?> polled = queue.poll();
if (polled != null && polled == ref) {
// 我们的 ref 入队了 → activity 已被回收 → 无泄漏
} else if (ref.get() != null) {
// ref 没入队,且 activity 还在 → 有强引用持有 → 泄漏
}
小结(引用队列): 队列里存的是「哪个 Reference 指向的对象已经被回收了」。我们的 ref 入队 = 对应对象已回收;ref 没入队且对象还在 = 泄漏。
LeakCanary 的检测流程(分步)
- 监控时机:通过
Application.registerActivityLifecycleCallbacks在 Activity.onDestroy() 时认为「这个 Activity 不该再被用了」。 - 创建弱引用 + 队列:用
WeakReference(activity, queue)包装该 Activity,不保留强引用。 - 等待 + 触发 GC:等几秒(给业务代码释放引用),再调
Runtime.getRuntime().gc()建议 GC。 - 检查队列:
queue.poll()看刚才的 WeakReference 是否入队。- 入队了 → Activity 已被回收 → 无泄漏。
- 没入队 且
ref.get() != null→ Activity 还在 → 有强引用持有 → 判定泄漏。
- 确认泄漏后:生成堆转储(heap dump),用 Shark 等分析「谁还在引用这个 Activity」(Path to GC Roots),得到泄漏引用链并通知开发者。
流程简图
sequenceDiagram
participant App
participant LC as LeakCanary
participant Q as RefQueue
participant GC
App->>LC: onDestroy
LC->>LC: WeakRef+Queue 不持强引用
LC->>LC: 等待后 gc
LC->>GC: gc
GC->>Q: 回收则Ref入队
LC->>Q: poll检查
alt 已入队
LC->>App: 无泄漏
else 未入队且get非空
LC->>LC: dump分析
LC->>App: 泄漏路径
end
小结: 弱引用不阻止回收;对象被回收后其 WeakReference 会入队;未入队且对象仍在 = 有强引用持有 = 泄漏。LeakCanary 据此判定泄漏并分析引用链。
5.7 常见问题排查思路
| 现象 | 排查方向 | 工具与手段 |
|---|---|---|
| GC 频繁 | 看谁在频繁分配、是否泄漏 | logcat 看 GC 频率;Profiler 分配跟踪 |
| 暂停时间长 | 是否多为 Full GC、堆是否过大 | logcat/Systrace;减少 Full GC、查泄漏 |
| 内存只升不降 | 泄漏或缓存过大 | Profiler 曲线;LeakCanary;MAT 看引用链 |
| OOM | 谁占得多、为何不回收 | 堆转储 + MAT Histogram/Dominator/Path to Roots |
| 卡顿 | 是否与 GC 重叠 | Systrace 看主线程与 GC;减少分配与 Full GC |
第六部分:面试题精选
按基础概念、算法与分代、实践与优化、工具与原理、设计题整理,答案可与前文对应章节对照。回答时可先说结论,再按点展开。
6.1 基础概念
Q:什么是 GC?为什么 Android 里特别重要?
答: GC(Garbage Collection)是自动识别并回收不可达对象、释放内存的机制。作用包括:减轻遗漏释放与泄漏、简化开发。在 Android 上特别重要是因为:设备内存有限、GC 会暂停用户线程,频繁或长时间 GC 会导致卡顿甚至 ANR,内存不足时应用可能被系统杀进程。
Q:Android 用什么方式判断对象可回收?为什么不用引用计数?
答: 使用可达性分析:从一组 GC Roots(栈、静态变量、常量、锁、JNI 等)出发,能到达的对象视为存活,不可达的视为可回收。不用引用计数是因为无法处理循环引用:两个对象互相引用时计数永不为 0,无法回收,而可达性分析下只要从 Roots 不可达就会被正确回收。
Q:GC Roots 具体有哪些?
答: 常见有:虚拟机栈帧中的局部变量与参数引用、方法区中静态变量与常量引用、本地方法栈中的 JNI 引用、被 synchronized 持有的对象、以及虚拟机内部引用(如 Class 对象、异常对象等)。面试时答“栈、静态、常量、锁、JNI”即可。
Q:强引用、软引用、弱引用、虚引用的区别?各用在什么场景?
答: 按强度从强到弱:强引用—存在就不会被回收,普通 new 出来的都是;软引用—内存紧张时才会被回收,可做缓存(Android 更常用 LruCache);弱引用—下次 GC 就可能被回收,适合“不阻止回收但需要时还能拿到”的场景,如 Handler 持 Activity、LeakCanary 检测泄漏;虚引用—最弱,多用于回收前做清理(如堆外内存),日常开发少见。回答时可以说:强度递减,弱引用常用来避免泄漏(如用 WeakReference 包装 Activity)。
Q:ART 和 JVM 在 GC 上有什么不同?
答: ART 只有一种收集器(并发复制 CC),面向低延迟、移动端;JVM 有多种收集器可选(如 G1、ZGC),可侧重吞吐或延迟。ART 采用 AOT 编译,JVM 多为 JIT。可以概括为:ART 单一收集器、为移动端优化;JVM 多方案、可配置。
Q:Partial GC、Full GC、Sticky GC 分别是什么?什么时候触发?
答: Partial GC(部分/年轻代回收):只收年轻代,Eden 满等触发,暂停短,对应前文的 Minor GC。Full GC(整堆回收):年轻代+老年代一起收,老年代满或 System.gc() 等触发,暂停长,应尽量避免。Sticky GC(粘性/局部年轻代):只清理最近新分配的那块,针对“分配很多、死得很快”的场景,暂停极短。面试重点:能说出三种的触发范围和暂停差异即可。
Q:年轻代和老年代是什么关系?晋升条件是什么?
答: 同属分代收集设计:年轻代包含 Eden + 两个 Survivor(S0/S1),用标记-复制;老年代独立一块,用标记-整理。Minor GC 时 Eden + 当前 From 的存活对象都进当前 To(S0/S1 之一),To 放不下则多出的直接晋升。晋升条件:Survivor(To)放不下,或对象年龄达阈值。老年代 ≠ Survivor,是单独区域。
6.2 算法与分代
Q:标记-清除、标记-复制、标记-整理各有什么特点?Android 里谁用在哪儿?
答: 标记-清除:先标记存活,再清除未标记;实现简单但易产生碎片。标记-复制:把存活对象复制到另一块再清空当前块;无碎片但占双倍空间,适合年轻代(对象朝生夕死,复制成本低)。标记-整理:标记后把存活对象向一端紧凑移动再清边界;无碎片、不浪费整块,但移动和更新引用成本高,适合老年代。Android 年轻代主用标记-复制,老年代用标记-整理。
Q:分代收集的思想是什么?为什么能减少停顿?
答: 思想是多数对象活不久:新对象放年轻代,用复制算法快速回收;熬过多次 GC 的进老年代,少动。这样大部分 GC 只扫年轻代(Partial GC),暂停短;只有老年代紧张时才 Full GC。通过“把很少变化的老对象和频繁生死的新对象分开处理”,整体减少停顿。
6.3 实践与优化
Q:如何减少 GC 频率、减轻卡顿?
答: 减少分配:对象池、复用、StringBuilder、基本类型代替包装类;列表用 ViewHolder 复用;避免在 onDraw、onBind、循环里大量 new。减少** Full GC**:防泄漏、及时释放、有界缓存(LruCache)、不主动调 System.gc()。卡顿多与 Full GC 或频繁 Partial GC 有关,所以“少分配 + 防泄漏”是核心。
Q:如何检测和解决内存泄漏?
答: 检测:LeakCanary 自动在 onDestroy 后判断对象是否被回收,并给出引用链;或 Profiler 抓堆转储,用 MAT 的 Path to GC Roots(排除弱/软引用)看谁在强引用。解决:单例/静态不持 Activity,用 Application 或 WeakReference;Handler、异步任务用静态内部类 + 弱引用,在 onDestroy 里 removeCallbacks/cancel;监听、广播、注册在适当时机解绑并置空引用。
Q:LeakCanary 是怎么检测泄漏的?(弱引用 + ReferenceQueue)
答: 用 WeakReference + ReferenceQueue:在 Activity onDestroy 时用 WeakReference(activity, queue) 包装,不保留强引用;等几秒后触发 GC;若对象该被回收,其 WeakReference 会被 JVM 放入 queue。若 ref 没入队且 ref.get() 还不为 null,说明还有强引用持有 → 判定泄漏;再 dump 堆、分析引用链给出报告。
Q:OOM 怎么排查?和内存泄漏有什么关系?
答: 排查:抓堆转储(OOM 时或 Profiler 主动抓)→ 用 hprof-conv 转成 MAT 可读格式 → MAT 里看 Histogram/Dominator Tree 找谁占得多,Path to GC Roots 看是否被强引用长期持有(泄漏)。关系:泄漏会导致对象无法回收,堆持续增长,最终容易 OOM;但 OOM 也可能只是瞬时分配过大(如大图、大数组),不一定有泄漏。要结合 dump 看是“谁占得多”和“是否该释放却没释放”。
Q:大对象、大图导致 OOM 怎么处理?
答: 大对象尽量少创建、复用或池化;大图用采样压缩(inSampleSize)、按需解码(BitmapRegionDecoder)、及时 recycle;列表里用缩略图 + 点击再加载大图。可配合 LruCache 控制缓存数量与总大小,避免无限堆积。
6.4 工具与原理
Q:Profiler、MAT、LeakCanary、logcat、Systrace 分别用来干什么?什么时候用哪个?
答: Profiler:看内存曲线、GC 事件、抓堆转储、Allocation Tracking 看谁在分配。MAT:分析 hprof,看对象数量、占用、引用链,精确定位泄漏路径。LeakCanary:自动检测 Activity/Fragment 等泄漏并给引用链,开发期首选。logcat:过滤 GC 日志看频率、暂停时间。Systrace:看主线程与 GC 时间线,判断卡顿是否和 GC 重叠。怀疑泄漏 → LeakCanary 或 Profiler + dump;要查“谁持有了谁” → MAT;看 GC 频率/暂停 → logcat;看卡顿是否和 GC 有关 → Systrace。
Q:MAT 里 Histogram、Dominator Tree、Path to GC Roots 怎么用?
答: Histogram:按类看实例数和 Retained Heap,找占用大的类。Dominator Tree:按对象看占用和支配关系,找大对象。Path to GC Roots:对可疑对象右键 → exclude weak/soft references,看从该对象到 GC Roots 的强引用链,即“谁在持有它”,用于定位泄漏。典型流程:Histogram/Dominator 找大或异常多的对象 → Path to GC Roots 看引用链 → 回代码改持有关系。
6.5 设计题
Q:对象池怎么设计?
答: 维护一个池(如 Queue 或 List),提供 acquire():池空则 new,否则从池里取;release(obj):把对象重置到可复用状态后放回池;并设最大池大小,超过则丢弃,防止泄漏或无限增长。注意线程安全(若多线程用)和对象重置是否彻底(避免残留上次状态)。
Q:如何设计一个高内存效率的应用?
答: 少分配:池化、复用、基本类型、ViewHolder、避免在热点路径大量 new。防泄漏:生命周期内注册/反注册、单例/静态不持 Activity、Handler/异步用弱引用并在 onDestroy 取消。有界缓存:LruCache 控制图片等缓存大小。大对象与资源:大图采样、及时 recycle、流/连接及时关闭。可观测:用 Profiler、LeakCanary、MAT 定期看内存与引用链,问题早发现早修。
Q:如果让你实现一个“带弱引用的缓存”,思路是什么?
答: 用 WeakReference 包装缓存对象,并配合 ReferenceQueue:当被缓存对象被 GC 回收时,对应 WeakReference 会入队,我们轮询或监听队列,把已入队的条目从缓存 Map 里移除,避免 Map 里堆积无效 key。这样既能在内存紧张时被回收,又能在未回收时命中缓存;需要强引用时由调用方在用时临时持有。
总结
- 基础: GC 自动回收不可达对象;Android 用可达性分析;强/软/弱/虚引用强度递减。
- 算法: 年轻代以标记-复制为主(Eden + 2 Survivor,S0/S1 轮流 From/To);Minor GC 时 Eden+From 存活进 To,To 放不下则多出的晋升;老年代标记-整理;整体为分代收集。
- ART: 仅一种并发复制收集器,分代、并发、增量、压缩等均为其特性;Partial/Full/Sticky 为触发类型。
- 问题: 泄漏导致无法回收;OOM 为堆超限;优化重点是少分配、防泄漏、控缓存与资源。
- 工具: Profiler 看内存与 GC;logcat 看日志;Systrace 看卡顿;MAT 分析 hprof;LeakCanary 自动查泄漏。