阅读 718

内存泄漏检测方案总结(持续更新)

内存泄漏处理的一般思路是先通过adb命令看看系统整体的运行情况,查看进程的堆、activity页面增加趋势,然后通过通过dump当前运行快照,借助更专业化的工具来分析,比如MAT、LeakCanary。下面按照一般思路分步骤介绍具体操作。 这里需要提示一下,我们说的内存一般是指主内存,不是指cpu高级缓存和寄存器那些位置。

内存泄漏可能导致两个主要问题: 1、OOM,这个倒不一定是真的空余内存不足,有可能是内存中碎片太多,导致能够装下一个稍大对象的新空间都没有了,也可能是线程数超过限制,这个系统是有限制的,或者文件句柄数超过上限(by the way, 线程数超标了,说明业务有问题,建议换成线程池,或者换成协程) 2、空余内存越来越少,系统为了腾出空间频繁GC,导致卡顿、延迟

检查内存常用的命令

dumpsys meminfo

内存指标概念 Item 全称 含义 等价 USS Unique Set Size 物理内存 进程独占的内存 PSS Proportional Set Size 物理内存 PSS= USS+ 按比例包含共享库 RSS Resident Set Size 物理内存 RSS= USS+ 包含共享库 VSS Virtual Set Size 虚拟内存 VSS= RSS+ 未分配实际物理内存

一般我们看PSS,这个item能比较实际地反应进程所占的内存。这个指令罗列了根据各种指标排序的各个进程的PSS

  1. Total PSS by process

根据不同进程占据的内存大小来排序

image.png

  1. Total PSS by OOM adjustment

根据OOM时候杀进程的优先级,在需要杀进程留出空间的时候,从列表底部到高位依次杀掉进程

image.png

进程OOM kill优先级说明 android/frameworks/base/services/core/java/com/android/server/am/ProcessList.java

    // (Generally this is something that is going to be cached, but we
    // don't know the exact value in the cached range to assign yet.)
    static final int UNKNOWN_ADJ = 1001;

    // This is a process only hosting activities that are not visible,
    // so it can be killed without any disruption.
    static final int CACHED_APP_MAX_ADJ = 906;
    static final int CACHED_APP_MIN_ADJ = 900;

    // The B list of SERVICE_ADJ -- these are the old and decrepit
    // services that aren't as shiny and interesting as the ones in the A list.
    static final int SERVICE_B_ADJ = 800;

    // This is the process of the previous application that the user was in.
    // This process is kept above other things, because it is very common to
    // switch back to the previous app.  This is important both for recent
    // task switch (toggling between the two top recent apps) as well as normal
    // UI flow such as clicking on a URI in the e-mail app to view in the browser,
    // and then pressing back to return to e-mail.
    static final int PREVIOUS_APP_ADJ = 700;

    // This is a process holding the home application -- we want to try
    // avoiding killing it, even if it would normally be in the background,
    // because the user interacts with it so much.
    static final int HOME_APP_ADJ = 600;

    // This is a process holding an application service -- killing it will not
    // have much of an impact as far as the user is concerned.
    static final int SERVICE_ADJ = 500;

    // This is a process with a heavy-weight application.  It is in the
    // background, but we want to try to avoid killing it.  Value set in
    // system/rootdir/init.rc on startup.
    static final int HEAVY_WEIGHT_APP_ADJ = 400;

    // This is a process currently hosting a backup operation.  Killing it
    // is not entirely fatal but is generally a bad idea.
    static final int BACKUP_APP_ADJ = 300;

    // This is a process only hosting components that are perceptible to the
    // user, and we really want to avoid killing them, but they are not
    // immediately visible. An example is background music playback.
    static final int PERCEPTIBLE_APP_ADJ = 200;

    // This is a process only hosting activities that are visible to the
    // user, so we'd prefer they don't disappear.
    static final int VISIBLE_APP_ADJ = 100;

    // This is the process running the current foreground app.  We'd really
    // rather not kill it!
    static final int FOREGROUND_APP_ADJ = 0;

    // This is a process that the system or a persistent process has bound to,
    // and indicated it is important.
    static final int PERSISTENT_SERVICE_ADJ = -700;

    // This is a system persistent process, such as telephony.  Definitely
    // don't want to kill it, but doing so is not completely fatal.
    static final int PERSISTENT_PROC_ADJ = -800;

    // The system process runs at the default adjustment.
    static final int SYSTEM_ADJ = -900;

    // Special code for native processes that are not being managed by the system (so
    // don't have an oom adj assigned by the system).
    static final int NATIVE_ADJ = -1000;
