Android 系统稳定性 - OOM(二)

2.3 如何分析内存溢出问题

无论怎么小心,想完全避免 bad code 是不可能的,此时就需要一些工具来帮助我们检查代码中是否存在会造成内存泄漏的地方。

既然要排查的是内存问题,自然需要与内存相关的工具,DDMS和MAT就是两个非常好的工具。下面详细介绍。

2.3.1 内存监测工具 DDMS --> Heap

Android tools 中的 DDMS 就带有一个很不错的内存监测工具 Heap(这里我使用 eclipse 的 ADT 插件,并以真机为例,在模拟器中的情况类似)。用 Heap 监测应用进程使用内存情况的步骤如下:

  1. 启动 eclipse 后,切换到 DDMS 透视图,并确认 Devices 视图、Heap 视图都是打开的;

  2. 将手机通过 USB 链接至电脑,链接时需要确认手机是处于“USB 调试”模式,而不是作为“Mass Storage”;

  3. 链接成功后,在 DDMS 的 Devices 视图中将会显示手机设备的序列号,以及设备中正在运行的部分进程信息;

  4. 点击选中想要监测的进程,比如 system_process 进程;

  5. 点击选中 Devices 视图界面中最上方一排图标中的“Update Heap”图标;

  6. 点击 Heap 视图中的“Cause GC”按钮;

  7. 此时在 Heap 视图中就会看到当前选中的进程的内存使用量的详细情况[如图所示]。


 

说明:

  • 点击“Cause GC”按钮相当于向虚拟机请求了一次 gc 操作;

  • 当内存使用信息第一次显示以后,无须再不断的点击“Cause GC”,Heap 视图界面会定时刷新,在对应用的不断的操作过程中就可以看到内存使用的变化;

  • 内存使用信息的各项参数根据名称即可知道其意思,在此不再赘述。如何才能知道我们的程序是否有内存泄漏的可能性呢。这里需要注意一个值:Heap 视图中部有一个 Type 叫做 data object,即数据对象,也就是我们的程序中大量存在的类类型的对象。在 data object 一行中有一列是“Total Size”,其值就是当前进程中所有 Java 数据对象的内存总量,一般情况下,这个值的大小决定了是否会有内存泄漏。可以这样判断:

  • 不断的操作当前应用,同时注意观察 data object 的 Total Size 值;

  • 正常情况下 Total Size 值都会稳定在一个有限的范围内,也就是说由于程序中的代码良好,没有造成对象不被垃圾回收的情况,所以说虽然我们不断的操作会不断的生成很多对象 ,而在虚拟机不断的进行GC 的过程中,这些对象都被回收了,内存占用量会会落到一个稳定的水平;

  • 反之如果代码中存在没有释放对象引用的情况,则 data object 的 Total Size 值在每次 GC后不会有明显的回落,随着操作次数的增多 Total Size 的值会越来越大,直到到达一个上限后导致进程被 kill掉。

  • 此处已 system_process 进程为例,在我的测试环境中 system_process 进程所占用的内存的data object 的 Total Size 正常情况下会稳定在 2.2~2.8 之间,而当其值超过 3.55 后进程就会被kill。

总之,使用 DDMS 的 Heap 视图工具可以很方便的确认我们的程序是否存在内存泄漏的可能性。

2.3.2 内存分析工具 MAT(Memory Analyzer Tool)

如果使用 DDMS 确实发现了我们的程序中存在内存泄漏,那又如何定位到具体出现问题的代码片段,最终找到问题所在呢?如果从头到尾的分析代码逻辑,那肯定会把人逼疯,特别是在维护别人写的代码的时候。这里介绍一个极好的内存分析工具 -- Memory Analyzer Tool(MAT)。

