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