复制代码
  1. Total PSS by category

该标签下统计系统中所有进程每个类别占用内存的总和,具体每个的含义后面按app来解释。

image.png

  1. 最后是整体内存使用信息的总结

其中Total PSS by OOM adjustment 列表的Cached标签下的进程是随时可以回收内存的缓存进程,所以该部分内存在后面会统计到Free RAM字段中

image.png

在命令后面加上 -package "package name" 可以看到指定进程的内存占用情况

image.png

一般我们看Dalvik Heap这个指标,右上角的Heap Size表示的是这个进程占用的总堆大小,分析整体情况看底部的total项。 下面是详细的参数说明:

  • Pss Total:是一个进程实际使用的内存,该统计方法包括比例分配共享库占用的内存,即如果有三个进程共享了一个共享库,则平摊分配该共享库占用的内存。Pss Total统计方法的一个需要注意的地方是如果使用共享库的一个进程被杀死,则共享库的内存占用按比例分配到其他共享该库的进程中,而不是将内存资源返回给系统,这种情况下PssTotal不能够准确代表内存返回给系统的情况。
  • Private Dirty:进程私有的脏页内存大小,该统计方法只包括进程私有的被修改的内存。
  • Private Clear:进程私有的干净页内存大小,该统计方法只包括进程私有的没有被修改的内存。
  • Swapped Dirty:被交换的脏页内存大小,该内存与其他进程共享。其中private Dirty + private Clean = Uss,该值是一个进程的使用的私有内存大小,即这些内存唯一被该进程所有。该统计方法真正描述了运行一个进程需要的内存和杀死一个进程释放的内存情况,是怀疑内存泄露最好的统计方法。共享比例:sharing_proportion = (Pss Total - private_clean - private_dirty) /(shared_clean+shared_dirty) 能够被共享的内存:swappable_pss = (sharing_proportion * shared_clean) + private_clean
  • Native Heap:本地堆使用的内存,包括C/C++在堆上分配的内存
  • Dalvik Heap:dalvik虚拟机使用的内存
  • Dalvik other:除Dalvik和Native之外分配的内存,包括C/C++分配的非堆内存
  • Cursor:数据库游标文件占用的内存
  • Ashmem:匿名共享内存
  • Stack:Dalvik栈占用的内存
  • Other dev:其他的dev占用的内存
  • .so mmap:so库占用的内存
  • .jar mmap:.jar文件占用的内存
  • .apk mmap:.apk文件占用的内存
  • .ttf mmap:.ttf文件占用的内存
  • .dex mmap:.dex文件占用的内存
  • image mmap:图像文件占用的内存
  • code mmap:代码文件占用的内存
  • Other mmap:其他文件占用的内存
  • Graphics:GPU使用图像时使用的内存
  • GL:GPU使用GL绘制时使用的内存
  • Memtrack:GPU使用多媒体、照相机时使用的内存
  • Unknown:不知道的内存消耗
  • Heap Size:堆的总内存大小
  • Heap Alloc:堆分配的内存大小
  • Heap Free:堆待分配的内存大小
  • Native Heap | Heap Size : 从mallinfo usmblks获的,当前进程Native堆的最大总共分配内存
  • Native Heap | Heap Alloc : 从mallinfo uorblks获的,当前进程navtive堆的总共分配内存
  • Native Heap | Heap Free : 从mallinfo fordblks获的,当前进程Native堆的剩余内存
  • Native Heap Size ≈ Native Heap Alloc + Native Heap Free
  • mallinfo是一个C库,mallinfo()函数提供了各种各样通过malloc()函数分配的内存的统计信息
  • Dalvik Heap | Heap Size : 从Runtime totalMemory()获得,Dalvik Heap总共的内存大小
  • Dalvik Heap | Heap Alloc : 从Runtime totalMemory() - freeMemory()获得,Dalvik Heap分配的内存大小
  • Dalvik Heap | Heap Free : 从Runtime freeMemory()获得,Dalvik Heap剩余的内存大小Dalvik Heap Size = Dalvik Heap Alloc + Dalvik Heap Free

