Probe:Android线上OOM问题定位组件,2021最新京东Android面试题目_m0_64604636的博客-程序员秘密

技术标签: 程序员  面试  android  移动开发  

第一步分配栈内存失败是由于进程的虚拟内存不足,抛出错误信息如下:

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)

第二步clone方法失败是因为线程数超出了限制,抛出错误信息如下:

W/libc: pthread_create failed: clone failed: Out of memory
W/art: Throwing OutOfMemoryError “pthread_create (1040KB stack) failed: Out of memory”
java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Out of memory
at java.lang.Thread.nativeCreate(Native Method)
at java.lang.Thread.start(Thread.java:1078)

OOM问题定位

在分析清楚OOM问题的原因之后,我们对于线上的OOM问题就可以做到对症下药。而针对OOM问题,我们可以根据堆栈信息的特征来确定这是哪一个类型的OOM,下面分别介绍使用Probe组件是如何去定位线上发生的每一种类型的OOM问题的。

堆内存不足

Android中最常见的OOM就是Java堆内存不足,对于堆内存不足导致的OOM问题,发生Crash时的堆栈信息往往只是“压死骆驼的最后一根稻草”,它并不能有效帮助我们准确地定位到问题。

堆内存分配失败,通常说明进程中大部分的内存已经被占用了,且不能被垃圾回收器回收,一般来说此时内存占用都存在一些问题,例如内存泄漏等。要想定位到问题所在,就需要知道进程中的内存都被哪些对象占用,以及这些对象的引用链路。而这些信息都可以在Java内存快照文件中得到,调用Debug.dumpHprofData(String fileName)函数就可以得到当前进程的Java内存快照文件(即HPROF文件)。所以,关键在于要获得进程的内存快照,由于dump函数比较耗时,在发生OOM之后再去执行dump操作,很可能无法得到完整的内存快照文件。

于是Probe对于线上场景做了内存监控,在一个后台线程中每隔1S去获取当前进程的内存占用(通过Runtime.getRuntime.totalMemory()-Runtime.getRuntime.freeMemory()计算得到),当内存占用达到设定的阈值时(阈值根据当前系统分配给应用的最大内存计算),就去执行dump函数,得到内存快照文件。

在得到内存快照文件之后,我们有两种思路,一种想法是直接将HPROF文件回传到服务器,我们拿到文件后就可以使用分析工具进行分析。另一种想法是在用户手机上直接分析HPROF文件,将分析完得到的分析结果回传给服务器。但这两种方案都存在着一些问题,下面分别介绍我们在这两种思路的实践过程中遇到的挑战和对应的解决方案。

线上分析

首先,我们介绍几个基本概念:

  • Dominator:从GC Roots到达某一个对象时,必须经过的对象,称为该对象的Dominator。例如在上图中,B就是E的Dominator,而B却不是F的Dominator。
  • ShallowSize:对象自身占用的内存大小,不包括它引用的对象。
  • RetainSize:对象自身的ShallowSize和对象所支配的(可直接或间接引用到的)对象的ShallowSize总和,就是该对象GC之后能回收的内存总和。例如上图中,D的RetainSize就是D、H、I三者的ShallowSize之和。

JVM在进行GC的时候会进行可达性分析,当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是可回收的。

Github上有一个开源项目HAHA库,用于自动解析和分析Java内存快照文件(即HPROF文件)。下面是HAHA库的分析步骤:

于是我们尝试在App中去新开一个进程使用HAHA库分析HPROF文件,在线下测试过程中遇到了几个问题,下面逐一进行叙述。

分析进程自身OOM

测试时遇到的最大问题就是分析进程自身经常会发生OOM,导致分析失败。为了弄清楚分析进程为什么会占用这么大内存,我们做了两个对比实验:

  • 在一个最大可用内存256MB的手机上,让一个成员变量申请特别大的一块内存200多MB,人造OOM,Dump内存,分析,内存快照文件达到250多MB,分析进程占用内存并不大,为70MB左右。

  • 在一个最大可用内存256MB的手机上,添加200万个小对象(72字节),人造OOM,Dump内存,分析,内存快照文件达到250多MB,分析进程占用内存增长很快,在解析时就发生OOM了。

