浅析 Android 虚拟机中 Heap

79 阅读6分钟

在 Android 系统中,堆(Heap) 是 ART(Android Runtime,或早期 Dalvik 虚拟机)用于动态分配和管理 Java/Kotlin 对象的主要内存区域。堆的结构设计直接影响垃圾回收(GC)效率、内存分配性能和应用流畅度。


  1. Android 堆的结构与关键特性

Android 的堆是 ART 管理的连续内存区域,用于存储运行时创建的对象(如 Java/Kotlin 实例、数组等)。它通过分代和分区设计优化内存分配与垃圾回收,主要特性包括:

  • 分代结构:基于 Generational GC,堆分为 年轻代(Young Generation) 和 老年代(Old Generation),分别处理短生命周期和长期存活对象。
  • 大对象空间(Large Object Space, LOS):独立存储大对象(如 Bitmap、数组),避免频繁复制。
  • Zygote Space / Image Space:存储预加载的系统类和资源,通常不参与 GC。
  • 卡表(Card Table)与标记位图(Mark Bitmap):辅助 GC 的元数据结构,跟踪引用变化和存活对象。
  • 动态调整:堆大小和分代比例根据设备内存(通过 ActivityManager.getMemoryClass() 获取)动态调整,适配低端到高端设备。

  1. 堆的逻辑组织

Android 堆的逻辑结构可以分为以下几个主要区域:

2.1 年轻代(Young Generation)

  • 作用:存储新分配的对象,通常生命周期短(如 UI 临时对象、局部变量)。

  • 子区域:

    • Eden 区:新对象首先分配在此,占年轻代大部分空间。
    • Survivor 区(From 和 To):存活对象在 Minor GC 中复制到 Survivor 区,轮换使用。
  • 算法:使用 Copying(复制算法),快速回收,暂停时间短(1-3ms)。

  • 典型对象:RecyclerView 的 ViewHolder、SpannableString 等。

2.2 老年代(Old Generation)

  • 作用:存储经过多次 GC 仍存活的对象(如单例、缓存)。
  • 算法:使用 Concurrent Mark-Sweep(CMS) 或 Mark-Compact,回收频率低但暂停时间较长(10-100ms)。
  • 典型对象:全局配置、Activity 实例(若泄漏)。

2.3 大对象空间(LOS)

  • 作用:存储大对象(如 Bitmap、大数组),避免频繁复制。
  • 算法:使用 Mark-Compact,解决碎片问题。
  • 典型对象:高清图片、视频缓冲区。

2.4 Zygote Space / Image Space

  • 作用:存储系统启动时预加载的类、资源和库(如 Java 标准库)。
  • 特性:通常不参与 GC,减少应用启动时间和内存占用。
  • 典型内容:java.lang.String、Android Framework 类。

2.5 元数据区域

  • 卡表(Card Table):字节数组,记录堆中引用修改的卡页(128 字节),支持 CMS 和跨代引用跟踪。
  • 标记位图(Mark Bitmap):记录存活对象,辅助 GC 标记阶段。
  • 其他:如空闲列表(Free List),管理未分配内存。

  1. 堆的“样子”:逻辑与物理视图

由于堆是内存中的逻辑结构,无法直接“看”到,但我们可以通过文字模拟其逻辑布局和物理分配的“样子”。以下是堆的简化表示,结合 ART 的分代和分区设计:

逻辑视图(分代结构)

[Android Heap]
┌─────────────────────────────────────────────────────────────┐
│ Zygote Space / Image Space (预加载类/资源, 不参与 GC)       │
│ [java.lang.String, android.app.Activity, ...]               │
├─────────────────────────────────────────────────────────────┤
│ Young Generation (年轻代, 复制算法)                         │
│ ┌──────────────────┐ ┌─────────────┐ ┌─────────────┐         │
│ │ Eden            │ │ From Surv.  │ │ To Surv.    │         │
│ │ [new objects]   │ │ [survivors] │ │ [empty]     │         │
│ └──────────────────┘ └─────────────┘ └─────────────┘         │
├─────────────────────────────────────────────────────────────┤
│ Old Generation (老年代, CMS/Mark-Compact)                    │
│ [long-lived objects: singletons, caches, ...]               │
├─────────────────────────────────────────────────────────────┤
│ Large Object Space (LOS, Mark-Compact)                       │
│ [Bitmaps, large arrays, ...]                                │
├─────────────────────────────────────────────────────────────┤
│ Metadata (卡表, Mark Bitmap, 空闲列表)                      │
│ [Card Table: byte array for dirty cards]                    │
│ [Mark Bitmap: tracks live objects]                          │
└─────────────────────────────────────────────────────────────┘

物理视图(内存分配)