image.png

Obejcts 当前进程中的对象个数

  • Views: 当前进程中实例化的视图View对象数量
  • ViewRootImpl:当前进程中实例化的视图根ViewRootImpl对象数量,代表一个窗口
  • AppContexts:当前进程中实例化的应用上下文ContextImpl对象数量
  • Activities:当前进程中实例化的Activity对象数量
  • Assets:当前进程的全局资产数量
  • AssetManagers:当前进程的全局资产管理数量
  • Local Binders:当前进程有效的本地binder对象数量
  • Proxy Binders:当前进程中引用的远程binder对象数量
  • Death Recipients:当前进程到binder的无效链接数量
  • OpenSSL Sockets:安全套接字对象数量

SQL

  • MEMORY_USED:当前进程中数据库使用的内存数量,kb
  • PAGECACHE_OVERFLOW:页面缓存的配置不能够满足的数量,kb
  • MALLOC_SIZE: 向sqlite3请求的最大内存分配数量,kb

DATABASES

  • pgsz:数据库的页面大小
  • dbsz:数据库大小
  • Lookaside(b):后备使用的内存大小
  • cache:数据缓存状态
  • Dbname:数据库表名
  • Asset Allocations 资源路径和资源大小

知道了这些参数的含义,那据此我们分析内存泄漏的惯常方法应该是什么呢? 很简单,可以反复执行某个被怀疑的用例,然后观察dumpsys meminfo 指令结果中展示的heap size大小,如果堆大小一直在增加,那很可能是有内存泄漏,或者指定包名,观察指令结果中activity数量,如果数量一直在增加,很可能activity内存泄漏了。

procrank

获取所有进程的内存使用的排行榜,排行是以 Pss 的大小而排序。 procrank 命令比 dumpsys meminfo 命令,能输出更详细的VSS/RSS/PSS/USS内存指标。

image.png

cat /proc/meminfo

能否查看更加详细的内存信息 image.png

free

查看可用内存,缺省单位KB。该命令比较简单、轻量,专注于查看剩余内存情况。

image.png

showmap

image.png

vmstat

image.png

image.png

小结

  1. dumpsys meminfo 适用场景: 查看进程的oom adj,或者dalvik/native等区域内存情况,或者某个进程或apk的内存情况,功能非常强大;
  2. procrank 适用场景: 查看进程的VSS/RSS/PSS/USS各个内存指标;
  3. cat /proc/meminfo 适用场景: 查看系统的详尽内存信息,包含内核情况;
  4. free 适用场景: 只查看系统的可用内存;
  5. showmap 适用场景: 查看进程的虚拟地址空间的内存分配情况;
  6. vmstat 适用场景: 周期性地打印出进程运行队列、系统切换、CPU时间占比等情况;

参考蚊香: www.jianshu.com/p/37539308f…

MAT工具的适使用

可以通过Android Studio的profile组件dump hprof文件,也可以通过代码自定义实现hprof文件的保存,前提是需要申请文件读写权限,代码如下