实验说明,分析进程占用内存与HPROF文件中的Instance数量是正相关的,在将HPROF文件映射到内存中解析时,如果Instance的数量太大,就会导致OOM。

HPROF文件映射到内存中会被解析成Snapshot对象(如下图所示),它构建了一颗对象引用关系树,我们可以在这颗树中查询各个Object的信息,包括Class信息、内存地址、持有的引用以及被持有引用的关系。

![](https://upload-images.jianshu.io/upload_images/19956127-acee7be1a431e32a.png?imageMogr2/auto-orient/st

《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》

【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整内容开源分享

rip%7CimageView2/2/w/1240)

HPROF文件映射到内存的过程:

// 1.构建内存映射的 HprofBuffer 针对大文件的一种快速的读取方式,其原理是将文件流的通道与 ByteBuffer 建立起关联,并只在真正发生读取时才从磁盘读取内容出来。
HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
// 2.构造 Hprof 解析器
HprofParser parser = new HprofParser(buffer);
// 3.获取快照
Snapshot snapshot = parser.parse();
// 4.去重 gcRoots
deduplicateGcRoots(snapshot);

为了解决分析进程OOM的问题,我们在HprofParser的解析逻辑中加入了计数压缩逻辑(如下图),目的是在文件映射过程去控制Instance的数量。在解析过程中对于ClassInstance和ArrayInstance,以类型为key进行计数,当同一类型的Instance数量超过阈值时,则不再向Snapshot中添加该类型的Instance,只是记录Intsance被丢弃的数量和Instance大小。这样就可以控制住每一种类型的Instance数量,减少了分析进程的内存占用,在很大程度上避免了分析进程自身的OOM问题。既然我们在解析时丢弃了一部分Instance,后面就得把丢弃的这部分找补回来,所以在计算RetainSize时我们会进行计数桶补偿,即把之前丢弃的相同类型的Instance数量和大小都补偿到这个对象上,累积去计算RetainSize。

链路分析时间过长

在线下测试过程中还遇到了一个问题,就是在手机上进行链路分析的耗时太长。

使用HAHA算法在PC上可以快速地对所有对象都进行链路分析,但是在手机上由于性能的局限性,如果对所有对象都进行链路分析会导致分析耗时非常长。

考虑到RetainSize越大的对象对内存的影响也越大,即RetainSize比较大的那部分Instance是最有可能造成OOM的“元凶”。

我们在生成Reference之后,做了一步链路归并(如上图),即对于同一个对象的不同Instance,如果其底下的引用链路中的对象类型也相同,则进行归并,并记录Instance的个数和每个Instance的RetainSize。

然后对归并后的Instance按RetainSize进行排序,取出TOP N的Instance,其中在排序过程中我们会对N的值进行动态调整,保证RetainSize达到一定阈值的Instance都能被发现。对于这些Instance才进行最后的链路分析,这样就能大大缩短分析时长。

排序过程:创建一个初始容量为5的集合,往里添加Instance后进行排序,然后遍历后面的Instance,当Instance的RetainSize大于总共消耗内存大小的5%时,进行扩容,并重新排序。当Instance的RetainSize大于现有集合中的最小值时,进行替换,并重新排序。

基础类型检测不到

为了解决HAHA算法中检测不到基础类型泄漏的问题,我们在遍历堆中的Instance时,如果发现是ArrayInstance,且是byte类型时,将它自身舍弃掉,并将它的RetainSize加在它的父Instance上,然后用父Instance进行后面的排序。

至此,我们对HAHA的原始算法做了诸多优化(如下图),很大程度解决了分析进程自身OOM问题、分析时间过长问题以及基础类型检测不到的问题。

