【Android 内存管理】JVM 内存模型与 GC 原理

文章目录

  • [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 中对象在堆中由三部分组成:

  1. 对象头(Header):Mark Word(哈希码、GC 分代年龄、锁状态)+ 类型指针(指向类元数据)+ 数组长度(数组对象才有)。
  2. 实例数据(Instance Data):字段值,按对齐规则排列。
  3. 对齐填充(Padding) :8 字节对齐,保证对象起始地址为 8 的倍数。
    Android ART 的对象布局类似,但使用 Compressed Oops(32 位压缩指针)在 64 位设备上节省内存,堆上限不超过 4GB 时默认开启。

1.4 Android 内存区域划分 ****

Java 堆(ART 虚拟机内存)进程整体虚拟内存

1.4.1 ART 虚拟机内存(Java 层)

  1. 堆 Heap
    所有 Java/Kotlin 对象、数组分配在这里。GC 主要管理这块内存。
  • 新生代(Young):新创建对象,存活时间短,频繁 Minor GC。
  • 老年代(Old):熬过多次 GC 的对象,大对象直接进入老年代,发生 Major GC。

注意:Android 没有永久代;类信息存放在 Image Space / Zygote Space / App Image

  1. Zygote Space(Zygote 堆)
    Zygote 进程孵化 App 时复制过来,存放系统预加载的类、资源。所有 App 进程共享只读部分,写时复制 COW。
  2. Image Space
    ART 预编译 oat 产生,存放预加载类、方法元数据,内存映射,提升启动速度。
  3. 线程栈(Thread Stack)
    每个 Java 线程拥有私有栈,存放局部变量、方法调用栈帧。栈上只存引用,对象在堆 ;栈内存固定大小,栈溢出会抛出StackOverflowError
  4. 方法区(元空间 Meta‑Space)
    存放 class 字节码元信息:类结构、方法、字段、常量。Android 8 + 使用元空间,属于 Native 内存,不在 Java 堆,不受 Heap 大小限制。
  5. 常量池
    字符串常量、字面量,一部分在元空间,字符串会驻留到堆。

1.4.2 Native 层内存(C/C++)

  1. Native Heap
    malloc / new 分配,Bitmap 像素缓冲区、自定义 Native 库、OpenGL 缓冲区都在这里。

重点面试点:Native 堆不受 Java Heap 限制 ,即使 Java 堆没 OOM,Native 堆耗尽同样会触发 App OOM。libart.so、第三方 so 库内存占用在这里。

  1. 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 而来,这带来两个关键内存特性:

  1. 写时复制(COW):fork 后子进程共享 Zygote 的只读页(框架类、资源),只有写入时才复制。这意味着系统类的元数据不占用每个 App 的独立内存。
  2. 预加载 :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),流程:

  1. 初始标记(STW):标记 GC Roots 直接引用的对象,停顿短。
  2. 并发标记:与用户线程并发,从 GC Roots 遍历所有可达对象。
  3. 并发预清理:处理并发标记期间引用变化的对象(Card Table)。
  4. 重新标记(STW):修正并发标记期间变动的对象,停顿较长。
  5. 并发清除:清除不可达对象。

问题: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 线程并发修改引用关系,可能导致存活对象被漏标。三色标记法将对象分为

  • 白色:未访问,可能可回收。
  • 灰色:已访问但其引用的对象尚未全部处理。
  • 黑色:已访问且所有引用对象都已处理。

漏标条件(必须同时满足)

  1. 黑色对象新增了对白色对象的引用。
  2. 灰色对象删除了对该白色对象的引用。

解决方案

  • 增量更新(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 内存优化最佳实践

  1. 避免在循环中创建对象:如 onDraw、onBindViewHolder 中避免 new 对象。
  2. 对象池化:频繁创建销毁的对象使用 Pool(如 Message.obtain())。
  3. SparseArray 替代 HashMap:key 为 int 时,SparseArray 避免自动装箱,内存更省。
  4. ArrayMap 替代 HashMap:小数据量(< 1000)时内存更优,但查找复杂度 O(logN)。
  5. 枚举优化:枚举比静态常量多占用约 2-3 倍内存,可用 @IntDef / @StringDef 替代。
  6. 避免过度绘制:减少布局层级,移除不必要背景,降低 GPU 内存与渲染开销。
  7. onTrimMemory 回调:实现 ComponentCallbacks2,根据内存等级释放缓存。

6. 内存分析工具

6.1 工具对比

6.2 内存泄漏排查流程

  1. 复现:反复进出可疑页面,回到桌面,触发 GC(Memory Profiler 中点击垃圾桶图标)。
  2. Dump 堆:点击 Dump Java Heap,生成 hprof 文件。
  3. 分析:按包名筛选,查看是否有已销毁的 Activity/Fragment 仍存活。
  4. 定位引用链:查看该对象的 GC Roots 引用链,找到泄漏源。
  5. 验证:修复后重复步骤 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 对象生命周期自动回收。

相关推荐
城管不管10 小时前
重生——第十一次面试之挖财一面2026.8.19已OC
java·服务器·jvm·数据库·spring·面试·职场和发展
夜雪一千13 小时前
MySQL 全局锁是什么?原理、风险、备份踩坑完整实战
android·mysql·adb
金銀銅鐵15 小时前
[Java] 一个方法最多可以有多少个入参?
java·jvm
哭哭啼15 小时前
JAVA服务问题诊断
java·开发语言·jvm
又见情义19 小时前
RK3568 Android 13 驱动适配实战:从零开始让SDK在自己的板子上跑起来
android·arm开发·驱动开发
霸道流氓气质20 小时前
MySQL 大表 DDL 变更 — 原理、方案与实践
android·数据库·mysql
明雨-开发21 小时前
Android打包时候Execution failed for task ‘:launcher:lintVitalAnalyzeRelease‘.
android
游戏开发爱好者821 小时前
开心上架是什么,一站式 Apple 开发者工作台总览
android·小程序·https·uni-app·iphone·webview
Android-Flutter21 小时前
android kotlin launch 源码分析
android·kotlin