Android 虚拟机中的内存分配与OOM问题详解

手机APP/开发
449
0
0
2023-02-02
目录
  • 背景知识
  • 一、Android VM的内存空间
  • 1.查看内存的API
  • 二、Android VM内存分配流程
  • 小结
  • 三、出现OOM的建议解决方案

背景知识

Android中每个App默认情况下是运行在一个独立进程中的, 而这个独立进程正是从Zygote孵化出来的VM进程, 也就是说, 也就是说每个Android APP在运行时会启动一个Java虚拟机。

并且系统会给它分配固定的内存空间(手机厂商会根据手机的配置情况来对其进行调整)。

一、Android VM的内存空间

Android是一个多任务系统, 为了保证多任务的运行, Android给每个App可使用的Heap大小设定了一个限定值.

这个值是系统设置的prop值, 保存在System/build.prop文件中. 一般国内的手机厂商都会做修改, 根据手机配置不同而不同, 可以直接打开查看与修改。

其中和虚拟机内存相关的主要有以下三个:

1 . dalvik.vm.heapstartsize

– App启动后,系统分配给它的Heap初始大小,随着App使用可增加。

2 . dalvik.vm.heapgrowthlimit

– 默认情况下, App可使用的Heap的最大值, 超过这个值就会产生OOM.

3 . dalvik.vm.heapsize

– 如果App的manifest文件中配置了largeHeap属性, 那么App可使用的Heap的最大值为此项设定值。

 <application
    android:largeHeap="true">
    ...
</application>

所以对于同一个手机,不开启largeHeap属性时与多进程时,每个APP的虚拟机分配的内存的上限都是heapgrowthlimit

1.查看内存的API

Android在ActivityManager类中提供了API可以运行时获取这些属性值,如下:

//ActivityManager的getMemoryClass()获得内用正常情况下内存的大小,即heapgrowthlimit的值
//ActivityManager的getLargeMemoryClass()可以获得开启largeHeap最大的内存大小,即heapsize的指
ActivityManager activityManager = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE);
activityManager.getMemoryClass();
activityManager.getLargeMemoryClass();

二、Android VM内存分配流程

虚拟机分配内存的具体源码可以AOSP的Heap.cpp文件中查看:

/* Try as hard as possible to allocate some memory.
 */
static void *tryMalloc(size_t size)
{
    void *ptr;
    //TODO: figure out better heuristics
    //    There will be a lot of churn if someone allocates a bunch of
    //    big objects in a row, and we hit the frag case each time.
    //    A full GC for each.
    //    Maybe we grow the heap in bigger leaps
    //    Maybe we skip the GC if the size is large and we did one recently
    //      (number of allocations ago) (watch for thread effects)
    //    DeflateTest allocs a bunch of ~128k buffers w/in 0-5 allocs of each other
    //      (or, at least, there are only 0-5 objects swept each time)
    ptr = dvmHeapSourceAlloc(size);
    if (ptr != NULL) {
        return ptr;
    }
    /*
     * The allocation failed.  If the GC is running, block until it
     * completes and retry.
     */
    if (gDvm.gcHeap->gcRunning) {
        /*
         * The GC is concurrently tracing the heap.  Release the heap
         * lock, wait for the GC to complete, and retrying allocating.
         */
        dvmWaitForConcurrentGcToComplete();
    } else {
      /*
       * Try a foreground GC since a concurrent GC is not currently running.
       */
      gcForMalloc(false);
    }
    ptr = dvmHeapSourceAlloc(size);
    if (ptr != NULL) {
        return ptr;
    }
    /* Even that didn't work;  this is an exceptional state.
     * Try harder, growing the heap if necessary.
     */
    ptr = dvmHeapSourceAllocAndGrow(size);
    if (ptr != NULL) {
        size_t newHeapSize;
        newHeapSize = dvmHeapSourceGetIdealFootprint();
        //TODO: may want to grow a little bit more so that the amount of free
        //      space is equal to the old free space + the utilization slop for
        //      the new allocation.
        LOGI_HEAP("Grow heap (frag case) to "
                "%zu.%03zuMB for %zu-byte allocation",
                FRACTIONAL_MB(newHeapSize), size);
        return ptr;
    }
    /* Most allocations should have succeeded by now, so the heap
     * is really full, really fragmented, or the requested size is
     * really big.  Do another GC, collecting SoftReferences this
     * time.  The VM spec requires that all SoftReferences have
     * been collected and cleared before throwing an OOME.
     */
    //TODO: wait for the finalizers from the previous GC to finish
    LOGI_HEAP("Forcing collection of SoftReferences for %zu-byte allocation",
            size);
    gcForMalloc(true);
    ptr = dvmHeapSourceAllocAndGrow(size);
    if (ptr != NULL) {
        return ptr;
    }
    //TODO: maybe wait for finalizers and try one last time
    LOGE_HEAP("Out of memory on a %zd-byte allocation.", size);
    //TODO: tell the HeapSource to dump its state
    dvmDumpThread(dvmThreadSelf(), false);
    return NULL;
}

