Android | 关于 OOM 的那些事

前言

Android 系统对每个app都会有一个最大的内存限制,如果超出这个限制,就会抛出 OOM,也就是Out Of Memory 。本质上是抛出的一个异常,一般是在内存超出限制之后抛出的。最为常见的 OOM 就是内存泄露(大量的对象无法被释放)导致的 OOM,或者说是需要的内存大小大于可分配的内存大小,例如加载一张非常大的图片,就可能出现 OOM。

常见的 OOM

堆溢出

堆内存溢出是最为常见的 OOM ,通常是由于堆内存已经满了,并且不能够被垃圾回收器回收,从而导致 OOM。

线程溢出

不同的手机允许的最大线程数量是不一样的,在有些手机上这个值被修改的非常低,就会比较容易出现线程溢出的问题

FD数量溢出

文件描述符溢出,当程序打开或者新建一个文件的时候,系统会返回一个索引值,指向该进程打开文件的记录表,例如当我们用输出流文件打开文件的时候,系统就会返回我们一个FD,FD是可能出现泄露的,例如输入输出流没有关闭的时候,详细可参考 Android FD泄露问题

虚拟内存不足

在新建线程的时候,底层需要创建 JNIEnv 对象,并且分配虚拟内存,如果虚拟内存耗尽,会导致创建线程失败,并抛出 OOM。

Jvm,Dvm,Art的内存区别

Android 中使用的是基于 Java 语言的虚拟机 Dalvik / ART ,而 Dalvik 和 ART 都是基于 JVM 的,但是需要注意的是 Android 中的 虚拟器和标准的 JVM 有所不同,因为它们需要运行在 Android 设备上,因此他们具有不同的优化和限制。

在回收方面,Dalvik 仅固定一种回收算法,而 ART 回收算法可在运行期按需选择,并且ART 具备内存整理能力,减少内存空洞。

JVM

JVM 是一个虚构出来的计算机,是通过在实际计算机上仿真各种计算机功能来实现的,它有完善的(虚拟)硬件架构,还有相应的指令系统,其指令集基于堆栈结构。使用 Java虚拟机就是为了支持系统操作无关,在任何系统中都可以运行的程序。

JVM 将所管理的内存分为以下几个部分:

202305121520882.png
  • 方法区
    各个线程锁共享的,用于存储已经被虚拟机加载的类信息,常量,静态变量等,当方法区无法满足内存分配需求时,将会抛出 OutOfMemoryError 异常。
    • 常量池
      常量池也是方法区的一部分,用于存放编译器生成的各种自变量和符号引用,用的最多的就是 String,当 new String 并调用intern 时,就会在常量池查看是否有该字符串,有则返回,没有则创建一个并返回。
  • Java 堆
    虚拟机内存中最大的一块内存,所有通过 new 创建的对象都会在堆内存进行分配,是虚拟机中最大的一块内存,也是gc需要回收的部分,同时OOM也容易发生在这里
    从内存回收角度来看,由于现在收集器大都采用分代收集法,所以还可以细分为新生代,老年代等。
    根据 Java 虚拟机规定,Java 堆可以处于物理上不连续的空间,只要逻辑上是连续的就行,如果对中没有可分配内存时,就会出现 OutOfMemoryError 异常
  • Java 栈
    线程私有,用来存放 java 方法执行时的所有数据,由栈贞组成,一个栈贞就代表一个方法的执行,每个方法的执行就相当于是一个栈贞在虚拟机中从入栈到出栈的过程。栈贞中主要包括,局部变量,栈操作数,动态链接等。
    Java 栈划分为操作数栈,栈帧数据和局部变量数据,方法中分配的局部变量在栈中,同时每一次方法的调用都会在栈中奉陪栈帧,栈的大小是把双刃剑,分配太小可能导致栈溢出,特别是在有递归,大量的循环操作的时候。如果太大就会影响到可创建栈的数量,如果是多线程应用,就会导致内存溢出。
  • 本地方法栈
    与 java 栈的效果基本类似,区别只不过是用来服务于 native 方法。
  • 程序计数器
    是一块较小的空间,它的作用可以看做是当前线程锁执行字节码的行号指示器,用于记录线程执行的字节码指令地址,使得线程切换时能够恢复到正确的执行位置。

