阅读 386

Android 资源溢出崩溃轻松解

资源溢出是什么?

毫无疑问,应用的运行需要占用系统的资源。其中最为人所熟知的资源是内存,内存溢出便是耳熟能详的OOM。

常见的简单OOM一般可以通过堆栈来解决,如Java OOM,一部分可以直接从堆栈中看到哪里使用了多大内存导致了内存溢出,复杂一些的Java OOM,则可以使用其他分析工具来进行处理。但如果堆栈里看不出来呢?或者它不是Java崩溃呢?

java.lang.OutOfMemoryError: Failed to allocate a 3237132 byte allocation with 1612328 free bytes and 1574KB until OOM
复制代码

比如下面这样的Native崩溃,堆栈全是系统堆栈,不花时间去研究就很难确定此崩溃的原因(事实上这个崩溃也是一个OOM)。尤其是,我们并不能说这是系统代码的问题。

接下来本文将会介绍,对于这类崩溃如何进行识别、以及解决。

内存溢出(俗称OOM)

如下case:

Signal 6(SIGABRT), Code -1(SI_QUEUE)
#00 pc 000604de  /apex/com.android.runtime/lib/bionic/libc.so (abort+165)
#01 pc 0003606d  /system/lib/libc++.so (abort_message+88)
#02 pc 000361f1  /system/lib/libc++.so (_ZL28demangling_terminate_handlerv+160)
#03 pc 00045e4b  /system/lib/libc++.so (_ZSt11__terminatePFvvE+2)
#04 pc 00045653  /system/lib/libc++.so (_ZN10__cxxabiv1L12failed_throwEPNS_15__cxa_exceptionE+12)
#05 pc 000455b5  /system/lib/libc++.so (__cxa_throw+72)
#06 pc 00047c6d  /system/lib/libc++.so (_Znwj+52)
#07 pc 0000132b  /system/lib/libbinderthreadstate.so (_ZNSt3__15dequeIN7android18IPCThreadStateBase9CallStateENS_9allocatorIS3_EEE19__add_back_capacityEv+186)
#08 pc 0000120b  /system/lib/libbinderthreadstate.so (_ZNSt3__15dequeIN7android18IPCThreadStateBase9CallStateENS_9allocatorIS3_EEE12emplace_backIJRS3_EEES8_DpOT_+52)
#09 pc 0000115f  /system/lib/libbinderthreadstate.so (_ZN7android18IPCThreadStateBase16pushCurrentStateENS0_9CallStateE+18)
#10 pc 0003b901  /system/lib/libbinder.so (_ZN7android14IPCThreadState14executeCommandEi+608)
#11 pc 0003b5db  /system/lib/libbinder.so (_ZN7android14IPCThreadState20getAndExecuteCommandEv+98)
#12 pc 0003bb7b  /system/lib/libbinder.so (_ZN7android14IPCThreadState14joinThreadPoolEb+38)
#13 pc 00054de5  /system/lib/libbinder.so (_ZN7android10PoolThread10threadLoopEv+12)
#14 pc 0000d96f  /system/lib/libutils.so (_ZN7android6Thread11_threadLoopEPv+210)
#15 pc 00080eb5  /system/lib/libandroid_runtime.so (_ZN7android14AndroidRuntime15javaThreadShellEPv+88)
#16 pc 000ab3dd  /apex/com.android.runtime/lib/bionic/libc.so (_ZL15__pthread_startPv+20)
#17 pc 00061989  /apex/com.android.runtime/lib/bionic/libc.so (__start_thread+30)
复制代码

特征很明显,堆栈全是系统代码(/system/lib/xxx)。

这时候无法一眼看出代码问题,那么就可以怀疑下内存原因

  1. 崩溃原因

众所周知,32位CPU寻址范围最大可以到2的32次方 = 4GB,其实就是32位操作系统最大支持4G内存

如果你试图装过系统就会明白,32位操作系统下,内存不可能达到4G以上,一般会是3G左右。

为什么是3G?因为还有1G被系统吃掉了(不一定真的是1G,可多可少但不会差的远),它们用于操作系统内核相关的运作,如下图。

这里直接总结重点

32位的App32位的手机操作系统上使用超过3G的内存,极大概率会发生Native崩溃

32位的App64位的手机操作系统上使用超过4G的内存,极大概率会发生Native崩溃

其中前者容易理解,1G被系统吃了,就剩下了3G;后者是因为64位手机上,系统是64位的,所以不需要跟App抢那4G空间。

至于64位App,可用内存已经突破天际(所以开发64位app将会减少大量Native崩溃)……

几种主流内存占用类型,可以文末会给出一个总结。

需要注意,这里提到的内存,均为虚拟内存(可以回忆回忆学校学的操作系统知识,网上搜索瞅瞅)。

  1. 定位解决

这里需要用到的工具为应用性能监控全链路版(APMPlus),APMPlus是字节跳动应用开发套件MARS下的性能监控产品,通过先进的数据采集与监控技术,为企业提供全链路的应用性能监控服务,解决企业对各端监控的需求。具备非侵入式监控、丰富的异常现场还原能力,助力企业提升异常问题排查与解决的效率、优化应用品质,以降低成本提高收入。

经过多年技术积累、亿级用户验证,APMPlus 集崩溃监控、上报、分析、归因于一体,可以轻松定位各种线上疑难杂症,更有超详细性能、卡顿、打点等全流程监控处理工具,覆盖近乎一切线上问题的处理。并拥有多个外部客户的实践,如:虎扑、作业帮、甄云科技等,为企业和开发者提供 一站式APM服务。

