Android内存管理

120 阅读10分钟

The Android Runtime(ART) 和Dalvik 虚拟机使用pagin 和memory-mapping(mmapping) 来管理内存,从应用程序释放内存的唯一方法是释放该应用程序持有的对象引用,从而使内存可用于垃圾回收器。

Android中有Native Heap和Dalvik Heap。Android的Native Heap言理论上可分配的空间取决了硬件RAM,而对于每个进程的Dalvik Heap都是有大小限制的

随机存取存储器 (RAM) 在任何软件开发环境中都是一项宝贵资源.虽然 Android 运行时 (ART) 和 Dalvik 虚拟机都执行例行的垃圾回收任务,但这并不意味着您可以忽略应用分配和释放内存的位置和时间。您仍然需要避免引入内存泄漏问题(通常因在静态成员变量中保留对象引用而引起),并在适当时间(如生命周期回调所定义)释放所有 Reference 对象。

Heap 和 Stack区别

  • Stack: 有序,先进后出,用于静态的内存分配
  • Heap: 无序,用于动态的内存分配

谨愼使用服务

注意在服务完成任务后使其停止运行。否则,您可能会在无意中导致内存泄漏。

在您启动某项服务后,系统更倾向于让此服务的进程始终保持运行状态。这种行为会导致服务进程代价十分高昂,因为一旦服务使用了某部分 RAM,那么这部分 RAM 就不再可供其他进程使用。这会减少系统可以在 LRU 缓存中保留的缓存进程数量,从而降低应用切换效率。当内存紧张,并且系统无法维护足够的进程以托管当前运行的所有服务时,这甚至可能导致系统出现颠簸。

您通常应该避免使用持久性服务,因为它们会对可用内存提出持续性的要求。我们建议您采用 JobScheduler 等替代实现方式。
如果您必须使用某项服务,则限制此服务的生命周期的最佳方式是使用 IntentService,它会在处理完启动它的 intent 后立即自行结束。有关详情,请参阅在后台服务中运行。

使用经过优化的数据容器

编程语言所提供的部分类并未针对移动设备做出优化。例如,常规 HashMap 实现的内存效率可能十分低下,因为每个映射都需要分别对应一个单独的条目对象。
Android 框架包含几个经过优化的数据容器,包括 SparseArray、SparseBooleanArray 和 LongSparseArray。  例如,SparseArray 类的效率更高,因为它们可以避免系统需要对键(有时还对值)进行自动装箱(这会为每个条目分别再创建 1-2 个对象)。
如果需要,您可以随时切换到原始数组以获得非常精简的数据结构。

  • 针对序列化数据使用精简版 Protobuf
  • 避免内存抖动
    您可以在 for 循环中分配多个临时对象。或者,您也可以在视图的 onDraw() 函数中创建新的 Paint 或 Bitmap 对象。在这两种情况下,应用都会快速创建大量对象。这些操作可以快速消耗新生代 (young generation) 区域中的所有可用内存,从而迫使垃圾回收事件发生。
  • 移除会占用大量内存的资源和库
  • 缩减总体 APK 大小
  • 使用 Dagger 2 实现依赖注入
    依赖注入框架可以简化您编写的代码,并提供一个可供您进行测试及其他配置更改的自适应环境。

谨慎使用外部库

外部库代码通常不是针对移动环境编写的,在移动客户端上运行时可能效率低下。如果您决定使用外部库,则可能需要针对移动设备优化该库。在决定是否使用该库之前,请提前规划,并在代码大小和 RAM 消耗量方面对库进行分析

限制应用内存

为了维持多任务环境的正常运行,Android 会为每个应用的堆大小设置硬性上限。不同设备的确切堆大小上限取决于设备的总体可用 RAM 大小。如果您的应用在达到堆容量上限后尝试分配更多内存,则可能会收到 OutOfMemoryError。
在某些情况下,例如,为了确定在缓存中保存多少数据比较安全,您可能需要查询系统以确定当前设备上确切可用的堆空间大小。您可以通过调用 getMemoryClass() 向系统查询此数值。此方法返回一个整数,表示应用堆的可用兆字节数。

内存泄漏排查

  • 使用 LeakCanary工具
    Leakcanary是由Square公司开源的一款轻量的第三方检测内存泄露的工具
    Leakcanary原理: 在一个Activity执行完onDestroy()之后,将它放入WeakReference中,然后将这个WeakReference类型的Activity对象与ReferenceQueque关联。这时再从ReferenceQueque中查看是否有没有该对象,如果没有,执行gc,再次查看,还是没有的话则判断发生内存泄露了。最后用HAHA这个开源库去分析dump之后的heap内存。
  • 使用 Memory Profiler (debug 版本的app)
  • 使用dumpsys工具(release 版本的app)
    adb shell dumpsys meminfo com.globalegrow.app.gearbest
  • Logcat 查看GC事件

Dalvik 日志消息
每个 GC 都会将以下信息输出到 logcat 中:
D/dalvikvm(PID): GC_Reason Amount_freed, Heap_stats, External_memory_stats, Pause_time
示例:
D/dalvikvm( 9050): GC_CONCURRENT freed 2049K, 65% free 3571K/9991K, external 4703K/5261K, paused 2ms+2ms

什么触发了 GC 以及是哪种回收。可能的原因包括:

  • GC_CONCURRENT
    在您的堆开始占用内存时释放内存的并发 GC。
  • GC_FOR_MALLOC
    您的堆已满而系统不得不停止您的应用并回收内存时,您的应用尝试分配内存而引起的 GC。
  • GC_HPROF_DUMP_HEAP
    当您请求创建 HPROF 文件来分析堆时发生的 GC。
  • GC_EXPLICIT
    显式 GC,例如当您调用 gc() 时(您应避免调用它,而应信任 GC 会根据需要运行)。
  • GC_EXTERNAL_ALLOC
    这仅适用于 API 级别 10 及更低级别(更新的版本会在 Dalvik 堆中分配任何内存)。外部分配内存的 GC(例如存储在原生内存或 NIO 字节缓冲区中的像素数据)。