DVM

原名 Dalvik 是 Google 公司自己设计用于 Android 平台的虚拟机,本质上也是一个 JAVA 虚拟机,是 Android 中 Java 程序运行的基础,其指令基于寄存器架构,执行其特有的文件格式-dex。

DVM 运行时堆

DVM 的堆结构和 JVM 的堆结构有所区别,主要体现在将堆分成了 Active 堆 和 Zygote 堆。Zygote 是一个虚拟机进程,同时也是一个虚拟机实例孵化器,zygote 堆是 Zygote 进程在启动时预加载的类,资源和对象,除此之外我们在代码中创建的实例,数组等都是存储在 Active 堆中的。

为什么要将 Dalvik 堆分为两块,主要是因为 Android 通过 fork 方法创建一个新的 zygote 进程,为了尽量避免父进程和子进程之间的数据拷贝。

Dalvik 的 Zygote 对存放的预加载类都是 Android 核心类和 Java 运行时库,这部分很少被修改,大多数情况下子进程和父进程共享这块区域,因此这部分类没有必要进行垃圾回收,而 Active 作为程序代码中创建的实例对象的堆,是垃圾回收的重点区域,因此需要将两个堆分开。

DVM 回收机制

DVM 的垃圾回收策略默认是标记清除算法(mark-and-sweep),基本流程如下

  1. 标记阶段:从根对象开始遍历,标记所有可达对象,将它们标记为非垃圾对象
  2. 清楚阶段:遍历整个堆,将所有未被标记的对象清除
  3. 压缩阶段(可选):将所有存货的对象压缩到一起,以便减少内存碎片

需要注意的是 DVM 垃圾回收器是基于标记清除算法的,这种算法会产生内存算法,可能会导致内存分配效率降低,因此 DVM 还支持分代回收算法,可以更好的处理内存碎片问题。
在分代垃圾回收中,内存被分为不同的年代,每个年代使用不同的垃圾回收算法进行处理,年轻代使用标记复制算法,老年代使用标记清除法,这样可以更好的平衡内存分配效率和垃圾回收效率

ART

ART 是在 Android 5.0 中引入的虚拟机,与 DVM 相比,ART 使用的是 AOT(Ahead of Time) 编译技术,这意味着他将应用程序的字节码转换为本机机器码,而不是在运行时逐条解释字节码,这种编译技术可以提高应用程序的执行效率,减少应用程序启动时间和内存占用量

JIT 和 AOT 区别
  • Just In Time
    DVM 使用 JIT 编译器,每次应用运行时,它实时的将一部分 dex 字节码翻译成机器码。在程序的执行过程中,更多的代码被编译缓存,由于 JIT 只翻译一部分代码,它消耗更少的内存,占用更少的物理内存空间
  • Ahead Of Time
    ART 内置了一个 AOT 编译器,在应用安装期间,她将 dex 字节码编译成机器码存储在设备的存储器上,这个过程旨在应用安装到设备的时候发生,由于不在需要 JIT 编译,代码的执行速度回快很多
ART运行时堆

与 DVM 不同的是,ART 采用了多种垃圾收集方案,每个方案会运行不同的垃圾收集器,默认是采用了 CMS (Concurrent Mark-Sweep) 方案,也就是并发标记清除,该方案主要使用了 sticky-CMS 和 partial-CMS。根据不同的方案,ART 运行时堆的空间也会有不同的划分,默认是由四个区域组成的。

分别是 Zygote、Active、Image 和 Large Object 组成的,其中 Zygote 和 Active 的作用越 DVM 中的作用是一样的,Image 区域用来存放一些预加载的类,Large Object 用来分配一下大对象(默认大小为12kb),其中 Zygote 和 Image 是进程间共享的,

为什么会出现 OOM?

出现 OOM 是应为 Android 系统对虚拟机的 heap 做了限制,当申请的空间超过这个限制时,就会抛出 OOM,这样做的目的是为了让系统能同时让比较多的进程常驻于内存,这样程序启动时就不用每次都重新加载到内存,能够给用户更快的响应

Android 获取可分配的内存大小

val manager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
manager.memoryClass