private static final int REQUEST_EXTERNAL_STORAGE = 1;
    private static String[] PERMISSIONS_STORAGE = {
            "android.permission.READ_EXTERNAL_STORAGE",
            "android.permission.WRITE_EXTERNAL_STORAGE"};
            
public static void verifyStoragePermissions(AppCompatActivity activity) {

        try {
            //检测是否有写的权限
            int permission = ActivityCompat.checkSelfPermission(activity,
                    "android.permission.WRITE_EXTERNAL_STORAGE");
            if (permission != PackageManager.PERMISSION_GRANTED) {
                // 没有写的权限,去申请写的权限,会弹出对话框
                ActivityCompat.requestPermissions(activity, PERMISSIONS_STORAGE, REQUEST_EXTERNAL_STORAGE);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
public static boolean createDumpFile(Context context) {
        Log.i("Sky", "开始dump...");
        String LOG_PATH = "/dump.gc/";
        boolean bool = false;
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH.mm.ssss");
        String createTime = sdf.format(new Date(System.currentTimeMillis()));
        String state = android.os.Environment.getExternalStorageState();
        // 判断SdCard是否存在并且是可用的
        if (android.os.Environment.MEDIA_MOUNTED.equals(state)) {
            File file = new File(Environment.getExternalStorageDirectory().getPath() + LOG_PATH);
            if (!file.exists()) {
                file.mkdirs();
            }
            String hprofPath = file.getAbsolutePath();
            if (!hprofPath.endsWith("/")) {
                hprofPath += "/";
            }

            hprofPath += createTime + ".hprof";
            try {
                android.os.Debug.dumpHprofData(hprofPath);
                bool = true;
                Log.d("ANDROID_LAB", "create dumpfile done!");
            } catch (IOException e) {
                e.printStackTrace();
            }
        } else {
            bool = false;
            Log.d("ANDROID_LAB", "nosdcard!");
        }

        return bool;
    }
复制代码

不过代码自定义的方式创造出来的hprof文件是不能直接被MAT工具打开的,需要通过 hprof-conv [source file] [target file]的指令转换一下,不想转换也行,直接拖到AS的profile窗口中也能解析,实践发现转换后的数据可能有丢失和差错,比如shallow heap, retained heap在MAT中就不能精确显示。此外,当前内存快照中对象越多,dump出的文件也越大。 通过我自己的实践发现,初步的内存泄漏分析用profile足矣,甚至不需要shell 命令和hprof文件。

新手使用的时候需要注意设置MAT的显示单位,如果调整成以MB显示,可能你的整个列表shallow heap 和 retained heap都是0,如果担心这种误导现象,建议改成以B显示。

image.png

MAT工具在加载hprof文件后hi显示它认为有嫌疑的问题,这些各位客观看看就好,一般不靠谱,呵呵。 我们最常用的分析技巧是分析。。。。先介绍一下这个工具的一些属性:

  • Shallow Heap 对象本身占有的内存
  • Retained Heap 对象和持有的引用所牵扯的其他对象加起来的总内存,或者这样理解————该对象回收后除了对象本身连带其能一并释放的其他对象的总内存大小,这是一个关键参数
  • Incoming Reference 查询该对象被谁持有
  • Outgoing Reference 查询该对象持有谁

好,通过一个Thread持有一个包含大量list对象的map的例子来演示一下具体的功能。

我们一般打开hprof文件的Histogram视图,然后通过包名排序列表,找到你发现Retained Heap比较大的对象,查看它的Incoming 或者 Outgoing reference,找到引用链中持有大量内存不释放的真凶。

image.png

或者通过排除所有软/虚 /弱引用后查看谁在引用该对象

image.png

或者通过Dominator tree 视图查看占用内存最多的对象和它对外引用了那些其他对象,用树状展示出来

image.png

兵无定法,通过不同的角度都可以定位出问题元凶就是Thread对象,它不结束,里面包含的很多数据不释放,导致泄漏。

LeakCanary原理

文章分类
Android
文章标签