[关闭]
@TryLoveCatch 2022-05-18T03:43:51.000000Z 字数 9528 阅读 999

Android知识体系之异常处理-OOM

Android知识体系


参考

Android性能监控(三):“头号顽疾OOM”监测方案的实践

「抄底 Android 内存优化 8」 —— 快手线上 OOM 监控学习笔记
Probe:Android线上OOM问题定位组件
抖音 Android 性能优化系列:Java 内存优化篇

Android高级性能调优;不可思议的OOM!

https://blog.yorek.xyz/android/paid/master/stuck_1/
https://gank.io/post/5e79880393b891c522d3bde6

OOM

Android设备出厂以后,java虚拟机对单个应用的最大内存分配就确定下来了,超出这个值就会OOM。单个应用可用的最大内存对应于 /system/build.prop 文件中的 dalvik.vm.heapgrowthlimit。

OOM异常

Android虚拟机最终抛出OutOfMemoryError代码在/art/runtime/thread.cc

  1. void Thread::ThrowOutOfMemoryError(const char* msg)
  2. 参数 msg 携带了 OOM 时的错误信息

Java堆内存异常

进行堆内存分配时抛出的OOM错误如下:

  1. java.lang.OutOfMemoryError: Failed to allocate a 23970828 byte allocation with 2097152 free bytes and 2MB until OOM

系统源码文件:/art/runtime/gc/heap.cc

  1. void Heap::ThrowOutOfMemoryError(Thread* self, size_t byte_count, AllocatorType allocator_type)
  2. 抛出时的错误信息:
  3. oss << "Failed to allocate a " << byte_count << " byte allocation with " << total_bytes_free << " free bytes and " << PrettySize(GetFreeMemoryUntilOOME()) << " until OOM";

这里也可以细分成两种不同的类型:

内存达到上限

由Runtime.getRuntime.MaxMemory()可以得到Android中每个进程被系统分配的内存上限,当进程占用内存达到这个上限时就会发生OOM,这也是Android中最常见的OOM类型。

无足够的连续内存空间
  1. java.lang.OutOfMemoryError: Failed to allocate a 604 byte allocation with 16777216 free bytes and 319MB until OOM; failed due to fragmentation (required continguous free 65536 bytes for a new buffer where largest contiguous free 53248 bytes)

这种情况一般是进程中存在大量的内存碎片导致的,其堆栈信息会比第一种OOM堆栈多出一段信息:

  1. failed due to fragmentation (required continguous free 65536 bytes for a new buffer where largest contiguous free 53248 bytes)

创建JNI失败

创建JNIEnv可以归为两个步骤:

文件描述符超限

第一步创建匿名共享内存时,需要打开/dev/ashmem文件,所以需要一个FD(文件描述符)。此时,如果创建的FD数已经达到上限,则会导致创建JNIEnv失败,抛出错误信息如下:

  1. E/art: ashmem_create_region failed for 'indirect ref table': Too many open files
  2. java.lang.OutOfMemoryError: Could not allocate JNI Env
  3. at java.lang.Thread.nativeCreate(Native Method)
  4. at java.lang.Thread.start(Thread.java:730)
虚拟内存不足

第二步调用mmap时,如果进程虚拟内存地址空间耗尽,也会导致创建JNIEnv失败,抛出错误信息如下:

  1. E/art: Failed anonymous mmap(0x0, 8192, 0x3, 0x2, 116, 0): Operation not permitted. See process maps in the log.
  2. java.lang.OutOfMemoryError: Could not allocate JNI Env
  3. at java.lang.Thread.nativeCreate(Native Method)
  4. at java.lang.Thread.start(Thread.java:1063)

创建线程异常

创建线程异常抛出的OOM错误如下:

  1. java.lang.OutOfMemoryError: pthread_create(1040KB statck) failed: Out of memory(代号1040)

创建线程也可以归纳为两个步骤:

上面两步都会抛出pthread_create异常,堆栈信息略有不同