返回当前设备的近似每个应用程序内存类。这让你知道你应该对应用程序施加多大的内存限制,让整个系统工作得最好。返回值以兆字节为单位; 基线Android内存类为16 (恰好是这些设备的Java堆限制); 一些内存更多的设备可能会返回24甚至更高的数字。

我使用的手机内存是 16 g,调用返回的是 256Mb,

manager.memoryClass 对应 build.prop 中 dalvik.vm.heapgrowthlimit

申请更大的堆内存

val manager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
manager.largeMemoryClass

可分配的最大对内存上限,需要在 manifest 文件中设置 android:largeHeap=”true” 方可启用

manager.largeMemoryClass 对应 build.prop 中 dalvik.vm.heapsize

Runtime.maxMemory

获取进程最大可获取的内存上限,等于上面这两个值之一

/system/build.prop

该目录是Android内存配置相关的文件,里面保存了系统的内存的限制等数据,执行 adb 命令可看到 Android 配置的内存相关信息:

adb shell
cat /system/build.prop

默认是打不开的,没有权限,需要 root

打开后找到 dalvik.vm 相关的配置

dalvik.vm.heapstartsize=5m	#单个应用程序分配的初始内存
dalvik.vm.heapgrowthlimit=48m	#单个应用程序最大内存限制,超过将被Kill,
dalvik.vm.heapsize=256m  #所有情况下(包括设置android:largeHeap="true"的情形)的最大堆内存值,超过直接oom。

未设置android:largeHeap=”true”的时候,只要申请的内存超过了heapgrowthlimit就会触发oom,而当设置android:largeHeap=”true”的时候,只有内存超过了heapsize才会触发oom。heapsize已经是该应用能申请的最大内存(这里不包括native申请的内存)。

OOM 演示

堆内存分配失败

堆内存分配失败对应的是 /art/runtime/gc/heap.cc ,如下代码

oid Heap::ThrowOutOfMemoryError(Thread* self, size_t byte_count, AllocatorType allocator_type) {
  // If we're in a stack overflow, do not create a new exception. It would require running the
  // constructor, which will of course still be in a stack overflow.
  if (self->IsHandlingStackOverflow()) {
    self->SetException(
        Runtime::Current()->GetPreAllocatedOutOfMemoryErrorWhenHandlingStackOverflow());
    return;
  }
  //....
  std::ostringstream oss;
  size_t total_bytes_free = GetFreeMemory();
  oss << "Failed to allocate a " << byte_count << " byte allocation with " << total_bytes_free
      << " free bytes and " << PrettySize(GetFreeMemoryUntilOOME()) << " until OOM,"
      << " target footprint " << target_footprint_.load(std::memory_order_relaxed)
      << ", growth limit "
      << growth_limit_;
  
  self->ThrowOutOfMemoryError(oss.str().c_str());
}

通过上面的分析,我们也知道系统对每个应用都做了最大内存的约束,超过这个值就会 OOM ,下面通过一段代码来演示一下这种类型的 OOM

fun testOOM() {
    val manager = requireContext().getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
    Timber.e("app maxMemory ${manager.memoryClass} Mb")
    Timber.e("large app maxMemory ${manager.largeMemoryClass} Mb")
    Timber.e("current app maxMemory ${Runtime.getRuntime().maxMemory() / 1024 / 1024} Mb")
    var count = 0
    val list = mutableListOf()
    while (true) {
        Timber.e("count $count    total ${count * 20}")
        list.add(ByteArray(1024 * 1024 * 20))
        count++
    }
}

上面代码中每次申请 20mb,测试分为两种情况,

未开启 largeHeap:

 E  app maxMemory 256 Mb
 E  large app maxMemory 512 Mb
 E  current app maxMemory 256 Mb
 E  count 0    total 0
 E  count 1    total 20
 E  count 2    total 40
 E  count 3    total 60
 E  count 4    total 80
 E  count 5    total 100
 E  count 6    total 120
 E  count 7    total 140
 E  count 8    total 160
 E  count 9    total 180
 E  count 10    total 200
 E  count 11    total 220
 E  count 12    total 240
java.lang.OutOfMemoryError: Failed to allocate a 20971536 byte allocation with 12386992 free bytes and 11MB until OOM, target footprint 268435456, growth limit 268435456
......

可以看到一共分配了 12次,在第十二次的时候抛出了异常,显示 分配 20 mb 失败,空闲只有 11 mb,

