Android Memory(三) -- 问题原因分析

2,691 阅读5分钟

Android-Go.jpeg

前言

     在工作这几年,我一直深受内存问题的困扰,在和内存的不断抗争中,我逐渐积累了一些内存的知识,接来下来我会用几篇文章简单记录一下这几年的我学到的内存相关的经验。另外,本系列文章不去过多的分析Linux底层代码,只是探讨遇到内存问题时的解决方法论。 以下是全部文章的标题和链接:

  1. Android Memory(一) -- 内存基础知识
  2. Android Memory(二) -- 应用内存占用分析
  3. Android Memory(三) -- 问题原因分析
  4. Android Memory(四) -- 问题定位&解决方案1
  5. Android Memory(五) -- 问题定位&解决方案2
  6. Android Memory(六) -- 内存日常监控

通过前两篇的文章,我们了解一些基本的Android知识,这一篇开始,我们这种分析应用的内存问题的定位、解决方向以及监控。

问题定位与分析

     应用发布以后,突然之间线上出现了很多OOM,首先我们要知道问题的原因,在没有定位出来原因之前,盲目的去做某一部分的优化,最终优化的结果会达不到我们的预期,并且费时费力。

     首先我们把线上OOM的情况大致列举一下,大体上分为以下几类:

  1. 进程虚拟内存不足
  2. Java堆内存不足
  3. 连续内存不足
  4. FD数量超过系统限制
  5. 线程数超过系统限制
  6. 手机物理内存不足
一、进程虚拟内存不足

     尽管2019年开始,Google Play就开始强制要求开发者上传包含 64位架构支持的应用,保证应用运行在64位模式。但多年以来,大部分国内应用仍然运行在 32 位兼容模式。并且有一部分设备还对64位模式支持的不是很好。32位进程的虚拟地址空间是232=4G,随着应用的功能不断膨胀,各种框架、媒体播放器、WebView等能力被不断添加进来,这时候你就会发现虚拟内存不够用了,起码我知道国内不少大型应用深受虚拟内存OOM的问题困扰。那么虚拟内存不足都有一些什么现象呢?

  1. 线上出现大量signal 6相关的Crash,点开以后也都是libc abort,并且Android 10的设置占比最多

image.png

image.png

  1. 线上出现大量的pthread异常,并且提示信息是Try again

image.png

  1. 线上还有一部分线程创建mmap失败的异常

image.png

如果有这几个现象的时候,您可能要怀疑是不是应用的虚拟内存不足了,此时如果您还不确定,可以自己在线上进行埋点佐证。

二、Java堆内存不足

这种问题一般都是内存泄露或者内存使用不合理导致,其有以下特征:

image.png

image.png

这类问题比较多时,要去考虑内存泄露和不正常的内存使用问题了

三、连续内存不足

一般来说这种问题上报相对较少, 但是我还是列举出来一些示例供大家参考: image.png

image.png

遇到这种问题,一般进程中存在大量的内存碎片。这时候就得去考虑如何优化内存分配问题了。

四、FD数量超过系统限制

FD超标的问题还是比较常见的,我们可以通过  /proc/pid/limits 来查看描述着系统对对应进程的限制:

tapd_51005639_1646640833_95.png

Soft Limit 软限制:系统资源的使用的上限值,应用自己可以改变,但是不能超过Hard Limit Hard Limit 硬限制:Root权限可以更改,可以改小,但不能改大

FD超出限制的原因有一下几种:

  1. 文件打开过多或打开以后没有关闭
  2. Resource资源打开过多或者没有关闭,这里边包含打开Socket、流、CursorWindow等
  3. 带Looper的Thread,Looper的创建需要使用FD
  4. 线程创建过多,线程创建需要使用FD
  5. InputChannel泄露 常见的异常有以下几种:
  • "Could not allocate JNI Env" 提示
 java.lang.OutOfMemoryError: Could not allocate JNI Env 
 	at java.lang.Thread.nativeCreate(Native Method) 
 	at java.lang.Thread.start(Thread.java:730) at java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:941) 
 	at java.util.concurrent.ThreadPoolExecutor.processWorkerExit(ThreadPoolExecutor.java:1009)
 	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1151) 
 	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607) 
 	at java.lang.Thread.run(Thread.java:761)
  • "Too many open files" 提示