MAT 是一个 Eclipse 插件,同时也有单独的 RCP 客户端。官方下载地址、MAT 介绍和详细的使用教程请参见:www.eclipse.org/mat,在此不进行说明了。另外在 MAT 安装后的帮助文档里也有完备的使用教程。在此仅举例说明其使用方法。我自己使用的是 MAT 的eclipse 插件,使用插件要比RCP 稍微方便一些。

MAT通过解析Hprof文件来分析内存使用情况。HPROF其实是在J2SE5.0中包含的用来分析CPU使用和堆内存占用的日志文件,实质上是虚拟机在某一时刻的内存快照,dalvik中也包含了这样的工具,但是其文件格式和JVM的格式不完全相同,可以用SDK中自带的hprof-conv工具进行转换,例如:

$./hprof-conv raw.hprof converted.hprof

可以使用hprof文件配合traceview来分析CPU使用情况(函数调用时间),此处仅仅讨论用它来分析内存使用情况,关于hprof的其他信息可以查看:http://java.sun.com/developer/technicalArticles/Programming/HPROF.html

以及Android源码中的/dalvik/docs/heap-profiling.html文件(这个比较重要,建议看看,例如kill -10在Android2.3中已经不支持了)。

使用 MAT 进行内存分析需要几个步骤,包括:生成.hprof 文件、打开 MAT 并导入hprof文件、使用MAT 的视图工具分析内存。以下详细介绍。

1. 生成hprof 文件

生成hprof 文件的方法有很多,而且 Android 的不同版本中生成hprof 的方式也稍有差别,我使用的版本的是 2.1,各个版本中生成hprof 文件的方法请参考:

http://android.git.kernel.org/?p=platform/dalvik.git;a=blob_plain;f=docs/heap-profiling.html;hb=HEAD。

(1) 打开 eclipse 并切换到 DDMS 透视图,同时确认 Devices、Heap 和 logcat 视图已经打开了 ;

(2) 将手机设备链接到电脑,并确保使用“USB 调试”模式链接,而不是“Mass Storage“模式;

(3) 链接成功后在 Devices 视图中就会看到设备的序列号,和设备中正在运行的部分进程;

(4) 点击选中想要分析的应用的进程,在 Devices 视图上方的一行图标按钮中,同时选中“Update Heap”和“Dump HPROF file”两个按钮;

(5) 这是 DDMS 工具将会自动生成当前选中进程的.hprof 文件,并将其进行转换后存放在sdcard当中,如果你已经安装了 MAT 插件,那么此时 MAT 将会自动被启用,并开始对.hprof文件进行分析;

注意: (4)步和第(5)步能够正常使用前提是我们需要有 sdcard,并且当前进程有向 sdcard中写入的权限(WRITE_EXTERNAL_STORAGE),否则.hprof 文件不会被生成,在 logcat 中会显示诸如ERROR/dalvikvm(8574): hprof: can't open /sdcard/com.xxx.hprof-hptemp: Permission denied.的信息。

如果我们没有 sdcard,或者当前进程没有向 sdcard 写入的权限(如 system_process) 那我们可以这样做:

(6) 在当前程序中,例如 framework 中某些代码中,可以使用 android.os.Debug 中的:

public static void dumpHprofData(String fileName) throws IOException

方法,手动的指定.hprof 文件的生成位置。例如:

xxxButton.setOnClickListener(new View.OnClickListener() {

        public void onClick(View view) {

                android.os.Debug.dumpHprofData("/data/temp/myapp.hprof");

                ... ...

        }

}

上述代码意图是希望在 xxxButton 被点击的时候开始抓取内存使用信息,并保存在我们指定的位置:/data/temp/myapp.hprof,这样就没有权限的限制了,而且也无须用 sdcard。但要保证/data/temp 目录是存在的。这个路径可以自己定义,当然也可以写成 sdcard 当中的某个路径。

如果不确定进程什么时候会OOM,例如我们在跑Monkey的过程中出现了OOM,此时最好的办法就是让程序在出现OOM之后,而没有将OOM的错误信息抛给虚拟机之前就将进程的hprof抓取出来。方法也很简单,只需要在代码中你认为会抛出OutOfMemoryError的地方try...catch,并在catch块中使用android.os.Debug.dumpHprofData(String file)方法就可以请求虚拟机dump出hprof到你指定的文件中。例如我们之前为了排查应用进程主线程中发生的OOM,就在ActivityThread.main()方法中添加了以下代码:

try {

        Looper.loop();

} catch (OutOfMemoryError e) {

        String file = "path_to_file.hprof"

        ... ...

        try {

                android.os.Debug.dumpHprofData(file);

        } catch (IOException e1) {

                e1.printStackTrace();

        }

}

在设置hprof的文件路径时,需要考虑权限问题,包括SD卡访问权限、/data分区私有目录访问权限。

之所以在以上位置添加代码,是因为在应用进程主线程中如果发生异常和错误没有捕获,最终都会从Looper.loop()中抛出来。如果你需要排查在其他线程,或者framework中的OOM问题时,同样可以在适当的位置使用android.os.Debug.dumpHprofData(String file)方法dump hprof文件。

有了hprof文件,并且用hprof-conv转换格式之后,第二步就可以用MemoryAnalyzerTool(MAT)工具来分析内存使用情况了。

2. 使用 MAT 导入hprof 文件

(1) 如果是 eclipse 自动生成的hprof 文件,可以使用 MAT 插件直接打开(可能是比较新的 ADT才支持);

(2) 如 果 eclipse 自 动 生 成 的 .hprof 文 件 不 能 被 MAT 直 接 打 开 , 或 者 是 使 用android.os.Debug.dumpHprofData()方法手动生成的hprof 文件,则需要将hprof 文件进行转换,转换的方法:

例如我将hprof 文件拷贝到 PC 上的/ANDROID_SDK/tools 目录下,并输入命令 hprof-conv xxx.hprof yyy.hprof,其中 xxx.hprof 为原始文件,yyy.hprof 为转换过后的文件。转换过后的文件自动放在/ANDROID_SDK/tools 目录下。OK,到此为止,hprof 文件处理完毕,可以用来分析内存泄露情况了。

(3) 在 Eclipse 中点击 Windows->Open Perspective->Other->Memory Analyzer,或者打 Memory Analyzer Tool 的 RCP。在 MAT 中点击 File->Open File,浏览并导入刚刚转换而得到的hprof文件。

3. 使用 MAT 的视图工具分析内存

导入hprof 文件以后,MAT 会自动解析并生成报告,点击 Dominator Tree,并按 Package分组,选择自己所定义的 Package 类点右键,在弹出菜单中选择 List objects->With incoming references。这时会列出所有可疑类,右键点击某一项,并选择 Path to GC Roots -> exclude weak/soft references,会进一步筛选出跟程序相关的所有有内存泄露的类。据此,可以追踪到代码中的某一个产生泄露的类。

MAT 的界面如下图所示。



 

了解 MAT 中各个视图的作用很重要,例如 www.eclipse.org/mat/about/screenshots.php 中介绍的。

总之使用 MAT 分析内存查找内存泄漏的根本思路,就是找到哪个类的对象的引用没有被释放,找到没有被释放的原因,也就可以很容易定位代码中的哪些片段的逻辑有问题了。下一节将用一个示例来说明MAT详细的使用过程。

2.3.3 MAT使用方法

1. 构建演示程序

首先需要构建一个演示程序,并获取hprof文件。程序很简单,按下Button后就循环地new自定义对象SomeObj,并将对象add到ArrayList中,直到抛出OutOfMemoryError,此时会捕获该错误,同时使用android.os.Debug.dumpHprofData方法dump该进程的内存快照到/sdcard/oom.hprof文件中。

package com.demo.oom;

 

import java.io.IOException;

import java.util.ArrayList;

 

import android.app.Activity;

import android.os.Bundle;

import android.widget.Button;

import android.view.View;

 

publicclass OOMDemoActivity extends Activity implements View.OnClickListener {

privatestaticfinal String HPROF_FILE = "/sdcard/oom.hprof";

private Button mBtn;

private ArrayList<SomeObj> list = new ArrayList<SomeObj>();

 

@Override

publicvoid onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.main);

 

        mBtn = (Button)findViewById(R.id.btn);

        mBtn.setOnClickListener(this);

}

 