虚拟内存不足

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

  1. W/libc: pthread_create failed: couldn't allocate 1073152-bytes mapped space: Out of memory
  2. W/tch.crowdsourc: Throwing OutOfMemoryError with VmSize 4191668 kB "pthread_create (1040KB stack) failed: Try again"
  3. java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Try again
  4. at java.lang.Thread.nativeCreate(Native Method)
  5. at java.lang.Thread.start(Thread.java:753)
线程数量超限

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

  1. W/libc: pthread_create failed: clone failed: Out of memory
  2. W/art: Throwing OutOfMemoryError "pthread_create (1040KB stack) failed: Out of memory"
  3. java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Out of memory
  4. at java.lang.Thread.nativeCreate(Native Method)
  5. at java.lang.Thread.start(Thread.java:1078)

文件描述符数量超限

  1. java.lang.OutOfMemoryError: Could not allocate JNI Env(代号JNIEnv)

在 Android 9 以前 FD 是宝贵的资源,系统默认设定一个进程最多分配 1024 个FD,可以通过 ulimit -a 查看。到了 Android 9 Google 工程师也意识到这个设定有点小了已经拓展到 32768 (3k个)。

我们很多应用场景都会默默的使用 FD 例如 Handler、stock 等比如创建一个 HandlerThread 在创建线程的同时还会创建两个 FD:mWeakEventFd和mEpollEvent(原因是 Handler 内使用了 epoll 机制来保障 Looper 的等待和唤醒,在 Android 5.0之前 Handler 的底层使用的是管道此时会使用三个 FD)。

线下排查

https://blog.csdn.net/arnozhang12/article/details/50817050

排查内存泄漏

参见之前的文章:https://www.zybuluo.com/TryLoveCatch/note/1801113

ADB

查看 JVM 的 HeapSize 等参数

  1. adb shell getprop dalvik.vm.heapsize

该命令可以直接查看 Dalvik 虚拟机为 App 规定的最大 HeapSize

一般来说,App 可达到的最大 HeapSize 为 dalvik.vm.heapgrowthlimit 所规定的大小。但是如果我们在 AndroidManifest.xml 中为 Application 添加 android:largeHeap="true" 属性,App 可达到的最大 HeapSize 则被调整为 dalvik.vm.heapsize 规定的值。

虽然添加 android:largeHeap="true" 属性将大大降低 OOM 的概率,但除非万不得已的情况下,否则不要使用该属性。出现 OOM 后,我们首先应该排查整个 App,找出内存瓶颈予以解决。

查看 App 内存占用

  1. adb shell dumpsys meminfo [package_name]

该命令可以查看 App 所占用内存

通过这个命令,App 所占资源情况一目了然,甚至我们可以看到整个 App 中 View 个数、Activity 个数——这对于排查 Activity 泄漏和优化 View 层级也是非常有帮助的。

其中Activities的数量是一个非常关键的信息,可以帮助我们快速找出内存泄漏的页面,我们可以反复进入待测页面,如果反复进入退出后,查询内存的占用情况,Activity数量一直在增加,那说明一定是发生内存泄漏了。

AS Profiler

在确定了哪个页面发生内存泄漏后,用Android Studio 自带工具就可以直接分析泄漏的Activity,完全没必要再单独安装MAT了,如下图打开Android Studio 的profile进入内存模块:

点击Dump,反复进入退出发生内存泄漏的页面

勾选下面的Activity/Fragment Leaks 就可以展示出具体哪些Activity或 Fragment发生了内存泄漏,右边还有具体的引用情况

在线监测

快手KOOM

https://github.com/KwaiAppTeam/KOOM

作者:小楠总
链接:https://juejin.cn/post/6876258422296166407
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

避免OOM

https://zhuanlan.zhihu.com/p/58110093

减小对象的内存占用

避免OOM的第一步就是要尽量减少新分配出来的对象占用内存的大小,尽量使用更加轻量级的对象。

内存对象的重复利用

大多数对象的复用,最终实施的方案都是利用对象池技术,要么是在编写代码的时候显示的在程序里面去创建对象池,然后处理好复用的实现逻辑,要么就是利用系统框架既有的某些复用特性达到减少对象的重复创建,从而减少内存的分配与回收。最常用的一个缓存算法是LRU(Least Recently Use)