堆在物理内存中是连续的,ART 通过指针和偏移管理不同区域。以下是简化的内存布局:

[Memory Address: 0x10000000 - 0x1FFFFFFF]
0x10000000: [Zygote Space: system classes, resources]
0x12000000: [Young Gen: Eden | From Survivor | To Survivor]
0x14000000: [Old Gen: long-lived objects]
0x16000000: [LOS: large objects]
0x18000000: [Metadata: Card Table, Mark Bitmap]
  • 比例:年轻代通常占堆的 1/8,老年代占大部分,LOS 按需分配。
  • 动态调整:ART 根据设备内存(512MB 到数 GB)调整堆大小和分代比例。
  • 碎片:老年代和 LOS 可能产生碎片,需通过 Mark-Compact 整理。

  1. 堆在实际开发中的表现

堆的结构直接影响 Android 应用的内存管理和性能。以下是堆在开发中的“样子”和影响:

4.1 通过工具观察堆

  • Android Studio Profiler:

    • 显示堆的分配情况(Eden、Survivor、老年代、LOS)。
    • 提供 Allocation Tracking,定位高频分配对象。
    • 示例:快速滑动 RecyclerView 时,Eden 区快速填满,触发 Minor GC。
  • adb shell dumpsys meminfo:

    • 输出堆使用情况,如:

      Total PSS by category:
          100 MB: Java Heap (Young + Old + LOS)
           20 MB: Zygote
            5 MB: Native Heap
      
    • 显示年轻代、老年代和 LOS 的占用比例。

  • LeakCanary:

    • 检测堆中的内存泄漏,定位老年代中的长期存活对象。

4.2 典型场景

  • 高频分配:RecyclerView 创建大量临时 ViewHolder,填满 Eden 区,触发 Minor GC(1-3ms)。
  • 大对象:加载高清 Bitmap 到 LOS,频繁分配可能触发 Full GC(10-100ms)。
  • 内存泄漏:Activity 未释放晋升到老年代,增加 CMS 或 Mark-Compact 压力。
  • 低端设备:堆大小受限(<100MB),年轻代比例小,GC 更频繁。

  1. 开发实践与优化

堆的结构直接影响 GC 性能和应用流畅度,以下是基于堆结构的优化建议:

5.1 减少年轻代压力

  • 优化对象分配:使用对象池(如 Message.obtain())或缓存(如 Glide)减少 Eden 区分配。
  • 案例:聊天应用中,缓存 SpannableString 结果,降低 Minor GC 频率。

5.2 管理老年代

  • 避免泄漏:使用 LeakCanary 检测长期存活对象,清理静态引用或监听器。
  • 弱引用:对缓存使用 WeakReference,减少老年代占用。
  • 案例:游戏应用中,使用 SoftReference 包装 UI 缓存,降低 Full GC 频率。

5.3 优化大对象

  • 复用内存:通过 BitmapFactory.Options.inBitmap 复用 Bitmap。
  • 异步加载:将大对象分配移到后台线程,避免主线程 GC 暂停。
  • 案例:图片编辑器预分配 Bitmap 池,减少 LOS 的 Mark-Compact。

5.4 低端设备适配

  • 动态调整:通过 ActivityManager.getMemoryClass() 检测堆大小,减少分配。
  • 降级策略:降低图片分辨率或禁用复杂动画,减少堆压力。
  • 案例:地图应用延迟加载瓦片,减少 Eden 区和 LOS 占用。

  1. 堆的“样子”在运行时的动态变化

堆的结构是动态的,随应用运行和 GC 触发不断变化。以下是运行时堆的典型“快照”:

  • 初始状态:Eden 区逐渐填满,Survivor 区为空,老年代和 LOS 占用少。
  • Minor GC 后:Eden 区清空,存活对象移到 To Survivor,部分对象晋升到老年代。
  • Full GC 后:老年代和 LOS 回收未标记对象,可能通过 Mark-Compact 整理碎片。
  • 高负载场景:快速滑动列表或加载大图时,Eden 和 LOS 占用激增,GC 频率上升。

  1. 总结

Android 的堆是一个分层、分区的内存结构,包含:

  • 年轻代(Eden + Survivor,复制算法,处理短生命周期对象)。
  • 老年代(CMS/Mark-Compact,处理长期存活对象)。
  • LOS(Mark-Compact,处理大对象)。
  • Zygote Space(不参与 GC,存储系统资源)。
  • 元数据(卡表、Mark Bitmap,辅助 GC)。

堆通过分代设计和动态调整,平衡内存效率和实时性。开发者可通过 Profiler、LeakCanary 和 dumpsys meminfo 观察堆状态,优化对象分配、减少泄漏和管理大对象,以提升应用性能和用户体验。