java.net.ConnectException: failed to connect to 40.118.73.216 (port 443) after 10000ms: connect failed: EMFILE (Too many open files)
    at libcore.io.IoBridge.connect(IoBridge.java:124)
    at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:183)
    at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:456)
    at java.net.Socket.connect(Socket.java:882)
    at com.squareup.okhttp.internal.Platform$Android.connectSocket(Platform.java:190)
    ...
  • "Could not read input channel" 提示
java.lang.RuntimeException: Could not read input channel file descriptors from 
    at android.view.InputChannel.nativeReadFromParcel(Native Method)
    at android.view.InputChannel.readFromParcel(InputChannel.java:148)
    at android.view.InputChannel$1.createFromParcel(InputChannel.java:39)
    at android.view.InputChannel$1.createFromParcel(InputChannel.java:37)
    at com.android.internal.view.InputBindResult.<init>(InputBindResult.java:68)
    ...
  • "Could not open input channel pair" 提示
java.lang.RuntimeException: Could not open input channel pair.  status=-24
  at android.view.InputChannel.nativeOpenInputChannelPair(Native Method)
  at android.view.InputChannel.openInputChannelPair(InputChannel.java:94)
  at com.android.server.wm.WindowState.openInputChannel(WindowState.java:2011)
  at com.android.server.wm.WindowManagerService.addWindow(WindowManagerService.java:1406)
  at com.android.server.wm.Session.addToDisplay(Session.java:197)
  ...
  • "Could not copy bitmap to parcel blob" 提示
java.lang.RuntimeException: Could not copy bitmap to parcel blob.
 at android.graphics.Bitmap.nativeWriteToParcel(Native Method)
 at android.graphics.Bitmap.writeToParcel(Bitmap.java:1553)
 at android.widget.RemoteViews$BitmapCache.writeBitmapsToParcel(RemoteViews.java:984)
 at android.widget.RemoteViews.writeToParcel(RemoteViews.java:2854)
 at android.widget.RemoteViews.clone(RemoteViews.java:1903)
 ...

还有一些其他的提示,就不一一列举了。

五. 线程数超过系统限制

和FD泄露一样,可以通过/proc/sys/kernel/threads-max来查看。目前来说在一些低版本手机上是有限制的,很多高版本手机上的线程数量限制都是1W+,等于没有了限制。 比如华为低版本手机上内存数量达到400+就会出现OOM:

java.lang.OutOfMemoryError
pthread_create (1040KB stack) failed: Out of memory
	java.lang.Thread.nativeCreate(Native Method)
	java.lang.Thread.start(Thread.java:743)
	java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:941)
	java.util.concurrent.ThreadPoolExecutor.processWorkerExit(ThreadPoolExecutor.java:1009)
	java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1151)
	java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
	java.lang.Thread.run(Thread.java:774)

要是不确定是不是线程数超标,可以做一个简单的监控线上的线程数量情况,然后再下定论,去做线程数的收敛。

六、 手机物理内存不足

    这种OOM和前边几种内存不足堆栈没有明显的区别,我也是在自己Crash时做的系统内存上报,在上报中才发现系统的可用内存不足,具体接口可以参考我写的Android Memory(一)

总结

     内存优化费事费力,内存问题的定位经常也非常棘手,所以在出现内存问题是要先确定问题的原因,后边解决内存问题时才能事半功倍。另外内存并不是用的越少越好,合理才是关键。但是很重要的一点是应用内存要像自己的房间一样,经常清扫。如果内存问题堆积比较多,解决起来会非常麻烦,所以还是在平时在内存优化、监控上要多花费一些功夫。

参考文档:

  1. tech.meituan.com/2019/11/14/…
  2. www.jianshu.com/p/befd4b86c…
  3. blog.csdn.net/CSDNno/arti…