具体流程如下:

  • 尝试分配,如果成功则返回,失败则转入步骤2
  • 判断是否gc正在进行垃圾回收,如果正在进行则等待回收完成之后,尝试分配。如果成功则返回,失败则转入步骤3
  • 自己启动gc进行垃圾回收,这里gcForMalloc的参数是false。所以不会回收软引用,回收完成后尝试分配,如果成功则返回,失败则转入步骤4
  • 调用dvmHeapSourceAllocAndGrow尝试分配,这个函数会扩张堆的大小,失败转入步骤5
  • 进入回收软引用阶段,这里gcForMalloc的参数是ture,所以需要回收软引用。然后再调用dvmHeapSourceAllocAndGrow尝试分配,如果失败则抛出OOM

小结

所以产生OOM时,一定是java的堆中 已有的内存 + 申请的内存 >= heapgrowthlimit导致的,不会因为手机目前物理内存是否紧张而改变 - 当物理内存非常紧张时系统会通过LowMemory Killer杀掉一些低优先级的进程。

相应的,物理内存非常充足的情况也会有OOM的情况发生。

三、出现OOM的建议解决方案

当APP出现OOM时,建议可以从以下两个方向来处理:

1 . 排查内存泄露问题

  • 排查各个功能是否内存泄露情况,可以通过Android Studio中的MemoryMonitor功能进行分析,Memory Monitor也集成了HPROF Viewer和Allocation Tracker可以分析内存快照与内存分配追踪。另外推荐一个工具,square公司开源的leakcanary,非常简洁好用。
  • 排查进程初始化时就直接申请并常驻内存的对象以及其他功能里申请的static对象或者单例对象的必要性。

2 . 内存优化

按照谷歌在youtube上发布的性能优化典范之内存篇,优化各功能的内存,或可参照 胡凯的总结 。

大致有以下这些,具体请参见原文:

  • 谨慎使用large heap
  • 综合考虑设备的内存阈值与其他因素设计合适的缓存大小
  • onLowMemory与onTrimMemory
  • 资源文件需要选择合适的文件夹进行存放
  • Try catch某些大内存分配的操作
  • 谨慎使用static对象
  • 特别留意单例对象中不合理的持有
  • 珍惜Services资源
  • 优化布局层次,减少内存消耗
  • 谨慎使用“抽象”编程
  • 使用nano protobufs序列化数据
  • 谨慎使用依赖注入框架
  • 谨慎使用多进程
  • 使用ProGuard来剔除不需要的代码
  • 谨慎使用第三方libraries

考虑不同的实现方式来优化内存占用

  • 注意Activity的泄露
  • 考虑使用Applicaiton Context代替Activity Context
  • 注意临时Bitmap对象的及时回收
  • 注意监听器的注销
  • 注意缓存容器里的对象泄露
  • 注意Webview的泄露

注意Cursor对象的及时关闭

  • 复用系统自带的资源
  • ListView中对ConvertView的复用
  • Bitmap对象的复用
  • 避免在ondraw方法里执行对象的创建

StringBuilder代替String

  • 使用更加轻量的数据结构
  • 避免在Android里使用enum
  • 减少Bitmap对象的内存占用

使用更小的图片

  • 减少对象的内存占用
  • 内存对象的重复利用
  • 避免对象的内存泄露
  • 内存使用策略的优化