@Override

publicvoid onClick(View v) {

        try {

                while (true) {

                        list.add(new SomeObj());

                }

        catch (OutOfMemoryError e) {

                try {

                        android.os.Debug.dumpHprofData(HPROF_FILE);

                        throw e;

                catch (IOException e1) {

                        e1.printStackTrace();

                }

        }

}

 

        private class SomeObj {

                private static final intDATA_SIZE = 1 * 1024 * 1024;

                private byte[] data;

                SomeObj() {

                        data = newbyte[DATA_SIZE];

                }

        }

}

因为要写入SDCard,所以要在AndroidManifest.xml中声明WRITE_EXTERNAL_STORAGE的权限。

注意:演示程序中是使用平台API来获取dump hprof文件的,你也可以使用ADT的DDMS工具来dump。每个hprof都是针对某一个Java进程的,如果你dump的是com.demo.oom进程的hprof,是无法用来分析system_server进程的内存情况的。

编译并运行程序最终会在SDCard中生成oom.hprof文件,log中会打印相关的日志信息,请留意红色字体:

I/dalvikvm(1238): hprof: dumping heap strings to "/sdcard/oom.hprof".

I/dalvikvm(1238): hprof: heap dump completed (21354KB)(虚拟机dump了hprof文件)

D/dalvikvm(1238): GC_HPROF_DUMP_HEAP freed <1K, 13% free 20992K/23879K, external 716K/1038K, paused 4034ms

D/AndroidRuntime(1238): Shutting down VM

W/dalvikvm(1238): threadid=1: thread exiting with uncaught exception (group=0x40015560)

E/AndroidRuntime(1238): FATAL EXCEPTION: main

E/AndroidRuntime(1238): java.lang.OutOfMemoryError(是OOM错误)

E/AndroidRuntime(1238): at com.demo.oom.OOMDemoActivity$SomeObj.<init>(OOMDemoActivity.java:45)

E/AndroidRuntime(1238): at com.demo.oom.OOMDemoActivity.onClick(OOMDemoActivity.java:29)

E/AndroidRuntime(1238): at android.view.View.performClick(View.java:2485)

E/AndroidRuntime(1238): at android.view.View$PerformClick.run(View.java:9080)

E/AndroidRuntime(1238): at android.os.Handler.handleCallback(Handler.java:587)

E/AndroidRuntime(1238): at android.os.Handler.dispatchMessage(Handler.java:92)

E/AndroidRuntime(1238): at android.os.Looper.loop(Looper.java:123)

E/AndroidRuntime(1238): at android.app.ActivityThread.main(ActivityThread.java:3683)

(从方法堆栈可以看到是应用进程的主线程中发生了OOM

E/AndroidRuntime(1238): at java.lang.reflect.Method.invokeNative(Native Method)

E/AndroidRuntime(1238): at java.lang.reflect.Method.invoke(Method.java:507)

E/AndroidRuntime(1238): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:839)

E/AndroidRuntime(1238): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:597)

E/AndroidRuntime(1238): at dalvik.system.NativeStart.main(Native Method)

W/ActivityManager(61): Force finishing activity com.demo.oom/.OOMDemoActivity

D/dalvikvm(229): GC_EXPLICIT freed 8K, 55% free 2599K/5703K, external 716K/1038K, paused 1381ms

W/ActivityManager(61): Activity pause timeout for HistoryRecord{406671e8 com.demo.oom/.OOMDemoActivity}