避免对象的内存泄漏

内存对象的泄漏,会导致一些不再使用的对象无法及时释放,这样一方面占用了宝贵的内存空间,很容易导致后续需要分配内存的时候,空闲空间不足而出现OOM。显然,这还使得每级Generation的内存区域可用空间变小,GC就会更容易被触发,容易出现内存抖动,从而引起性能问题。

图片处理

大图监控

ASM 字节码插桩:监控大图加载
App运行时大图监控
Gradle 插件 + ASM 实战 - 监控图片加载告警

拾遗

实战:
https://juejin.cn/post/6844903829327052814

线下查看所有线程信息

https://blog.csdn.net/Jaden_hool/article/details/73457320
一次线程OOM排查看线程使用注意事项
https://www.jianshu.com/p/d22d643765d1

线程数过多,会导致内存紧张,也可能会造成OOM,所以我们需要关心进程的所有线程,那么如何查看所有的线程信息呢?

Android CPU Profiler

ps 命令

根据包名查看当前进程

  1. adb shell ps | grep xxx

得到当前进程pid或名字则查看当前所有的线程

  1. adb shell ps -T | grep 6661

这样就可以看到当前的所有的线程了,可以使用wc -l来统计线程数量。

top命令

top命令可以实时显示各个线程情况

watch命令

我们并不清楚到底是哪个部分有问题导致的线程数的增长,所以我们需要一个每1s可以打印一下当前的线程数再通过页面交互来确定到底是哪里出现的问题,可以使用watch命令来完成我们的想法,如下所示:

  1. watch -n 1 -d 'adb shell ps -T | grep u0_a589 | wc -l'

仔细观察了所有线程名的输出发现以OkHttp Connect 和 pool-前缀的线程非常之多,我们知道线程池里默认创建的线程名称就是以pool-来命名的。

epic

一般来说,在我们的项目里,不仅仅自己使用了线程/线程池,还有许多的第三方SDK也是用了线程/线程池,那怎么去排查这个问题呢?

我们使用了epic这个库来做hook,可以监控到当前的线程创建,在里面打印了堆栈信息便于我们排查,如下所示:

  1. private void hookThread() {
  2. DexposedBridge.hookAllConstructors(Thread.class, new XC_MethodHook() {
  3. @Override
  4. protected void afterHookedMethod(MethodHookParam param) throws Throwable {
  5. super.afterHookedMethod(param);
  6. Thread thread = (Thread) param.thisObject;
  7. Class<?> clazz = thread.getClass();
  8. if (clazz != Thread.class) {
  9. Log.d(ThreadMethodHook.TAG, "found class extend Thread:" + clazz);
  10. DexposedBridge.findAndHookMethod(clazz, "run", new ThreadMethodHook());
  11. }
  12. Log.d(ThreadMethodHook.TAG, "Thread: " + thread.getName() + " class:" + thread.getClass() + " is created.");
  13. Log.d(ThreadMethodHook.TAG, "Thread:" + thread.getName() + "stack:" + Log.getStackTraceString(new Throwable()));
  14. }
  15. });
  16. }

手动打印

打印当前的所有线程,名字、状态和堆栈信息

  1. private void printThread() {
  2. Map<Thread, StackTraceElement[]> stacks = Thread.getAllStackTraces();
  3. Set<Thread> set = stacks.keySet();
  4. for (Thread key : set) {
  5. StackTraceElement[] stackTraceElements = stacks.get(key);
  6. android.util.Log.e("TryLoveCatch", "---- print thread: " + key.getName() + ", state: " + key.getState() + " start ----");
  7. for (StackTraceElement st : stackTraceElements) {
  8. android.util.Log.e("TryLoveCatch", "StackTraceElement: " + st.toString());
  9. }
  10. android.util.Log.e("TryLoveCatch", "---- print thread: " + key.getName() + ", state: " + key.getState() + " end ----");
  11. }
  12. }

打印同一个名字的线程数量

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注