Android OutOfMemoryError原理解析

这篇文章我们直接来分析为什么我们的应用会抛出 OutOfMemoryError,以及哪些情况下会发生 OutOfMemoryError。 OOM的异常在java层只有 java,lang.OutOfMemoryError 这一个Throwable的定义,抛出这个异常的行为由jni层触发: Thread::ThrowmOutOfMemoryError Heap::ThrowOutOfMemoryError

快速理解的case

我们追溯一下哪些地方可能直接调用 Thread 的 ThrowOutOfMemoryError,先列举几个不常见不怎么需要深入去理解的case:

  • ti_class.cc MakeSingleDexFIle

这个地方发生在 ClassPreDefine 的时候

  • jni NewStringUTF:

通过jni的NewStringUTF分配字符串并且超出最大长度的时候。

  • 分配java String的时候,触发条件其实和jni类似,都是字符串太长去触发。
  • Java Unsafe分配内存失败

可以看到Unsafe是直接通过jni层malloc去分配内存的,失败了就扔oom出去。

  • Thread创建

Java Thread#start 的时候是通过 start -> nativeCreate -> Thread_nativeCreate -> Thread::CreateNativeThread -> pthread_create 执行的。 当 pthread_create 分配失败的时候,就会抛出一个 OOM:

最常见case:堆内存分配

OOM会在 Heap 的 AllocateInternalWithGc 里面抛出。所以我们需要接着上一篇文章再来看看我们的堆内存分配的步骤,在 Heap 的 AllocObjectWithAllocator 函数里会调用TryToAllocate函数去分配,如果分配失败会尝试gc后再重新分配: AllocateInternalWithGc里面的代码比较多,我不截图了,直接把每一步的关键步骤copy过来,我们大概能理解他的意思就行:

  1. 如果gc正在进行,那么等待gc结束
cpp 复制代码
if ((was_default_allocator && allocator != GetCurrentAllocator()) ||
      (!instrumented && EntrypointsInstrumented())) {
    return nullptr;
}
  1. gc结束,直接调用 TryToAllocate 分配一次内存,这里 KGrow 传false
cpp 复制代码
if (last_gc != collector::kGcTypeNone) {
    // A GC was in progress and we blocked, retry allocation now that memory has been freed.
    mirror::Object* ptr = TryToAllocate<true, false>(self, allocator, alloc_size, bytes_allocated,
                                                     usable_size, bytes_tl_bulk_allocated);
    if (ptr != nullptr) {
      return ptr;
    }
}
  1. 如果失败了,那么在调用一次gc,不清除软引用,gc成功之后再调用 TryToAllocate。
cpp 复制代码
if (last_gc < tried_type) {
    const bool gc_ran = PERFORM_SUSPENDING_OPERATION(
        CollectGarbageInternal(tried_type, kGcCauseForAlloc, false, starting_gc_num + 1)
        != collector::kGcTypeNone);

    if ((was_default_allocator && allocator != GetCurrentAllocator()) ||
        (!instrumented && EntrypointsInstrumented())) {
      return nullptr;
    }
    if (gc_ran && have_reclaimed_enough()) {
      mirror::Object* ptr = TryToAllocate<true, false>(self, allocator,
                                                       alloc_size, bytes_allocated,
                                                       usable_size, bytes_tl_bulk_allocated);
      if (ptr != nullptr) {
        return ptr;
      }
    }
}
  1. 如果还失败,那么再调用一次gc,这次gc会清除软引用。更强力一点。这里 TryToAllocate 的 KGrow 传入了 true
cpp 复制代码
PERFORM_SUSPENDING_OPERATION(CollectGarbageInternal(gc_plan_.back(), kGcCauseForAlloc, true, GC_NUM_ANY));
if ((was_default_allocator && allocator != GetCurrentAllocator()) ||
      (!instrumented && EntrypointsInstrumented())) {
    return nullptr;
}
mirror::Object* ptr = nullptr;
if (have_reclaimed_enough()) {
    ptr = TryToAllocate<true, true>(self, allocator, alloc_size, bytes_allocated,
                                    usable_size, bytes_tl_bulk_allocated);
}
  1. 如果到这一步还失败,还会根据allocator类型来做一些策略,比如RosAlloc会做一次空间压缩后再调用 TryToAllocate,代码比较多我们先跳过不看。如果这最后一步挽救措施仍然分配失败的话,那就会抛出OOM异常了:
cpp 复制代码
if (ptr == nullptr) {
    ScopedAllowThreadSuspension ats;
    ThrowOutOfMemoryError(self, alloc_size, allocator);
}

上述流程画到图里便于理解: 那么 TryToAllocate 里面是如何判断内存是否足够呢?在分配器分配的地方都会调用 IsOutOfMemoryOnAllocation 函数来判断内存是否够,而 TryToAllocate传入的kGrow参数也是在这个函数使用: 这里会对比目标内存大小和最大限制 growth_limit_,如果大于growth_limit_,那么就肯定是OOM了。 如果grow是true的话,就会在没有超出最大限制的条件下扩容。所以在分配的时候,前面一次很弱的gc是不清楚软引用+不扩容,后面一次就会升级成清楚软引用+扩容,所以可见虚拟机在保证内存分配尽量成功的前提下,也考虑到了尽量不要占用过多的系统资源。 当前内存分配的总大小是从num_bytes_allocated_里面获取的。 这个变量的新增在2个地方能看到:

  1. AllocObjectWithAllocator 结尾,加上的大小就是各个分配器里熟悉的 bytes_tl_bulk_allocated:
  1. LargeObjectSpace分配成功:

看到这里我们能得到一个结论了,别看art虚拟机把内存分配分成了一大堆Space,像LargeObjectSpace这种,在arm64上分配了固定大小,在非arm64上没有明显限制,但是在堆内存分配的时候,总内存计算是把这些Space都算到一起去了的。

内存优化机制

了解了art里heap的内存分布和对象回收机制,我们基于这些知识点总结一些对应的内存优化思路。

减少不合理的内存分配和内存占用

对应的一些思路包括:

  • 减少不必要的内存缓存,缓存要设计机制及时清理
  • 减少不必要的预加载,按需初始化分配对象
  • 减少不必要的大对象分配,例如重复new的List、Map等容器
  • 适当做一下虚拟内存优化,32bit虚拟内存有限而Java层最终分配还是依赖虚拟内存
  • 通过inlinehook修改Android8以下 Bitmap 的内存分配,减少Java堆内存占用
及时释放内存
  • 避免java层的内存泄漏
  • 防止虚拟内存泄漏出现的线程创建失败、fd溢出
修改堆内存计数黑科技方案
  • 通过inlinehook修改堆内存计数。把LargeObjectSpace的计数单独魔改。这样理论上在arm64架构上可用堆内存变为 进程可用堆内存 * 2,而在非arm64架构上,可用对内存会变成 进程可用堆内存+可用虚拟内存

该方案原理可以参考文章:《拯救OOM!字节自研 Android 虚拟机内存管理优化黑科技 mSponge》 链接:juejin.cn/post/705257... 该方案没有公开源码,我写了一个复现的demo,源码仅供参考:github.com/shaomaichen...

总结

除了上述一些内存优化机制的总结,还有一点比较重要就是治理内存的必要性,内存缺乏治理除了直接OOM的导致,在内存不足的条件下分配,即使没有达到OOM的条件,也仍然可能触发多次的GC,而GC是一个比较占用资源的行为,很可能在一些设备上导致主线程抢占不到时间片,从而影响到APP的各种其他性能指标,例如ANR率,Crash率和卡顿。

相关推荐
JoyceMill31 分钟前
Android 图像效果的奥秘
android
想要打 Acm 的小周同学呀2 小时前
ThreadLocal学习
android·java·学习
天下是个小趴菜2 小时前
蚁剑编码器编写——中篇
android
命运之手2 小时前
【Android】自定义换肤框架05之Skinner框架集成
android·skinner·换肤框架·不重启换肤·无侵入换肤
DS小龙哥2 小时前
QT+OpenCV在Android上实现人脸实时检测与目标检测
android·人工智能·qt·opencv·目标检测
SwBack2 小时前
【pearcmd】通过pearcmd.php 进行GetShell
android·开发语言·php
miao_zz3 小时前
react native中依赖@react-native-clipboard/clipboard库实现文本复制以及在app中获取复制的文本内容
android·react native·react.js
小羊子说3 小时前
Android 开发中 C++ 和Java 日志调试
android·java·c++
Android 开发者3 小时前
平台稳定性里程碑 | Android 15 Beta 3 已发布
android
命运之手3 小时前
【Android】自定义换肤框架02之自定义AssetManager和Resource
android·skin·skinner·换肤框架·不重启换肤