W/ActivityManager(61): Activity destroy timeout for HistoryRecord{406671e8 com.demo.oom/.OOMDemoActivity}

I/Process(1238): Sending signal. PID: 1238 SIG: 9(错误没有捕获被抛给虚拟机,最终被kill掉)

I/ActivityManager(61): Process com.demo.oom (pid 1238) has died.(应用进程挂掉了)

获取hprof文件后再用hprof-conv工具转换一下格式:

D:\work\android\sdk\tools>hprof-conv.exe C:\Users\ray\Desktop\oom.hprof C:\Users

\ray\Desktop\oom\oom.hprof(将转换后的hprof放到一个单独的目录下,因为分析时会生成很多中间文件)

2. MAT提供的各种分析工具

使用MAT导入转换后的hprof文件,导入时会让你选择报告类型,选择“Leak Suspects Report”即可。然后就可以看到如下的初步分析报告:



 

MAT在Overview视图中用饼图展示了内存的使用情况,列出了占用内存最大的Java对象com.demo.oom.OOMDemoActivity,我们可以根据这个线索来继续调查,但如果没有这样的提示,也可以根据自己推断来分析。在进一步分析之前,需要先熟悉MAT为我们提供的各种工具。

(1) Histogram

列出每个类的实例对象的数量,是第一个非常有用的分析工具。



 

可以看到该视图一共有四列,点击列名可以按照不同的列以升序或降序排序。每一列的含义为:

Class Name:类名

Objects:每一种类型的对象数量

Shallow Heap:一个对象本身(不包括该对象引用的其他对象)所占用的内存

Retained Heap:一个对象本身,以及由该对象引用的其他对象的Shallow Heap的总和。官方文档中解释为:Generally speaking, shallow heap of an object is its size in the heap and retained size of the same object is the amount of heap memory that will be freed when the object is garbage collected.

默认情况下该视图是按照Class来分类的,也可以点击工具栏中的选择不同的分类类型,这样可以更方便的筛选需要的信息。

默认情况下该视图只是粗略的计算了每种类型所有对象的Retained Heap,如果要精确计算的话可以点击工具栏中的来选择。

有时为了分析进程的内存使用情况,会对一个在不同的时间点抓取多个hprof文件来观察,MAT提供了一个非常好的工具来对比这些hprof文件,点击工具栏中的可以选择已经打开的其他hprof文件,选择后MAT将会对当前的hprof和要对比的hprof做一个插值,这样就可以很方便的观察对象的变化了。不过这个工具只有在Histogram视图中才有。

列表的第一行是一个搜索框,可以输入正则式或者数量来过滤列表的内容。



 

    1. (2) Dominator Tree

列出进程中所有的对象,是第二个非常有用的分析工具。



 

和Histogram不同的是左侧列的是对象而不是类(每个对象还有内存地址,例如@0x40516b08),而且还多了Percentage一列。

右键点击任意一个类型,会弹出一个上下文菜单:



 

菜单中有很多其他非常有用的功能,例如:

List Objects(with outgoing references/with incoming references):列出由该对象引用的其他对象/引用该对象的其他对象;

Open Source File:打开该对象的源码文件;

Path To GC Roots:由当前对象到GC Roots引用链

GC Roots:A garbage collection root is an object that is accessible from outside the heap.也就是指那些不会被垃圾回收的对象。图中标识有黄色圆点的对象就是GC Roots,每个GC Root之后都会有灰黑色的标识表明这个对象之所以是GC Root的原因。使得一个对象成为GC Root的原因一般有以下几个:

System Class

Class loaded by bootstrap/system class loader. For example, everything from the rt.jar like java.util.* .

JNI Local

Local variable in native code, such as user defined JNI code or JVM internal code.

JNI Global

Global variable in native code, such as user defined JNI code or JVM internal code.

Thread Block

Object referred to from a currently active thread block.

Thread