我们直接在APMPlus平台中查看崩溃,点击“Native 信息 -> Maps详情”,查看虚拟内存占用

一眼看出,这个内存占用明显接近上一节中提到的内存占满的阈值(32App在64位设备上最多使用4G内存)!此时基本可以确认,该崩溃为内存占满导致的Native崩溃,即Native OOM。

知道是Native OOM就完了?

再点一个按钮,直接告诉你怎么解决:“Native 信息 -> Maps智能归类”,查看虚拟内存占用分布

我们可以看到,这里直接提示出三个地方占用的虚拟内存最多,分别是Java runtime、Thread、Files;其中Thread占用最多,高达2.59GB!

直接根据提示,逐级展开内存占用最多的条目:

立即破案:doTestThread线程过多导致虚拟内存占满!接下来只需要去代码里看,哪里创建的这个线程,便可进行问题解决。

类似的,一旦在崩溃中发现Maps智能归类中给出的任意一个条目过高,都可以确认出Native OOM的原因;假如发现Files条目占用内存达到了2G,那么只需根据内存名即可确认什么文件占用内存多,从而进行问题定位解决。

其中由于**“**Java runtime”条目占用起点较高,其内包含Java堆内存等虚拟机自用区域,基本上固定占用1G上下,且一般情况下其占用不会受我们的代码控制,所以需要注意不要被它混淆了视线,优先关注其他条目即可。

另外,Thread内存占用过多且需要查看线程的详细信息时,可以在“Native信息 -> 线程状态”中查看。

注:不同App下,虚拟内存分布的结果都有不同,具体分析需联系自身App正常情况下的内存分布来确认问题。

  1. 内存类型简要解释

ApmInsight平台当前的内存分类方式:

  • Java runtime:安卓系统Java虚拟机占用,一般App默认会占用1G以上,可降低关注优先级
  • Native Heap:C代码使用的堆内存大小,如malloc调用分配的内存等,都会在这里体现;
  • Thread:线程使用的内存大小,默认情况下每个线程启动后(Java、Native均如此)便会占用1M内存
  • Files:映射入内存中的文件,一般由C代码中调用mmap直接加载文件到内存里,Java中使用FileInputStream不会在这里体现
  • Devices:设备相关内存使用
  • nameless:部分没有名字的未知内存使用
  • Other:其他未识别内存

FD溢出

如下case:

Signal 6(SIGABRT), Code -1(SI_QUEUE)
abort message: 'Could not make wake event fd: Too many open files'
#00 pc 000604de  /apex/com.android.runtime/lib/bionic/libc.so (abort+165)
#01 pc 00005a95  /system/lib/liblog.so (__android_log_assert+176)
#02 pc 000100bf  /system/lib/libutils.so (_ZN7android6LooperC2Eb+218)
#03 pc 000d3c51  /system/lib/libandroid_runtime.so (_ZN7android18NativeMessageQueueC1Ev+112)
#04 pc 000d424d  /system/lib/libandroid_runtime.so (_ZN7androidL34android_os_MessageQueue_nativeInitEP7_JNIEnvP7_jclass+12)
#05 pc 002920e7  /system/framework/arm/boot-framework.oat (art_jni_trampoline+94)
#06 pc 000d7bc5  /apex/com.android.runtime/lib/libart.so (art_quick_invoke_stub_internal+68)
......
#30 pc 003afb7f  /apex/com.android.runtime/lib/libart.so (_ZN3art6Thread14CreateCallbackEPv+1018)
#31 pc 000ab3dd  /apex/com.android.runtime/lib/bionic/libc.so (_ZL15__pthread_startPv+20)
#32 pc 00061989  /apex/com.android.runtime/lib/bionic/libc.so (__start_thread+30)
复制代码

同样的,堆栈基本无意义,但有一句看起来能看懂的“Too many open files”。

  1. 崩溃原因

FD即文件描述符(File Descriptor),打开一个文件就占用一个。

看起来没什么的,大家读写文件都是常规操作,一个App产生千八百个文件不过分吧。

但是,系统会限制单个App打开的FD个数

该数字在部分低版本安卓机上一般为1024,也就是你打开1024个FD后,就不能再打开了,有时候就会因此产生Native崩溃

  1. 定位解决

直接点开“Native 信息 -> FD****归类”,来确认是不是FD****过多导致的崩溃

很明显,确实可以看到使用的FD过多,达到了3万以上。向下滚动可以直接看到App在运行时到底打开了哪些文件,只要找到打开的文件名,便能轻松解决此类崩溃。

总结

本文提到的两种崩溃类型,本质上都是系统、应用资源不足下产生的。

资源不足实际上并不会直接导致崩溃,但是会使某些系统调用返回出错,如open打开文件失败返回无效值、malloc分配内存失败返回无效值等。这些返回的无效值如果在使用时未做合理容错判断,则会引起如空指针等这样的代码错误。

更多的崩溃问题归类及解析,将在应用性能监控全链路版(APMPlus)上及后续的文章中进行补充。

如果还未接入使用应用性能监控全链路版(APMPlus),也可以立刻开始进行免费试用,目前 APMPlus面向新用户提供试用30 天的限时免费服务。其中包含 App 监控、Web 监控、Server 监控、小程序控,App 监控和 Web 监控各500 万条事件量, Server 与小程序监控限时不限量。

更多产品信息,欢迎微信进群交流:

文章分类
Android
文章标签