针对线上堆内存不足问题,Probe最后会自动分析出RetainSize大小Top N对象到GC Roots的链路,上报给服务器,进行报警。下面是一个线上案例,这里截取了上报的链路分析结果中的一部分,完整的分析结果就是多个这样的组合。在第一段链路分析可以看到,有个Bitmap对象占用了2MB左右的内存,根据链路定位到代码,修复了Bitmap泄漏问题。第二段链路分析反映的是一个Timer泄漏问题,可以看出内存中存在4个这样的Instance,每个Instance的Retain Size是595634,所以这个问题会泄漏的内存大小是4*595634=2.27MB。

裁剪回捞HPROF文件

在Probe上线分析方案之后,发现尽管我们做了很多优化,但是受到手机自身性能的约束,线上分析的成功率也只有65%。

于是,我们对另一种思路即回捞HPROF文件后本地分析进行了探索,这种方案最大的问题就是线上流量问题,因为HPROF文件动辄几百MB,如果直接进行上传,势必会对用户的流量消耗带来巨大影响。

使用这种方案的关键点就在于减少上传的HPROF文件大小,减少文件大小首先想到的就是压缩,不过只是做压缩的话,文件还是太大。接下来,我们就考虑几百MB的文件内容是否都是我们需要的,是否可以对文件进行裁剪。我们希望对HPROF无用的信息进行裁剪,只保留我们关心的数据,就需要先了解HPROF文件的格式:

Debug.dumpHprofData()其内部调用的是VMDebug的同名函数,层层深入最终可以找到/art/runtime/hprof/hprof.cc,HPROF的生成操作基本都是在这里执行的,结合HAHA库代码阅读hrpof.cc的源码。

HPROF文件的大体格式如下:

一个HPROF文件主要分为这四部分:

  • 文件头。
  • 字符串信息:保存着所有的字符串,在解析的时候通过索引id被引用。
  • 类的结构信息:是所有Class的结构信息,包括内部的变量布局,父类的信息等等。
  • 堆信息:即我们关心的内存占用与对象引用的详细信息。

其中我们最关心的堆信息是由若干个相同格式的元素组成,这些元素的大体格式如下图:

每个元素都有个TAG用来标识自己的身份,而后续字节数则表示元素的内容长度。元素携带的内容则是若干个子元素组合而成,通过子TAG来标识身份。

具体的TAG和身份的对应关系可以在hrpof.cc源码中找到,这里不进行展开。

弄清楚了文件格式,接下来需要确定裁剪内容。经过思考,我们决定裁减掉全部基本类型数组的值,原因是我们的使用场景一般是排查内存泄漏以及OOM,只关心对象间的引用关系以及对象大小即可,很多时候对于值并不是很在意,所以裁减掉这部分的内容不会对后续的分析造成影响。

最后需要确定裁剪方案。先是尝试了dump后在Java层进行裁剪,发现效率很低,很多时候这一套操作下来需要20s。然后又尝试了dump后在Native层进行裁剪,这样做效率是高了点,但依然达不到预期。

经过思考,如果能够在dump的过程中筛选出哪些内容是需要保留的,哪些内容是需要裁剪的,需要裁剪的内容直接不写入文件,这样整个流程的性能和效率绝对是最高的。

为了实现这个想法,我们使用了GOT表Hook技术(不展开介绍)。有了Hook手段,但是还没有找到合适的Hook点。通过阅读hrpof.cc的源码,发现最适合的点就是在写入文件时,拿到字节流进行裁剪操作,然后把有用的信息写入文件。于是项目最终的结构如下图:

我们对IO的关键函数open和write进行Hook。Hook方案使用的是爱奇艺开源的xHook库

在执行dump的准备阶段,我们会调用Native层的open函数获得一个文件句柄,但实际执行时会进入到Hook层中,然后将返回的FD保存下来,用作write时匹配。