A started, but not stopped, thread.

Busy Monitor

Everything that has called wait() or notify() or that is synchronized. For example, by calling synchronized(Object) or by entering a synchronized method. Static method means class, non-static method means object.

Java Local

Local variable. For example, input parameters or locally created objects of methods that are still in the stack of a thread.

Native Stack

In or out parameters in native code, such as user defined JNI code or JVM internal code. This is often the case as many methods have native parts and the objects handled as method parameters become GC roots. For example, parameters used for file/network I/O methods or reflection.

Finalizer

An object which is in a queue awaiting its finalizer to be run.

Unfinalized

An object which has a finalize method, but has not been finalized and is not yet on the finalizer queue.

Unreachable

An object which is unreachable from any other root, but has been marked as a root by MAT to retain objects which otherwise would not be included in the analysis.

Unknown

An object of unknown root type. Some dumps, such as IBM Portable Heap Dump files, do not have root information. For these dumps the MAT parser marks objects which are have no inbound references or are unreachable from any other root as roots of this type. This ensures that MAT retains all the objects in the dump.

在上图的“Path To GC Roots”的菜单中可以选择排除不同的非强引用组合来筛选到GC Roots的引用链,这样就可以知道有哪些GC Roots直接或间接的强引用着当前对象,导致无法释放了。



 

(3) Top Consumers

以class和package分类表示占用内存比较多的对象。

(4) Leak Suspects

对内存泄露原因的简单分析,列出了可能的怀疑对象,这些对象可以做为分析的线索。

(5) OQL

MAT提供了一种叫做对象查询语言(Object Query Language,OQL)的工具,方便用于按照自己的规则过滤对象数据。例如想查询我的Activity的所有对象:

SELECT * FROM com.demo.oom.OOMDemoActivity

或者想查询指定package下的所有对象:

SELECT * FROM “com.demo.oom.*” (如果使用通配符,需要用引号)

或者想查询某一个类及其子类的所有对象:

SELECT * FROM INSTANCEOF android.app.Activity

还有很多高级的用法请参考帮助文档。

3. 使用MAT分析OOM原因

熟悉了以上的各种工具,就可以来分析问题原因了。分析的思路有很多。

思路一:

首先我们从MAT的提示中得知com.demo.oom.OOMDemoActivity @ 0x40516b08对象占用了非常多的内存(Shallow Size: 160 B Retained Size: 18 MB),我们可以在DominatorTree视图中查找该对象,或者通过OQL直接查询该类的对象。



 

按照Retained Heap降序排列,可以知道OOMDemoActivity对象之所以很大是因为有一个占用内存很大的ArrayList类型的成员变量,而根本原因是这个集合内包含了很多1MB以上的SomeObj对象。此时就可以查看代码中对SomeObj的操作逻辑,查找为什么会有大量SomeObj存在,为什么每个SomeObj都很大。找到问题后想办法解决,例如对SomeObj的存储使用SoftReference,或者减小SomeObj的体积,或者发现是由于SomeObj没有被正确的关闭/释放,或者有其他static的变量引用这SomeObj。

思路二:

如果MAT没能给出任何有价值的提示信息,我们可以根据自己的判断来查找可以的对象。因为发生OOM的进程是com.demo.oom,可以使用OQL列出该进程package的所有对象,然后再查找可疑的对象。对应用程序来说,这是非常常用的方法,如下图。



 

通过查询发现SomeObj的对象数量特别多,假设正常情况下对象用完后应该立即释放才对,是什么导致这些对象没有被释放呢?通过“Path To GC Roots”的引用链可以知道是OOMDemoActivity中的list引用了SomeObj,所以可以考虑SomeObj是否错误的被添加进了list中,如下图。



 

总之,分析的根本目的就是找到那些数量很大或者体积很大的对象,以及他们被什么样的GC Roots引用而没有被释放,然后再通过检查代码逻辑找到问题原因。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值