开启 largeHeap

app maxMemory 256 Mb                      
large app maxMemory 512 Mb
current app maxMemory 512 Mb
E  count 0    total 0
E  count 1    total 20
E  count 2    total 40
E  count 3    total 60
E  count 4    total 80
E  count 5    total 100
E  count 6    total 120
E  count 7    total 140
E  count 8    total 160
E  count 9    total 180
E  count 10    total 200
E  count 11    total 220
E  count 12    total 240
E  count 13    total 260
E  count 14    total 280
E  count 15    total 300
E  count 16    total 320
E  count 17    total 340
E  count 18    total 360
E  count 19    total 380
E  count 20    total 400
E  count 21    total 420
E  count 22    total 440
E  count 23    total 460
E  count 24    total 480
E  count 25    total 500
FATAL EXCEPTION: main
Process: com.dzl.duanzil, PID: 31874
java.lang.OutOfMemoryError: Failed to allocate a 20971536 byte allocation with 8127816 free bytes and 7937KB until OOM, target footprint 536870912, growth limit 536870912

可以看到分配了25 次,可使用的内存也增加到了 512 mb

创建线程失败

线程创建会消耗大量的内存资源,创建的过程涉及 java 层 和 native 层,本质上是在 native 层完成的,对应的是 /art/runtime/thread.cc ,如下代码

void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) {
  //........
  env->SetLongField(java_peer, WellKnownClasses::java_lang_Thread_nativePeer, 0);
  {
    std::string msg(child_jni_env_ext.get() == nullptr ?
        StringPrintf("Could not allocate JNI Env: %s", error_msg.c_str()) :
        StringPrintf("pthread_create (%s stack) failed: %s",
                                 PrettySize(stack_size).c_str(), strerror(pthread_create_result)));
    ScopedObjectAccess soa(env);
    soa.Self()->ThrowOutOfMemoryError(msg.c_str());
  }
}

这里借用网上的一张照片来看一下创建线程的流程

202305221618788.jpg

根据上图可以看到主要有两部分,分别是创建 JNI Env 和 创建线程

创建 JNI Env 失败

FD 溢出导致 JNIEnv 创建失败

E/art: ashmem_create_region failed for 'indirect ref table': Too many open files java.lang.OutOfMemoryError:Could not allocate JNI Env at java.lang.Thread.nativeCreate(Native Method) at java.lang.Thread.start(Thread.java:730)

虚拟内存不足导致 JNIEnv 创建失败

E OOM_TEST: create thread : 1104
W com.demo: Throwing OutOfMemoryError "Could not allocate JNI Env: Failed anonymous mmap(0x0, 8192, 0x3, 0x22, -1, 0): Operation not permitted. See process maps in the log." (VmSize 2865432 kB)
E InputEventSender: Exception dispatching finished signal.
E MessageQueue-JNI: Exception in MessageQueue callback: handleReceiveCallback
MessageQueue-JNI: java.lang.OutOfMemoryError: Could not allocate JNI Env: Failed anonymous mmap(0x0, 8192, 0x3, 0x22, -1, 0): Operation not permitted. See process maps in the log.
E MessageQueue-JNI:      at java.lang.Thread.nativeCreate(Native Method)
E MessageQueue-JNI:      at java.lang.Thread.start(Thread.java:887)

E AndroidRuntime: FATAL EXCEPTION: main
E AndroidRuntime: Process: com.demo, PID: 3533
E AndroidRuntime: java.lang.OutOfMemoryError: Could not allocate JNI Env: Failed anonymous mmap(0x0, 8192, 0x3, 0x22, -1, 0): Operation not permitted. See process maps in the log.
E AndroidRuntime:        at java.lang.Thread.nativeCreate(Native Method)
E AndroidRuntime:        at java.lang.Thread.start(Thread.java:887)

创建线程失败

虚拟机内存不足导致失败

native 通过 FixStackSize 设置线程大小