ART 日志消息

与 Dalvik 不同,ART 不会为未明确请求的 GC 记录消息。只有在系统认为 GC 速度较慢时才会输出 GC 消息。更确切地说,仅在 GC 暂停时间超过 5 毫秒或 GC 持续时间超过 100 毫秒时。如果应用未处于可察觉到暂停的状态(例如应用在后台运行时,这种情况下,用户无法察觉 GC 暂停),则其所有 GC 都不会被视为速度较慢。系统一直会记录显式 GC。
ART 会在其垃圾回收日志消息中包含以下信息:
I/art: GC_Reason GC_Name Objects_freed(Size_freed) AllocSpace Objects,
Large_objects_freed(Large_object_size_freed) Heap_stats LOS objects, Pause_time(s)
示例:
I/art : Explicit concurrent mark sweep GC freed 104710(7MB) AllocSpace objects,
21(416KB) LOS objects, 33% free, 25MB/38MB, paused 1.230ms total 67.216ms

GC 原因
什么触发了 GC 以及是哪种回收。可能的原因包括:

  • Concurrent
    不会暂停应用线程的并发 GC。此 GC 在后台线程中运行,而且不会阻止分配。
  • Alloc
    您的应用在堆已满时尝试分配内存而引起的 GC。在这种情况下,垃圾回收在分配线程中发生。
  • Explicit
    由应用明确请求的垃圾回收,例如,通过调用 gc() 或 gc()。与 Dalvik 一样,在 ART 中,最佳做法是您信任 GC 并避免请求显式 GC(如果可能)。不建议请求显式 GC,因为它们会阻止分配线程并不必要地浪费 CPU 周期。此外,如果显式 GC 导致其他线程被抢占,则也可能会导致卡顿(应用出现卡顿、抖动或暂停)。
  • NativeAlloc
    原生分配(例如位图或 RenderScript 分配对象)导致出现原生内存压力,进而引起的回收。
  • CollectorTransition
    由堆转换引起的回收;这由在运行时变更 GC 策略引起(例如应用在可察觉到暂停的状态之间切换时)。回收器转换包括将所有对象从空闲列表空间复制到碰撞指针空间(反之亦然)。
    回收器转换仅在以下情况下出现:在 Android 8.0 之前的低内存设备上,应用将进程状态从可察觉到暂停的状态(例如应用在前台运行时,这种情况下,用户可以察觉 GC 暂停)更改为察觉不到暂停的状态(反之亦然)。
  • HomogeneousSpaceCompact
    同构空间压缩是空闲列表空间到空闲列表空间压缩,通常在应用进入到察觉不到暂停的进程状态时发生。这样做的主要原因是减少内存使用量并对堆进行碎片整理。
  • DisableMovingGc
    这不是真正的 GC 原因,但请注意,由于在发生并发堆压缩时使用了 GetPrimitiveArrayCritical,回收遭到阻止。一般情况下,强烈建议不要使用 GetPrimitiveArrayCritical,因为它在移动回收器方面存在限制。
  • HeapTrim
    这不是 GC 原因,但请注意,在堆修剪完成之前,回收会一直受到阻止

memory profile含义解释

内存分析中用到的字段的函义

  • Allocations:堆中的分配数。
  • Native Size:此对象类型使用的原生内存总量(以字节为单位)。只有在使用 Android 7.0 及更高版本时,才会看到此列。
    您会在此处看到采用 Java 分配的某些对象的内存,因为 Android 对某些框架类(如 Bitmap)使用原生内存。
  • Shallow Size:此对象类型使用的 Java 内存总量(以字节为单位)。
  • Retained Size:为此类的所有实例而保留的内存总大小(以字节为单位)。
  • image heap:系统启动映像,包含启动期间预加载的类。此处的分配保证绝不会移动或消失。
  • zygote heap:写时复制堆,其中的应用进程是从 Android 系统中派生的。
  • app heap:您的应用在其中分配内存的主堆。

内存计数中的类别如下:

  • Java:从 Java 或 Kotlin 代码分配的对象的内存。
  • Native:从 C 或 C++ 代码分配的对象的内存。
    即使您的应用中不使用 C++,您也可能会看到此处使用的一些原生内存,因为 Android 框架使用原生内存代表您处理各种任务,如处理图像资源和其他图形时,即使您编写的代码采用 Java 或 Kotlin 语言。
  • Graphics:图形缓冲区队列向屏幕显示像素(包括 GL 表面、GL 纹理等等)所使用的内存。(请注意,这是与 CPU 共享的内存,不是 GPU 专用内存。)
  • Stack:您的应用中的原生堆栈和 Java 堆栈使用的内存。这通常与您的应用运行多少线程有关。
  • Code:您的应用用于处理代码和资源(如 dex 字节码、经过优化或编译的 dex 代码、.so 库和字体)的内存。
  • Others:您的应用使用的系统不确定如何分类的内存。
    Dump the heap可以查看的数据
  • 您的应用分配了哪些类型的对象,以及每种对象有多少。
  • 每个对象当前使用多少内存。
  • 在代码中的什么位置保持着对每个对象的引用。
  • 对象所分配到的调用堆栈。(目前,对于 Android 7.1 及更低版本,只有在记录分配期间捕获堆转储时,才会显示调用堆栈的堆转储。)

Bitmap内存泄漏处理

  • 通过inSampleSize压缩图片
  • 载入较大图片资源,使用inTempStorage创建临时空间

参考

Android Developer 1
Android Developer 2