在dump开始时,系统会不断的调用write函数将内容写入到文件中。由于我们的Hook是以so为目标的,系统运行时也会有许多写文件的操作,所以我们需要对前面保存的FD进行匹配。若FD匹配成功则进行裁剪,否则直接调用origin-write进行写入操作。

流程结束后,就会得到裁剪后的mini-file,裁剪后的文件大小只有原始文件大小的十分之一左右,用于线上可以节省大部分的流量消耗。拿到mini-file后,我们将裁剪部分的位置填上字节0来进行恢复,这样就可以使用传统工具打开进行分析了。

原始HPROF文件和裁剪后再恢复的HPROF文件分别在Android Studio中打开,发现裁剪再恢复的HPROF文件打开后,只是看不到对象中的基础数据类型值,而整个的结构、对象的分布以及引用链路等与原始HPROF文件是完全一致的。事实证明裁剪方案不会影响后续对堆内存的链路分析。

方案融合

由于目前裁剪方案在部分机型上(主要是Android 7.X系统)不起作用,所以在Probe中同时使用了这两种方案,对两种方案进行了融合。即通过一次dump操作得到两份HPROF文件,一份原始文件用于下次启动时分析,一份裁剪后的文件用于上传服务器。

Probe的最终方案实现如下图,主要是在调用dump函数之前先将两个文件路径(希望生成的原始文件路径和裁剪文件路径)传到Native层,Native层记录下两个文件路径,并对open和write函数进行Hook。hookopen函数主要是通过open函数传入的path和之前记录的path比对,如果相同,我们就会同时调用之前记录的两个path的open,并记录下两个FD,如果不相同则直接调原生open函数。hookwrite函数主要是通过传入的FD与之前hookopen中记录的FD比对,如果相同会先对原始文件对应的FD执行原生write,然后对裁剪文件对应的FD执行我们自定义的write,进行裁剪压缩。这样再传入原始文件路径调用系统的dump函数,就能够同时得到一份完整的HPROF文件和一份裁剪后的HPROF文件。

线程数超出限制

对于创建线程失败导致的OOM,Probe会获取当前进程所占用的虚拟内存、进程中的线程数量、每个线程的信息(线程名、所属线程组、堆栈信息)以及系统的线程数限制,并将这些信息上传用于分析问题。

/proc/sys/kernel/threads-max规定了每个进程创建线程数目的上限。在华为的部分机型上,这个上限被修改的很低(大约500),比较容易出现线程数溢出的问题,而大部分手机这个限制都很大(一般为1W多)。在这些手机上创建线程失败大多都是因为虚拟内存空间耗尽导致的,进程所使用的虚拟内存可以查看/proc/pid/status的VmPeak/VmSize记录。

然后通过Thread.getAllStackTraces()可以得到进程中的所有线程以及对应的堆栈信息。

一般来说,当进程中线程数异常增多时,都是某一类线程被大量的重复创建。所以我们只需要定位到这类线程的创建时机,就能知道问题所在。如果线程是有自定义名称的,那么直接就可以在代码中搜索到创建线程的位置,从而定位问题,如果线程创建时没有指定名称,那么就需要通过该线程的堆栈信息来辅助定位。下面这个例子,就是一个“crowdSource msg”的线程被大量重复创建,在代码中搜索名称很快就查出了问题。针对这类线程问题推荐的做法就是在项目中统一使用线程池,可以很大程度上避免线程数的溢出问题。

线程信息:

thread name: Thread[nio_tunnel_handler,5,main] count: 1
thread name: Thread[OkHttp Dispatcher,5,main] count: 30
thread name: Thread[process_read_thread,5,main] count: 4
thread name: Thread[Jit thread pool worker thread 0,5,main] count: 1
thread name: Thread[crowdSource msg,5,main] count: 202
thread name: Thread[Timer-4,5,main] count: 1
thread name: Thread[mqt_js,5,main] count: 1