static size_t FixStackSize(size_t stack_size) {
  if (stack_size == 0) {
    stack_size = Runtime::Current()->GetDefaultStackSize();
  }
  stack_size += 1 * MB;
  if (kMemoryToolIsAvailable) {
    stack_size = std::max(2 * MB, stack_size);
  }  if (stack_size < PTHREAD_STACK_MIN) {
    stack_size = PTHREAD_STACK_MIN;
  }
  if (Runtime::Current()->ExplicitStackOverflowChecks()) {
    stack_size += GetStackOverflowReservedBytes(kRuntimeISA);
  } else {
    stack_size += Thread::kStackOverflowImplicitCheckSize +
        GetStackOverflowReservedBytes(kRuntimeISA);
  }
  stack_size = RoundUp(stack_size, kPageSize);
  return stack_size;
}

W/libc: pthread_create failed: couldn't allocate 1073152-bytes mapped space: Out of memory
W/tch.crowdsourc: Throwing OutOfMemoryError with VmSize  4191668 kB "pthread_create (1040KB stack) failed: Try again"
java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Try again
        at java.lang.Thread.nativeCreate(Native Method)
        at java.lang.Thread.start(Thread.java:753)

线程数量超过限制

用一段简单的代码来测试一下

fun testOOM() {
    var count = 0
    while (true) {
        val thread = Thread(Runnable {
            Thread.sleep(1000000000)
        })
        thread.start()
        count++
        Timber.e("current thread count $count")
    }
}

通过打印日志发现,一共创建了 2473 个线程,当然这些线程都是没有任务的线程,报错信息如下所示

pthread_create failed: couldn't allocate 1085440-bytes mapped space: Out of memory
Throwing OutOfMemoryError "pthread_create (1040KB stack) failed: Try again" (VmSize 4192344 kB)

FATAL EXCEPTION: main
Process: com.dzl.duanzil, PID: 18085
java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Try again
    at java.lang.Thread.nativeCreate(Native Method)

通过测试可以看出来,具体的原因也是内存不足引起的,而不是线程数量超过限制,可能是测试的方法有问题,或者说是还没有达到最大线程的限制,由于手机没有权限,无法查看线程数量限制,所以等有机会了再看。

OOM 监控

我们都知道,OOM 的出现就是大部分原因都是由于内存泄露,导致内存无法释放,才出现了 OOM,所以监控主要监控的是内存泄露,现在市面上对于内存泄露检查这方面已经非常成熟了,我们来看几个常用的监控方式

LeakCanary

使用非常简单,只需要添加依赖后就可以直接使用,无需手动初始化,就能实现内存泄露检测,当内存发生泄露后,会自动发出一个通知,点击就可以查看具体的泄露堆栈信息

LeakCannary 只能在 debug 环境使用,因为他是在当前进程 dump 内存快照,会冻结当前进程一段时间,所以不适于在正式环境使用。

Android Profile

可以以图像的方式直观的查看内存使用情况,并且可以直接 capture heap dump,或者抓取原生内存(C/C++) 以及 Java/Kotlin 内存分配。只能在线下使用,功能非常强大,可是吧内存泄露,抖动,强制 GC 等。

ResourceCanary

ResourceCanary 属于 Matrix 的一个子模块,它将原本难以发现的 Acivity 泄露和 Activity 泄露和重复创建的沉余的 Bitmap 暴露出来,并提供引用链等信息帮助排查这些问题

ResourceCanary 将检测和分析分离,客户端只负责检测和dump内存镜像文件,并且对检查部分生成的 Hprof 文件进行了裁剪,移除了大部分无用数据。也增加了 Bitmap 对象检测,方便通过减少沉余 Bitmap 数量,降低内存消耗。

使用可查看 Matrix

KOOM

上面的两者都只能在线下使用,而 KOOM 可以再线上使用,KOOM 是快手出的一套完整的解决方案,可以实现 Java,native 和 thread 的泄露监控

使用可查看 KOOM

OOM 优化

OOM 的优化其实相当于是内存优化,对于这部分内容,网上有一篇讲的非常详细的文章 **深入探索 Android 内存优化**,该文章内容非常全面且难度也比较高,建议仔细阅读。

参考链接

【性能优化】大厂OOM优化和监控方案

深入探索 Android 内存优化

DVM和ART原理初探

Android OOM 问题探究

….

文章来源于互联网:Android | 关于 OOM 的那些事

阅读全文
下载说明:
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.dandroid.cn/21812,转载请注明出处。
0

评论0

显示验证码
没有账号?注册  忘记密码?