• Android Koom 处理 app 的OOM 一些系列问题(java /native/thread leak)


    本篇文档是基于快手团队的Koom 2.2.0 的tag 版本的使用介绍。

    前期工作

        VERSION_NAME=2.2.0
        // 引入koom 的静态库,这里版本2.2.0
        implementation "com.kuaishou.koom:koom-native-leak-static:${VERSION_NAME}"
        implementation "com.kuaishou.koom:koom-java-leak-static:${VERSION_NAME}"
        implementation "com.kuaishou.koom:koom-thread-leak-static:${VERSION_NAME}"
        implementation "com.kuaishou.koom:xhook-static:${VERSION_NAME}"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    使用快手发布koom 的静态库,通过源码编译,可能遇到一些问题,编译不通过。
    更多信息,请阅读快手 KOOM 详细文档

    1.JavaLeak

    1.1 koom输出java 泄漏的json 信息

    该json中包含:

    • runningInfo: app 当前进程信息,包含线程数、fd 数据等关键信息

    • gcPaths: 触发gc的对象的调用链

    • leakObjects:泄漏对象

    • classInfos:类信息

    先来看下,leakObjects:

    [
    {
    "className":"android.graphics.Bitmap",
    "extDetail":"1920 x 1080",
    "objectId":"327801464",
    "size":"2073600"
    },
    {
    "className":"int[]",
    "objectId":"1972002816",
    "size":"455869"
    },
    {
    "className":"byte[]",
    "objectId":"1973350400",
    "size":"524301"
    },
    {
    "className":"char[]",
    "objectId":"1974407184",
    "size":"1048589"
    }
    ]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    从上面看,可知有bimap 和数组存在泄漏,但无更详细信息。

    接下来看下gcPaths中一部分信息:

    {
    "gcRoot":"Local variable in native code",
    "instanceCount":1,
    "leakReason":"Bitmap Size Over Threshold, 1920x1080",
    "path":[
    {
    "declaredClass":"java.lang.ClassLoader",
    "reference":"dalvik.system.PathClassLoader.runtimeInternalObjects",
    "referenceType":"INSTANCE_FIELD"
    },
    {
    "declaredClass":"java.lang.Object[]",
    "reference":"java.lang.Object[]",
    "referenceType":"ARRAY_ENTRY"
    },
    {
    "declaredClass":"com.kwai.koom.demo.javaleak.test.LeakMaker",
    "reference":"com.kwai.koom.demo.javaleak.test.LeakMaker.leakMakerList",
    "referenceType":"STATIC_FIELD"
    },
    {
    "declaredClass":"java.util.ArrayList",
    "reference":"java.util.ArrayList.elementData",
    "referenceType":"INSTANCE_FIELD"
    },
    {
    "declaredClass":"java.lang.Object[]",
    "reference":"java.lang.Object[]",
    "referenceType":"ARRAY_ENTRY"
    },
    {
    "declaredClass":"com.kwai.koom.demo.javaleak.test.LeakMaker",
    "reference":"com.kwai.koom.demo.javaleak.test.BitmapLeakMaker.uselessObjectList",
    "referenceType":"INSTANCE_FIELD"
    },
    {
    "declaredClass":"java.util.ArrayList",
    "reference":"java.util.ArrayList.elementData",
    "referenceType":"INSTANCE_FIELD"
    },
    {
    "declaredClass":"java.lang.Object[]",
    "reference":"java.lang.Object[]",
    "referenceType":"ARRAY_ENTRY"
    },
    {
    "reference":"android.graphics.Bitmap",
    "referenceType":"instance"
    }
    ],
    "signature":"38ba5ba71b7599737372f965417abcf2765dbb2a"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    从gc 调用链看出,bitmap 被LeakMaker持有,LeakMaker 被BitmapLeakMaker持有,BitmapLeakMaker被LeakMaker 中静态leakMakerList持有,导致bitmap 一直无法被释放。

    接下来看下runningInfo 的部分信息:

    {
    "buildModel":"PCLM50",
    "currentPage":"javaleak.JavaLeakTestActivity",
    "deviceMemAvaliable":"3643.6367",
    "deviceMemTotal":"7398.6797",
    "dumpReason":"reason_thread_oom",
    "fdCount":"138",
    "filterInstanceTime":"1.837",
    "findGCPathTime":"16.967",
    "jvmMax":"384.0",
    "jvmUsed":"6.4137344",
    "manufacture":"OPPO",
    "nowTime":"2022-08-17_15-29-50_432",
    "pss":"125.66699mb",
    "rss":"161.82812mb",
    "sdkInt":"31",
    "threadCount":"725"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    从上面信息,可知 线程是725个,fd 是138个,当前页面是JavaLeakTestActivity等关键信息。

    1.2 studio 解析hprof 文件

    接下来,通过studio 解析下koom 生成的泄漏hprof 文件(sdcard/android/data/包名/files/performance/oom/memory/hrof-aly 目录下)。

    先查看下UI(framgent/activity)泄漏:
    在这里插入图片描述

    接下来看下,json 文件中bitmap 泄漏的情况:

    在这里插入图片描述

    更多hprof 文件解读,自行百度。

    2.NativeLeak:

    2.1查看logcat 中输出的native 泄漏的日志

    2022-08-09 11:21:21.987 15584-15696/com.kwai.koom.demo I/NativeLeakTestActivity: Activity: com.kwai.koom.demo.nativeleak.NativeLeakTestActivity@36fb614
       //.......
        LeakSize: 24 Byte
        LeakThread: .kwai.koom.demo
        Backtrace:
        #0 pc 0x1d9c  libnative-leak-test.so
        #1 pc 0x190c  libnative-leak-test.so
        #2 pc 0xda278  libc.so
        #3 pc 0x7a448  libc.so
        
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    2.2 借用android ndk 工具(ndk-stack或者addr21line )来定位代码位置。

    执行addr2line的相关命令:
    在这里插入图片描述
    从上面可以看出native-leak-test.cpp 文件中93行:

    static NOINLINE void TestContainerLeak() {
      std::vector str_vector(NR_TEST_CASE);
      for (int i = 0; i < NR_TEST_CASE; i++) {
        str_vector[i] = new std::string("test_leak_container");
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    c++ 与java 是很大不同,没有gc 垃圾回收机制,在c++ 中 听new 开辟的内存,必须手动delete 删除。从上面代码可见,通过new 创建了string 指针后,执行完TestContainerLeak()后并没有delete删除 该内存,因此造成native 泄漏。

    3.ThreadLeakMonitor使用

    3.1了解下Koom中线程泄漏的案例

    先来解读下Koom中线程泄漏案例的代码:

    static NOINLINE void TestThreadLeak(int64_t delay) {
      //这里使用的是c++ thread,使用lamba表达式方式来创建线程,在c++ 函数也是指针。
      std::thread test_thread([](int64_t delay) {
        //设置线程名称为test_thread
        pthread_setname_np(pthread_self(), "test_thread");
        LOGI("test_thread run");
        // 声明线程指针
        std::thread *test_thread_1;
        std::thread *test_thread_2;
        test_thread_1 = new std::thread([]() {
          pthread_setname_np(pthread_self(), "test_thread_1");
          LOGI("test_thread_1 run");
        });
        test_thread_2 = new std::thread([]() {
          pthread_setname_np(pthread_self(), "test_thread_2");
          LOGI("test_thread_2 run");
        });
        // 沉睡delay 秒时间,再调用 上面线程的detach()和join()
        std::this_thread::sleep_for(std::chrono::milliseconds(delay));
        test_thread_1->detach();
        LOGI("test_thread_1 detach");
        test_thread_2->join();
        LOGI("test_thread_2 join");
      }, delay);
      test_thread.detach();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    简单来说,创建一个名为test_thread的线程,在其内部开启两个线程test_thread1和test_thread2 ,沉睡指定时间后,再调用它两的detach()和join()。

    接下来,看下实际的效果:

    3.2 查看线程泄漏的日志

    当点击测试案例,线程test_thread 开启线程test_thread2和test_thread_1, 沉睡10秒后再调用它两的join()或者detach()。
    先是在logcat中输出一下日志:

    2022-08-09 09:57:13.334 13961-26723/com.kwai.koom.demo I/ThreadLeakTest: test_thread run
    2022-08-09 09:57:13.335 13961-26726/com.kwai.koom.demo I/ThreadLeakTest: test_thread_2 run
    2022-08-09 09:57:13.335 13961-26724/com.kwai.koom.demo I/ThreadLeakTest: test_thread_1 run
    
    • 1
    • 2
    • 3

    监控thread 没有执行join()或者detach()方法下,执行了pthread_exit,则记录下泄露线程信息。
    2022-08-09 09:57:13到2022-08-09 09:57:18的间隔时间是5秒,刚好是enableThreadLeakCheck(2 * 1000L, 5 * 1000L)中的泄漏时间,超过这个时间,就会上报线程泄漏信息。更多详细日志如下所示:

    2022-08-09 09:57:18.538 13961-14017/com.kwai.koom.demo I/ThreadLeakTest: tid: 26726
        createTime: 284943128292812 Byte
        startTime: 284943128354687
        endTime: 284943128382916
        name: test_thread_2
        createCallStack:
          #00 pc 0000000000002084  /data/app/~~UA5bVzbMO-QDKhKIgEGpxg==/com.kwai.koom.demo-Fnd4kAPgIstzWnyszO4chg==/lib/arm64/libnative-leak-test.so (BuildId: b3e2c22d2f281ecd24ed2bdd07577439)
          #01 pc 00000000000da278  /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+64) (BuildId: 1ca28d785d6567d2b225cf978ef04de5)
          #02 pc 000000000007a448  /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: 1ca28d785d6567d2b225cf978ef04de5)
    2022-08-09 09:57:18.538 13961-14017/com.kwai.koom.demo I/ThreadLeakTest: tid: 26724
        createTime: 284943128217864 Byte
        startTime: 284943128528853
        endTime: 284943128671353
        name: test_thread_1
        createCallStack:
          #00 pc 000000000000203c  /data/app/~~UA5bVzbMO-QDKhKIgEGpxg==/com.kwai.koom.demo-Fnd4kAPgIstzWnyszO4chg==/lib/arm64/libnative-leak-test.so (BuildId: b3e2c22d2f281ecd24ed2bdd07577439)
          #01 pc 00000000000da278  /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+64) (BuildId: 1ca28d785d6567d2b225cf978ef04de5)
          #02 pc 000000000007a448  /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: 1ca28d785d6567d2b225cf978ef04de5)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    当然Koom 的线程监控并不影响自身线程的逻辑,2022-08-09 09:57:13到2022-08-09 09:57:23,期间刚好沉睡10秒后,会再调用它两的join()或者detach(),以下日志也刚好验证。

    2022-08-09 09:57:23.335 13961-26723/com.kwai.koom.demo I/ThreadLeakTest: test_thread_1 detach
    2022-08-09 09:57:23.335 13961-26723/com.kwai.koom.demo I/ThreadLeakTest: test_thread_2 join。
    
    • 1
    • 2
  • 相关阅读:
    高并发下Redis缓存与数据库双写一致性问题原理分析和解决方案
    62. 不同路径-动态规划-双百代码
    【Linux】进程控制
    java-php-python-ssm计算机类专业考研交流学习平台计算机毕业设计
    开发模型>螺旋模型
    【数据结构】图的遍历、图的应用
    websocket flv 客户端解封包
    手撕 视觉slam14讲 ch13 代码(5)双目初始化 StereoInit()
    Python二分查找的左闭右闭和左闭右开
    2021年软件测试面试题大全
  • 原文地址:https://blog.csdn.net/hexingen/article/details/126388563