threadnames:Thread[Thread-5,5,main] count:1
trace:
java.lang.Object.wait(NativeMethod)
com.dianping.networklog.d.run(UnknownSource:28)

FD数超出限制

前面介绍了,当进程中的FD数量达到最大限制时,再去新建线程,在创建JNIEnv时会抛出OOM错误。但是FD数量超出限制除了会导致创建线程抛出OOM以外,还会导致很多其它的异常,为了能够统一处理这类FD数量溢出的问题,Probe中对进程中的FD数量做了监控。在后台启动一个线程,每隔1s读取一次当前进程创建的FD数量,当检测到FD数量达到阈值时(FD最大限制的95%),读取当前进程的所有FD信息归并后上报。

在/proc/pid/limits描述着Linux系统对对应进程的限制,其中Max open files就代表可创建FD的最大数目。

进程中创建的FD记录在/proc/pid/fd中,通过遍历/proc/pid/fd,可以得到FD的信息。

获取FD信息:

File fdFile=new File("/proc/" + Process.myPid() + “/fd”);
File[] files = fdFile.listFiles();
int length = files.length; //即进程中的fd数量
for (int i = 0; i < length ; i++) {
if (Build.VERSION.SDK_INT >= 21) {
Os.readlink(files[i].getAbsolutePath()); //得到软链接实际指向的文件
} else {
//6.0以下系统可以通过执行readlink命令去得到软连接实际指向文件,但是耗时较久
}
}

得到进程中所有的FD信息后,我们会先按照FD的类型进行一个归并,FD的用途主要有打开文件、创建socket连接、创建handlerThread等。

比如像下面这个例子中,就是anon_inode:[eventpoll]和anon_inode:[eventfd]的数量异常的多,说明进程中很可能是启动了大量的handlerThread,再结合回传上来的线程信息就能快速定位到问题代码的具体位置。

FD溢出案例:

FD信息:
anon_inode:[eventpoll] count: 381
anon_inode:[eventfd] count: 381
pipe count 26
socket count 32
/system/framework/framework-res.apk count: 1

Thread信息:
thread name: Thread[Jit thread pool worker thread 0,5,main] count: 1
thread name: Thread[mtqq handler,5,main] count: 302
thread name: Thread[Timer-4,5,main] count: 1
thread name: Thread[mqt_js,5,main] count: 1

count: 1

Thread信息:
thread name: Thread[Jit thread pool worker thread 0,5,main] count: 1
thread name: Thread[mtqq handler,5,main] count: 302
thread name: Thread[Timer-4,5,main] count: 1
thread name: Thread[mqt_js,5,main] count: 1

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/m0_64604636/article/details/121822633

智能推荐

使用GENBANK数据进行分子系统发育树的构建_zhanyongjia_cnu的博客-程序员秘密

一、引言    GENBANK是目前最大而权威的分子序列数据库,调用其中数据可以进行分子系统发育树的构建。1、序列数据获取(以皿蛛系统发育树为例)     在GenBank中,每一个物种或阶元都有一个taxid,他是taxa的ID。而且taxa之间存在父子关系。我们的研究对象是蜘蛛目(Aranaea),其taxaid为6893,其父级阶元是蛛形纲(Arachnida),taxaid

SpringBoot开发Keycloak Spi实例_keycloak <spi>_土豆沒加的博客-程序员秘密

SpringBoot开发Keycloak Spi环境准备SpringBoot准备添加依赖添加maven部署插件代码开发实现Provider实现ProviderFactory部署环境准备keycloak10.0.1安装教程SpringBoot准备添加依赖注意:依赖包的scope需要是provided&lt;dependency&gt; &lt;groupId&gt;org.keycloak&lt;/groupId&gt; &lt;artifactId&gt;keycloak-services&

ESLint配置参数介绍_weixin_34192732的博客-程序员秘密

