前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android OutOfMemoryError原理解析

Android OutOfMemoryError原理解析

作者头像
烧麦程
发布2024-03-22 13:58:49
1220
发布2024-03-22 13:58:49
举报
文章被收录于专栏:半行代码半行代码

这篇文章我们直接来分析为什么我们的应用会抛出 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结束
代码语言:javascript
复制
if ((was_default_allocator && allocator != GetCurrentAllocator()) ||
      (!instrumented && EntrypointsInstrumented())) {
    return nullptr;
}
  1. gc结束,直接调用 TryToAllocate 分配一次内存,这里 KGrow 传false
代码语言:javascript
复制
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。
代码语言:javascript
复制
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
代码语言:javascript
复制
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异常了:
代码语言:javascript
复制
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:

image.png

  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》 链接:https://juejin.cn/post/7052574440734851085该方案没有公开源码,我写了一个复现的demo,源码仅供参考:https://github.com/shaomaicheng/memoryescape

总结

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-03-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 半行代码 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 快速理解的case
  • 最常见case:堆内存分配
  • 内存优化机制
    • 修改堆内存计数黑科技方案
    • 总结
    相关产品与服务
    容器服务
    腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档