文章目录
- [1. JVM 内存模型](#1. JVM 内存模型)
-
- [1.1 运行时数据区总览](#1.1 运行时数据区总览)
- [1.2 堆内存分代](#1.2 堆内存分代)
- [1.3 对象的内存布局](#1.3 对象的内存布局)
- [1.4 Android 内存区域划分 ****](#1.4 Android 内存区域划分 ****)
-
- [1.4.1 ART 虚拟机内存(Java 层)](#1.4.1 ART 虚拟机内存(Java 层))
- [1.4.2 Native 层内存(C/C++)](#1.4.2 Native 层内存(C/C++))
- [2. Android 运行时内存特殊性](#2. Android 运行时内存特殊性)
-
- [2.1 Dalvik 与 ART 的核心差异](#2.1 Dalvik 与 ART 的核心差异)
- [2.2 Android 堆大小限制](#2.2 Android 堆大小限制)
- [2.3 Zygote 与共享内存](#2.3 Zygote 与共享内存)
- [2.4 Native 内存与 Java 堆的关系](#2.4 Native 内存与 Java 堆的关系)
- [3. GC 算法基础](#3. GC 算法基础)
-
- [3.1 如何判断对象可回收](#3.1 如何判断对象可回收)
-
- [3.1.1 引用计数法](#3.1.1 引用计数法)
- [3.1.2 可达性分析(根搜索算法)](#3.1.2 可达性分析(根搜索算法))
- [3.2 四种引用类型](#3.2 四种引用类型)
- [3.3 经典 GC 算法](#3.3 经典 GC 算法)
-
- [3.3.1 标记-清除(Mark-Sweep)](#3.3.1 标记-清除(Mark-Sweep))
- [3.3.2 复制算法(Copying)](#3.3.2 复制算法(Copying))
- [3.3.3 标记-整理(Mark-Compact)](#3.3.3 标记-整理(Mark-Compact))
- [3.3.4 分代收集](#3.3.4 分代收集)
- [4. Android GC 演进与实现](#4. Android GC 演进与实现)
-
- [4.1 Dalvik GC](#4.1 Dalvik GC)
- [4.2 ART Concurrent Copying(CC)](#4.2 ART Concurrent Copying(CC))
-
- [4.2.1 CC 的 GC 触发时机](#4.2.1 CC 的 GC 触发时机)
- [4.3 Android 13+ 分代 CC](#4.3 Android 13+ 分代 CC)
- [4.4 三色标记与 SATB](#4.4 三色标记与 SATB)
- [5. 内存泄漏与优化](#5. 内存泄漏与优化)
-
- [5.1 Android 常见内存泄漏场景](#5.1 Android 常见内存泄漏场景)
- [5.2 Bitmap 内存优化](#5.2 Bitmap 内存优化)
- [5.3 内存优化最佳实践](#5.3 内存优化最佳实践)
- [6. 内存分析工具](#6. 内存分析工具)
-
- [6.1 工具对比](#6.1 工具对比)
- [6.2 内存泄漏排查流程](#6.2 内存泄漏排查流程)
- [6.3 内存抖动](#6.3 内存抖动)
- [6.4 Android 8.0 前后 Bitmap 内存分配有什么变化?](#6.4 Android 8.0 前后 Bitmap 内存分配有什么变化?)
1. JVM 内存模型
1.1 运行时数据区总览
JVM 运行时数据区分为线程私有与线程共享两大类,这是所有内存问题的基础心智模型。

1.2 堆内存分代
传统 HotSpot 堆采用分代假设:弱分代假设(多数对象朝生夕死)与强分代假设(熬过多次 GC 的对象难以消亡)。
- 新生代(Young Gen):Eden + 两个 Survivor(From / To),默认比例 8:1:1。新对象优先在 Eden 分配,大对象直接进入老年代。
- 老年代(Old Gen):长期存活对象、大对象、担保失败对象。
- 永久代(Perm Gen):JDK 7 及之前存放类元数据,JDK 8 被元空间(Metaspace)取代,使用本地内存而非堆内存。
Android 的 Dalvik/ART 不采用 HotSpot 的分代堆结构(早期 ART 也没有分代),直到 Android 13 才引入分代 CC。面试时切勿直接套用 HotSpot 分代模型回答 Android GC。
1.3 对象的内存布局
HotSpot 中对象在堆中由三部分组成:
- 对象头(Header):Mark Word(哈希码、GC 分代年龄、锁状态)+ 类型指针(指向类元数据)+ 数组长度(数组对象才有)。
- 实例数据(Instance Data):字段值,按对齐规则排列。
- 对齐填充(Padding) :8 字节对齐,保证对象起始地址为 8 的倍数。
Android ART 的对象布局类似,但使用 Compressed Oops(32 位压缩指针)在 64 位设备上节省内存,堆上限不超过 4GB 时默认开启。
1.4 Android 内存区域划分 ****
Java 堆(ART 虚拟机内存) 和 进程整体虚拟内存
1.4.1 ART 虚拟机内存(Java 层)
- 堆 Heap
所有 Java/Kotlin 对象、数组分配在这里。GC 主要管理这块内存。
- 新生代(Young):新创建对象,存活时间短,频繁 Minor GC。
- 老年代(Old):熬过多次 GC 的对象,大对象直接进入老年代,发生 Major GC。
注意:Android 没有永久代;类信息存放在 Image Space / Zygote Space / App Image。
- Zygote Space(Zygote 堆)
Zygote 进程孵化 App 时复制过来,存放系统预加载的类、资源。所有 App 进程共享只读部分,写时复制 COW。 - Image Space
ART 预编译 oat 产生,存放预加载类、方法元数据,内存映射,提升启动速度。 - 线程栈(Thread Stack)
每个 Java 线程拥有私有栈,存放局部变量、方法调用栈帧。栈上只存引用,对象在堆 ;栈内存固定大小,栈溢出会抛出StackOverflowError。 - 方法区(元空间 Meta‑Space)
存放 class 字节码元信息:类结构、方法、字段、常量。Android 8 + 使用元空间,属于 Native 内存,不在 Java 堆,不受 Heap 大小限制。 - 常量池
字符串常量、字面量,一部分在元空间,字符串会驻留到堆。
1.4.2 Native 层内存(C/C++)
- Native Heap
malloc / new分配,Bitmap 像素缓冲区、自定义 Native 库、OpenGL 缓冲区都在这里。
重点面试点:Native 堆不受 Java Heap 限制 ,即使 Java 堆没 OOM,Native 堆耗尽同样会触发 App OOM。
libart.so、第三方 so 库内存占用在这里。
- Native 线程栈
native 线程私有栈。
2. Android 运行时内存特殊性
2.1 Dalvik 与 ART 的核心差异

2.2 Android 堆大小限制
每个 App 进程的堆大小由系统属性控制,而非 JVM 的 -Xmx:
- dalvik.vm.heapsize:默认堆上限,普通设备通常 192MB / 256MB / 384MB。
- largeHeap:Manifest 中声明 android:largeHeap="true" 可申请更大堆(dalvik.vm.largeheap),通常 512MB,但不保证所有设备都生效,且会增加 GC 压力。
- 前台/后台差异:后台进程的堆上限可能被收紧,系统通过 setDalvikHeapLimit 动态调整。
kotlin
<application
android:largeHeap="true"
android:hardwareAccelerated="true">
</application>
2.3 Zygote 与共享内存
Android 所有 App 进程由 Zygote fork 而来,这带来两个关键内存特性:
- 写时复制(COW):fork 后子进程共享 Zygote 的只读页(框架类、资源),只有写入时才复制。这意味着系统类的元数据不占用每个 App 的独立内存。
- 预加载 :Zygote 预加载常用类和资源,fork 后子进程直接共享,降低启动内存与时间。
面试延伸:为什么 Android 不直接用 JVM? 因为移动设备内存受限,Dalvik/ART 针对寄存器架构优化,dex 比 class 更紧凑,且 Zygote 的 COW 机制能大幅降低多进程内存开销。
2.4 Native 内存与 Java 堆的关系
Android 的内存压力不仅来自 Java 堆,还包括:
- Graphics 内存:Bitmap 在 Android 3.0 前存放在 Native 堆,3.0+ 移至 Java 堆;Android 8.0+ 又将 Bitmap 像素数据移回 Native(HardwareBuffer),Java 堆只存引用。
- Native 堆:so 库分配的内存,不受 Java 堆上限限制,但受进程总虚拟内存限制(32 位进程约 3-4GB)。
- 代码映射:dex、oat、so 的 mmap 映射内存。
- Stack:每个线程默认 1MB 栈空间,线程数过多会导致虚拟内存耗尽。
Android 8.0+ Bitmap 像素存 Native 的好处:Java 堆 OOM 概率降低,但 Native 内存泄漏更难排查,需用 Perfetto / heapprofd 定位。
3. GC 算法基础
3.1 如何判断对象可回收
3.1.1 引用计数法
每个对象维护引用计数器,引用 +1,失效 -1,计数为 0 即回收。无法解决循环引用,Python 采用但需配合分代回收。JVM 不采用。
3.1.2 可达性分析(根搜索算法)
从 GC Roots 出发,遍历引用链,不可达的对象判定为可回收。这是 JVM / ART 采用的算法
GC Roots 包括:
- 虚拟机栈中引用的对象(局部变量)
- 方法区中静态变量引用的对象
- 方法区中常量引用的对象
- 本地方法栈中 JNI 引用的对象
- 活跃线程本身
- ART 特有的:Image Roots(boot.art 中的对象)、Thread Roots、Interned Strings
3.2 四种引用类型

3.3 经典 GC 算法
3.3.1 标记-清除(Mark-Sweep)
标记所有可达对象,清除未标记对象。缺点:产生内存碎片,分配大对象时可能提前触发 GC。
3.3.2 复制算法(Copying)
将存活对象复制到另一块区域,清空原区域。优点:无碎片,分配快;缺点:空间利用率低(50%)。适用于新生代(对象存活率低)。
3.3.3 标记-整理(Mark-Compact)
标记后将存活对象向一端移动,清理边界外内存。优点:无碎片;缺点:移动对象开销大,需 STW。适用于老年代。
3.3.4 分代收集
结合上述算法:新生代用复制(Minor GC),老年代用标记-清除或标记-整理(Major GC / Full GC)
4. Android GC 演进与实现

4.1 Dalvik GC
Dalvik 使用 Concurrent Mark-Sweep(CMS),流程:
- 初始标记(STW):标记 GC Roots 直接引用的对象,停顿短。
- 并发标记:与用户线程并发,从 GC Roots 遍历所有可达对象。
- 并发预清理:处理并发标记期间引用变化的对象(Card Table)。
- 重新标记(STW):修正并发标记期间变动的对象,停顿较长。
- 并发清除:清除不可达对象。
问题:CMS 不压缩,堆碎片化严重 ;大对象分配失败时退化为 Full GC(全部 STW),卡顿明显。
4.2 ART Concurrent Copying(CC)
Android 8.0+ 默认使用 Concurrent Copying(CC),这是 Android GC 的核心面试点。
核心思想 :将堆分为多个 Region,GC 时把存活对象从 From-Space 复制到 To-Space,利用读屏障(Read Barrier)保证并发复制时用户线程总能读到正确对象。
关键机制:
- Region 化堆:堆被划分为固定大小的 Region(通常 256KB),对象按大小分配到不同 Region。
- baker 读屏障:每次对象引用读取时检查是否已被复制,若在 From-Space 则转发到 To-Space。通过字节码插桩或硬件支持实现。
- 无 STW 复制:整个复制过程并发执行,只有 GC 开始和结束时短暂 STW(通常 < 2ms),且停顿时间与堆大小、存活对象数无关。
- 无碎片:复制过程天然压缩,消除内存碎片。
4.2.1 CC 的 GC 触发时机
- Concurrent GC:堆使用量达到水位线(如 25% / 45%)时触发,后台并发执行。
- Alloc GC:分配对象时空间不足,触发同步 GC(可能 STW)。
- Native allocation fails:Native 分配失败时触发。
- Explicit GC:调用 System.gc()(ART 默认忽略,除非设置 -XX:+DisableExplicitGC 为 false)。
4.3 Android 13+ 分代 CC
Android 13(Tiramisu)引入分代 Concurrent Copying(Generational CC),将堆分为年轻代和老年代:
- 年轻代 GC:只扫描年轻代 Region,频率高但范围小,停顿更短。
- 老年代 GC:全堆扫描,频率低。
- 跨代引用:通过写屏障(Write Barrier)+ Card Table 记录老年代对年轻代的引用,避免年轻代 GC 时扫描全堆。
这使得 Android GC 终于与 HotSpot 的分代思想对齐,但实现仍是 CC 而非传统的分代复制/标记整理。
4.4 三色标记与 SATB
并发标记的核心难题是用户线程与 GC 线程并发修改引用关系,可能导致存活对象被漏标。三色标记法将对象分为:
- 白色:未访问,可能可回收。
- 灰色:已访问但其引用的对象尚未全部处理。
- 黑色:已访问且所有引用对象都已处理。
漏标条件(必须同时满足):
- 黑色对象新增了对白色对象的引用。
- 灰色对象删除了对该白色对象的引用。
解决方案:
- 增量更新(Incremental Update):黑色对象新增引用时,将其变回灰色(写屏障)。CMS 采用。
- SATB(Snapshot At The Beginning):GC 开始时保存对象图快照,删除引用时记录旧引用到 GC 栈,确保快照中存活的对象最终都被标记。G1 采用。
5. 内存泄漏与优化
5.1 Android 常见内存泄漏场景

5.2 Bitmap 内存优化
- inSampleSize:采样率压缩,按 2 的幂次缩放,避免 OOM。
- inBitmap:复用已分配的 Bitmap 内存,减少 GC 频率(Android 3.0+ 支持,4.4+ 支持不同尺寸复用)。
- RGB_565:不透明图片用 2 字节/像素替代 ARGB_8888 的 4 字节/像素,内存减半。
- recycle():Android 8.0 前手动释放 Native 像素内存;8.0+ 像素在 Native,由 GC 管理,但仍建议在不再使用时 recycle。
- Glide / Coil:使用成熟图片库,内置内存缓存、复用、生命周期管理。
5.3 内存优化最佳实践
- 避免在循环中创建对象:如 onDraw、onBindViewHolder 中避免 new 对象。
- 对象池化:频繁创建销毁的对象使用 Pool(如 Message.obtain())。
- SparseArray 替代 HashMap:key 为 int 时,SparseArray 避免自动装箱,内存更省。
- ArrayMap 替代 HashMap:小数据量(< 1000)时内存更优,但查找复杂度 O(logN)。
- 枚举优化:枚举比静态常量多占用约 2-3 倍内存,可用 @IntDef / @StringDef 替代。
- 避免过度绘制:减少布局层级,移除不必要背景,降低 GPU 内存与渲染开销。
- onTrimMemory 回调:实现 ComponentCallbacks2,根据内存等级释放缓存。
6. 内存分析工具
6.1 工具对比

6.2 内存泄漏排查流程
- 复现:反复进出可疑页面,回到桌面,触发 GC(Memory Profiler 中点击垃圾桶图标)。
- Dump 堆:点击 Dump Java Heap,生成 hprof 文件。
- 分析:按包名筛选,查看是否有已销毁的 Activity/Fragment 仍存活。
- 定位引用链:查看该对象的 GC Roots 引用链,找到泄漏源。
- 验证:修复后重复步骤 1-3,确认对象已被回收。
6.3 内存抖动
答:内存抖动指短时间内大量对象被创建又立即回收,导致 GC 频繁触发。常见于 onDraw 中创建对象、循环中 new 对象、字符串拼接。排查用 Memory Profiler 的Allocation Tracker(分配追踪),查看高频分配的对象类型和调用栈,将临时对象移到循环外或使用对象池。
6.4 Android 8.0 前后 Bitmap 内存分配有什么变化?
Android 2.3.3 及以下,Bitmap 像素数据存 Native 堆 ,需手动 recycle();
Android 3.0-7.1,像素数据存 Java 堆 ,随对象 GC 回收;
Android 8.0+,像素数据又移回 Native (通过 HardwareBuffer / ashmem),Java 堆只存 Bitmap 对象引用和少量元数据。好处是降低 Java 堆 OOM 概率,Native 内存由 NativeAllocationRegistry 关联 Java 对象生命周期自动回收。