ESLint 由 JavaScript 红宝书 作者 Nicholas C. Zakas 编写, 2013 年发布第一个版本。 NCZ 的初衷不是重复造一个轮子,而是在实际需求得不到 JSHint 团队响应 的情况下做出的选择:以可扩展、每条规则独立、不内置编码风格为理念编写一个 lint 工具。ESLint 主要有以下特点:默认规则包...

matlab中资源管理无法打开,安装资源的解决办法_matlab附加功能资源管理器打不开_zhenglu777999的博客-程序员秘密

本人使用的是matlab2022b,当出现如上图的问题时,可以通过如下办法解决。在mathwork网站,找到对应的安装网址,下图右下方下载。前提条件,有mathwork账号,用邮箱注册即可。

服务器显示资源不够用,Docker服务器存储资源池不足的问题解决_谁不言的博客-程序员秘密

系统环境:Docker 版本:19.03.13操作系统版本:CentOS 7.8一、问题描述最近在执行 Docker 运行命令启动镜像时候,无法正常执行 Docker 启动镜像命令,提示了如下错误信息:Error: Error response from daemon: devmapper: Thin Pool has 163051 free data blocks which is less t...

随便推点

【leetcode之旅】字符串 - 345. 反转字符串中的元音字母_evan_qb的博客-程序员秘密

需求描述:编写一个函数,以字符串作为输入,反转该字符串中的元音字母。示例 1:输入:“hello”输出:“holle”示例 2:输入:“leetcode”输出:“leotcede”提示:元音字母不包含字母 “y” 。解题解析:该题的目的很明确,即将元音字母进行反转即可,那么问题来了,什么叫元音字母呢?a、e、i、o、u 5个字母就是元音字母,所以我们只需反转这几个字母即可,其他字母不变。思路解析:我们可以定义两个下标left和right,分别从头往后、从最后往前进行遍历。

org.springframework.beans.factory.BeanCreationException_南风不渐暖的博客-程序员秘密

今天在学习 ssm整合项目配置文件遇到了两个简单的问题,但是不注意是很难发现的数据源配置错误org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'dataSource' defined in URL [file:/E:/IDEA_code/mySSM/ssm/target/classes/spring.xml]: Initialization of bean failed; nest

APP自动化基础之界面操作_自动化软件界面_weixin_44885027的博客-程序员秘密

操作类型跟selenium一样,appium也分为两种操作类型:单一操作、动作链条单一动作跟selenium一样,appium中的webdriver中单独封装了一些单一、简单的操作,包括:滚动、拖拽、点击、滑动等。源码class ActionHelpers(webdriver.Remote): def scroll(self: T, origin_el: WebElement, destination_el: WebElement, duration: Optional[int] = N

Iscroll4使用心得_weixin_30475039的博客-程序员秘密

最近做web app项目做到心血来潮,有太多经验想记录,太多细节想分享。今日把使用iscroll4 一直困扰了很久的问题研究解决了,很高兴决定把使用的心得写出来,方便大家参考,不要再走太多的弯路。iscroll4 是一款针对web app使用的滚动控件,它可以模拟原生IOS应用里的滚动列表操作,虽然插件不大但是效果十分好。是web app开发必备的控件之一。iscroll4官网 :htt...

NCPC2016  —— C-Card Hand Sorting(枚举子集+最长上升子序列)_Achanss的博客-程序员秘密

3028: Card Hand Sorting                                                                     这是题目链接哦时间限制: 1 Sec  内存限制: 64 MB提交: 76  解决: 30[提交] [状态] [讨论版] [命题人:外部导入] 题目描述When dealt cards i...

oracle 计算2行的差值,oracle计算两行差值_有所不知的博客-程序员秘密

深入理解javascript原型和闭包(3)——prototype原型既typeof之后的另一位老朋友! prototype也是我们的老朋友,即使不了解的人,也应该都听过它的大名.如果它还是您的新朋友,我估计您也是javascript的新朋友. 在咱们的第一节(深入理解 ...88&amp;period; Merge Sorted Array题目: Given two sorted integer...

推荐文章

热门文章

相关标签