作者:字节跳动终端技术—李权飞
资源溢出是什么?
毫无疑问,应用的运行需要占用系统的资源。其中最为人所熟知的资源是内存,内存溢出便是耳熟能详的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)。
这时候 无法一眼看出代码问题,那么就可以怀疑下内存原因。
崩溃原因
众所周知,32位CPU寻址范围最大可以到2的32次方 = 4GB,其实就是 32位操作系统 最大支持 4G内存。
如果你试图装过系统就会明白,32位操作系统下,内存不可能达到4G以上,一般会是3G左右。
为什么是3G?因为还有 1G被系统吃掉 了(不一定真的是1G,可多可少但不会差的远),它们用于操作系统内核相关的运作,如下图。
这里直接 总结重点 :
32位的App 在 32位的手机 操作系统上使用 超过3G的内存 ,极大概率会发生 崩溃 ;
32位的App 在 64位的手机 操作系统上使用 超过4G的内存 ,极大概率会发生 崩溃 ;
其中前者容易理解,1G被系统吃了,就剩下了3G;后者是因为64位手机上,系统是64位的,所以不需要跟App抢那4G空间。
至于64位App,可用内存已经突破天际(所以开发64位app将会减少大量崩溃)……
需要注意,这里提到的内存,均为 虚拟内存 (可以回忆回忆学校学的操作系统知识,网上搜索瞅瞅)。
定位解决
这里需要用到的工具为字节跳动企业级技术服务平台火山引擎下的 应用性能监控全链路版 ,是终端技术团队基于字节跳动内部抖音、今日头条、飞书等多个超大规模用户量级移动App的多年沉淀和积累后的完全自研的应用性能监控产品。
经过多年技术积累、亿级用户验证,应用性能监控全链路版 集崩溃监控、上报、分析、归因 于一体,可以轻松定位各种线上疑难杂症,更有超详细 性能、卡顿、打点 等全流程监控处理工具,覆盖近乎一切线上问题的处理。并拥有多个外部客户的实践,如:虎扑、作业帮、甄云科技等,为企业和开发者提供 一站式APM服务。
我们直接在 应用性能监控全链路版 中查看崩溃,点击“ Native 信息 -> Maps详情 ”,查看 虚拟内存占用 。
一眼看出,这个内存占用明显接近上一节中提到的 内存占满的阈值 (32App在64位设备上最多使用4G内存)!此时基本可以确认,该崩溃为内存占满导致的崩溃,即OOM。
知道是OOM就完了?
再点一个按钮,直接告诉你怎么解决:“ Native 信息 -> Maps智能归类 ”,查看 虚拟内存占用分布 。
我们可以看到,这里直接提示出三个地方占用的虚拟内存最多,分别是Java runtime、Thread、Files;其中 Thread占用最多 ,高达2.59GB!
直接根据提示,逐级展开内存占用最多的条目:
立即破案:doTestThread线程过多导致虚拟内存占满!接下来只需要去代码里看,哪里创建的这个线程,便可进行问题解决。
类似的,一旦在崩溃中发现Maps智能归类中给出的任意一个条目过高,都可以确认出OOM的原因;假如发现Files条目占用内存达到了2G,那么只需根据内存名即可确认什么文件占用内存多,从而进行问题定位解决。
其中由于 “ Java runtime”条目占用起点较高 ,其内包含Java堆内存等虚拟机自用区域,基本上固定占用1G上下,且一般情况下其占用不会受我们的代码控制,所以需要注意不要被它混淆了视线, 优先关注其他条目 即可。
另外,Thread内存占用过多且需要查看线程的详细信息时,可以在“Native信息 -> 线程状态”中查看。
注:不同App下,虚拟内存分布的结果都有不同,具体分析需联系自身App正常情况下的内存分布来确认问题。
内存类型简要解释
平台中当前的内存分类方式:
- 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”。
崩溃原因
FD即文件描述符(File Descriptor),打开一个文件就占用一个。
看起来没什么的,大家读写文件都是常规操作,一个App产生千八百个文件不过分吧。
但是,系统会 限制单个App打开的 FD 个数 !
该数字在部分低版本安卓机上 一般为1024 ,也就是你打开1024个FD后,就不能再打开了,有时候就会因此 产生崩溃 。
定位解决
直接点开“ Native 信息 -> FD 归类 ”,来确认是不是 FD 过多导致的崩溃 。
很明显,确实可以看到使用的FD过多,达到了3万以上。向下滚动可以直接看到App在运行时到底打开了哪些文件,只要找到打开的文件名,便能轻松解决此类崩溃。
总结
本文提到的两种崩溃类型,本质上都是 系统、应用资源不足下 产生的。
资源不足实际上并不会直接导致崩溃,但是会 使某些系统调用返回出错 ,如open打开文件失败返回无效值、malloc分配内存失败返回无效值等。这些返回的无效值如果在使用时未做合理容错判断,则会 引起如空指针等 这样的代码错误。
更多的崩溃问题归类及解析,将在应用性能监控全链路版(APMPlus)上及后续的文章中进行补充。
如果还未接入使用应用性能监控全链路版(APMPlus),也可以立刻开始进行免费试用,目前 APMPlus面向新用户提供试用30 天的限时免费服务。其中包含 App 监控、Web 监控、Server 监控、小程序监控,App 监控和 Web 监控各500 万条事件量, Server 与小程序监控限时不限量。