博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Android 系统稳定性 - OOM
阅读量:4075 次
发布时间:2019-05-25

本文共 21972 字,大约阅读时间需要 73 分钟。

文章都为原创,转载请注明出处,未经允许而盗用者追究法律责任。 

很久之前写的了,留着有点浪费,共享之。 

编写者:李文栋

 

2.1.1 什么是内存溢出

 

2.1.2 为什么会有内存溢出

Android 主要应用在嵌入式设备当中,而嵌入式设备由于一些众所周知的条件限制,通常都不会有很高的配置,特别是内存比较有限。如果我们编写的代码当中有太多的对内存使用不当的地方,难免会使得我们的设备运行缓慢,甚至是死机。为了能够使系统安全且快速的运行,Android 的每个应用程序都运行在单独的进程中,这个进程是由 Zygote 进程孵化出来的,每个应用进程中都有且仅有一个虚拟机实例。如果程序在运行过程中出现了内存泄漏的问题,只会影响自己的进程,不会直接影响其他进程。

Java虽然有自己的垃圾回收机制,但并不是说用Java编写的程序就不会内存溢出了。Java程序运行在虚拟机中,虚拟机初始化时会设定它的堆内存的上限值,在Android中这个上限值默认是“16m”,而你可以根据实际的硬件配置来调整这个上限值,调整的方法是在系统启动时加载的某个配置文件中设置一个系统属性:

dalvik.vm.heapsize=24m

当然也可以设置成更大的值(例如“32m”)。这样Android中每个应用进程的DalvikVM实例的堆内存上限值就变成了24MB,也就是说一个应用进程中可以同时存在更多的Java数据对象了。有一些大型的应用程序(例如游戏)运行时需要比较多的内存,heapsize太小的话根本无法运行,此时就需要考虑调整heapsize的大小了。heapsize的大小是同时对整个系统生效的,原生代码中无法单独的调整某一个Java进程的heapsize(除非我们自己修改源码,不过我们从来没这么做过)。

当代码中的缺陷造成内存泄漏时,泄漏的内存无法在虚拟机GC的时候被释放,因为这些内存被一些数据对象占用着,而这些数据对象之所以没有被释放,可以归结为两类情况:

a) 被强引用着

例如被一个正在运行的线程、一个类中的static变量强引用着,或者当前对象被注册进了framework中的一些接口中。

b) JNI中的指针引用着

Framework中的一些类经常会在Java层创建一个对象,同时也在C++层创建一个对象,然后通过JNI让这两个对象相互引用(保存对方的地址),BinderProxy对象就是一个很典型的例子,在这种情况下,Java层的对象同样不会被释放。

当泄漏的内存随着程序的运行越来越多时,最终就会达到heapsize设定的上限值,此时虚拟机就会抛出OutOfMemoryError错误,内存溢出了。

2.2 容易引起内存泄漏的常见问题

2.2.1 Cursor对象未正确关闭

关于此类问题其实已经是老生常谈了,但是由于Android应用源码中的缺陷和使用的场合比较复杂,所以还是会时常出现这类问题。

1. 问题举例

Cursor cursor = getContentResolver().query(...);

        if (cursor.moveToNext()) {

        ... ...

}

2. 问题修正

Cursor cursor = null;

try {

        cursor = getContentResolver().query(...);

        if (cursor != null && cursor.moveToNext()) {

        ... ...

        }

} catch (Exception e) {

        ... ...

} finally {

        if (cursor != null) {

                cursor.close();

        }

}

3. 引申内容

(1) 实际在使用的时候代码的逻辑通常会比上述示例要复杂的多,但总的原则是一定要在使用完毕Cursor以后正确的关闭。

(2) 如果你的Cursor需要在Activity的不同的生命周期方法中打开和关闭,那么一般可以这样做:

onCreate()中打开,在onDestroy()中关闭;

onStart() 中打开,在onStop() 中关闭;

onResume()中打开,在onPause() 中关闭;

即要在成对的生命周期方法中打开/关闭。

(3) 如果程序中使用了CursorAdapter(例如Music),那么可以使用它的changeCursor(Cursor cursor)方法同时完成关闭旧Cursor使用新Cursor的操作。

(4) 至于在cursor.close时需不需要try...catchcursor非空时),其实在close时做的工作就是释放资源,包括通过Binder跨进程注销ContentObserver时已经捕获了RemoteException异常,所以其实可以不用try...catch

(5) 关于deactiveclosedeactive不等同于close,看他们的API comments就能知道,如果deactive了一个Cursor,说明以后还是会用到它(利用requery方法),这个Cursor会释放一部分资源,但是并没有完全释放;如果确认不再使用这个Cursor了,一定要close

(6)除了Cursor有时我们也会对Database对象做操作,例如要修正MediaProvider中的一个attachVolume方法,在每次检测到attach的是一个externalvolume时就重新建立一个数据库,而不是采用以前的,那么在remove旧的数据库对象的时候不要忘记关闭它。<!-- 第6点关于Database是否考虑去掉 -->

4. 影响范围

如果没有关闭Cursor,在测试次数足够多的情况下,就会出现:

(1) 内存泄漏

我们先简单的看一下Cursor的结构,这样会更好理解。数据库操作涉及到服务端的ContentProvider和客户端程序,客户端通常会通过ContentResolver.query函数查询并获取一个结果集的Cursor对象。而这个Cursor对象实际上也只是一个代理,因为要考虑到客户端和服务端在不同进程的情况,所以Cursor的使用本身也是利用了Binder机制的,而客户端和服务端的数据共享是利用共享内存来实现的,如下图所示。

 

客户端和服务端使用的Cursor经过了层层封装,显得十分臃肿,但它们的工作其实可以简单的从控制流和数据流两个方面来看。在控制流方面,客户端为了能和远端的服务端通信,使用实现了IBulkCursor接口的BulkCursorProxyCusorToBulkCursorAdapter对象,例如要获取结果集数据时,客户端通过BulkCursoryProxy.onMove函数调用到CursorToBulkCursorAdapter.onMove函数,然后再调用到SQLiteCursor.onMove函数来填充数据的。在数据流方面,服务端的SQLiteCursor将从数据库中查询到的结果集写入到共享内存中,然后Binder调用返回到客户端,客户端就可以从共享内存中获取到想要的数据了。客户端的控制流和数据流的访问由BulkCursorToCursorAdapter负责,服务端则是分别由CursorToBulkCursorAdapterSQLiteCursor负责。

如果Cursor没有正常关闭,那么客户端和服务端的CursorWindow对象和申请的那块共享内存都不会被回收,尽管其他相关的Java对象可能由于没有强引用而被回收,但是真正占用内存的通常是存放结果集数据的共享内存。大量的Cursor没有关闭的话,你可能会看到以下类型的异常信息:

  • 创建新的Java对象时发现没有足够的内存,抛出内存溢出错误:OutOfMemoryError

  • 创建新的CursorWindow时无法申请到足够的内存,可能的异常信息有:

    RuntimeException: No memory for native window object
    IllegalStateException: Couldn't init cursor window
    CursorWindow heap allocation failed 
    failed to create the CursorWindow heap

(2) 文件描述符泄漏

当然有可能很幸运,每次查询的结果集都很小,做几千次查询都不会内存溢出,但是AndroidLinux内核还有另外一个限制,就是文件描述符的上限,这个上限默认是1024

文件描述符本身是一个整数,用来表示每一个被进程所打开的文件和Socket,第一个打开的文件是0,第二个是1,依此类推。而Linux给每个进程能打开的文件数量设置了一个上限,可以使用命令“ulimit -n”查看。另外,操作系统还有一个系统级的限制。

每次创建一个Cursor对象,都会向内核申请创建一块共享内存,这块内存以文件形式提供给应用进程,应用进程会获得这个文件的描述符,并将其映射到自己的进程空间中。如果有大量的Cursor对象没有正常关闭,可想而知就会有大量的共享内存的文件描述符无法关闭,同时再加上应用进程中的其他文件描述符,就很容易达到1024这个上限,一旦达到,进程就挂掉了。

提示:可以到系统的“/proc/进程号/fd”目录中查看进程所有的文件描述符。

 

(3) GREF has increased to 2001

先说明一下“死亡代理”的概念。利用Binder做进程间通信时,允许对Binder的客户端代理设置一个DeathRecipient对象,它只有一个名为binderDied的函数。当Binder的服务端进程死掉了,binder驱动会通知客户端进程,最终回调DeathRecipient对象的binderDied函数,客户端进程可以借此做一些清理工作。

需要注意的是,“死亡代理”的概念只对进程间通信有效,对进程内通信没有意义;另外,Binder的客户端和服务端的概念是相对的,例如BulkCursorProxyCursorToBulkCursorAdapter的客户端,而后者又有一个IContentObserver的客户端,其对应的服务端在BulkCursorToCursorAdaptergetObserver函数中创建。这里需要关注的就是在CursorToBulkCursorAdapter对象被创建时,会同时将该对象注册为IContentObserver的客户端对象的“死亡代理”,代码如下:

CursorToBulkCursorAdaptor的内部类ContentObserverProxy的构造函数中

public ContentObserverProxy(IContentObserver remoteObserver, DeathRecipient recipient) {

        super(null);

        mRemote = remoteObserver;

        try {

                //此处的recipient就是CursorToBulkCursorAdapter对象

                remoteObserver.asBinder().linkToDeath(recipient, 0);

        catch (RemoteException e) {

        }

}

 

死亡代理”对象的引用会被Native层的Binder代理对象的mObituaries集合引用,所以“死亡代理”对象及其关联对象由于被强引用而不会被垃圾回收掉,同时JNI在实现linkToDeath函数的过程中也创建了一些具有全局性的引用,被称作“Global Reference(简写为GREF)”,每一个GREF都会被记录到虚拟机中维护的一个“全局引用表”中。

eng模式下,JNI全局引用计数(GREF)有一个上限值为2000,如果大量Cursor对象没有被正常关闭,服务端进程就会因为“死亡代理”对象的创建使得虚拟机中的全局引用计数增多,当超过2000时,虚拟机就会抛出异常,导致进程挂掉,典型的异常信息就是“GREF has increased to 2001”

提示:全局引用计数的上限2000已经是一个比较大的值,正常情况下很难达到。Androideng模式下开启这项检查,就是为了能够在开发阶段发现Native层的内存泄漏问题。在usr模式下这项检查会被禁用,此时如果有内存泄漏就只有等到抛出内存溢出错误或者文件描述符超出上限等其他异常时才能发现了。

Cursor未正常关闭是导致GREF越界的原因之一,后续会在其他章节中详细讨论。

2.2.2 释放对象的引用

内存的问题是Bugzilla中的常客,经常会在不经意间遗留一些对象没有释放或销毁

1. 静态成员变量

有时因为一些原因(比如希望节省Activity初始化时间等),将一些对象设置为static的,比如:

private static TextView mTv;

... ...

mTv = (TextView) findViewById(...);

而且没有在Activity退出时释放mTv的引用,那么此时mTv本身,和与mTv相关的那个Activity的对象也不会在GC时被释放掉,Activity强引用的其他对象也无法被释放掉,这样就造成了内存泄漏。如果没有充分的理由,或者不能够清楚的控制这样做带来的影响,请不要这样写代码。

2. 正确注册/注销监听器对象

经常要用到一些XxxListener对象,或者是XxxObserverXxxReceiver对象,然后用registerXxx方法注册,用unregisterXxx方法注销。本身用法也很简单,但是从一些实际开发中的代码来看,仍然会有一些问题:

(1) registerXxxunregisterXxx方法的调用通常也和Cursor的打开/关闭类似,在Activity的生命周期中成对的出现即可:

在 onCreate() 中 register,在 onDestroy() 中 unregitster

在 onStart() 中 register,在 onStop() 中 unregitster

在 onResume() 中 register,在 onPause() 中 unregitster

(2) 忘记unregister

以前看到过一段代码,在Activity中定义了一个PhoneStateListener的对象,将其注册到TelephonyManager中:

TelephonyManager.listen(lPhoneStateListener.LISTEN_SERVICE_STATE);

但是在Activity退出的时候注销掉这个监听,即没有调用以下方法:

TelephonyManager.listen(lPhoneStateListener.LISTEN_NONE);

因为PhoneStateListener的成员变量callback,被注册到了TelephonyRegistry中,TelephonyRegistry是后台的一个服务会一直运行着。所以如果不注销,则callback对象无法被释放,PhoneStateListener对象也就无法被释放,最终导致Activity对象无法被释放。

3. 适当的使用SoftReferenceWeakReference

如果要写一个缓存之类的类(例如图片缓存),建议使用SoftReference,而不要直接用强引用,例如:

private final ConcurrentHashMap<Long, SoftReference<Bitmap>> mBitmapCache = new ConcurrentHashMap<LongSoftReference<Bitmap>>();

当加载的图片过多,应用可用堆内存不足的时候,就可以自动的释放这些缓存的Bitmap对象。

关于Java中的强引用、软引用、弱引用和虚引用是一些比较重要的概念,在Android开发中经常会用到。

2.2.3 构造 Adapter 时,没有使用缓存的 convertView

以构造 ListView 的 BaseAdapter 为例,在 BaseAdapter 中提供了以下方法:

public View getView(int positionView convertViewViewGroup parent)

来向 ListView 提供每一个 item 所需要的 view 对象。初始时 ListView 会从 BaseAdapter 中根据当前的屏幕布局实例化一定数量的 view 对象,同时 ListView 会将这些 view 对象缓存起来 。当向上滚动ListView 时,原先位于最上面的 list item 的 view 对象会被回收,然后被用来构造新出现的最下面的listitem。这个构造过程就是由 getView()方法完成的,getView()的第二个形参 View convertView 就是被缓存起来的 list item 的 view 对象(初始化时缓存中没有 view对象则 convertView 是 null)。由此可以看出,如果我们不去使用 convertView,而是每次都在 getView()中重新实例化一个 View 对象的话,即浪费资源也浪费时间,也会使得内存占用越来越大ListView 回收listitem 的 view 对象的过程可以查看:android.widget.AbsListView类中的addScrapView(View scrap) 方法。

示例代码:

public View getView(int positionView convertViewViewGroup parent) {

        View view = new Xxx(...);

        ... ...

        return view;

}

修正示例代码:

public View getView(int positionView convertViewViewGroup parent) {

        View view = null;

        if (convertView != null) {

                view = convertView;

                populate(viewgetItem(position));

                ...

        } else {

                view = new Xxx(...);

                ...

        }

        return view;

}

2.2.4 Bitmap 对象不再使用时调用 recycle()释放内存

有时我们会自己操作 Bitmap 对象,如果一个 Bitmap 对象比较占内存,当它不再被使用的时候,可以调用 Bitmap.recycle()方法回收此对象的像素所占用的内存,但这不是必须的 ,视情况而定。可以看一下代码中的注释:

/**

* Free up the memory associated with this bitmap's pixelsand mark the

* bitmap as "dead"meaning it will throw an exception if getPixels() or

* setPixels() is calledand will draw nothing. This operation cannot be

* reversedso it should only be called if you are sure there are no

* further uses for the bitmap. This is an advanced calland normally need

* not be calledsince the normal GC process will free up this memory when

* there are no more references to this bitmap.

*/

文章都为原创,转载请注明出处,未经允许而盗用者追究法律责任。 

很久之前写的了,留着有点浪费,共享之。 

编写者:李文栋   微博关注: 云且留猪

2.3 如何分析内存溢出问题

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

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

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的其他信息可以查看:

以及Android源码中的/dalvik/docs/heap-profiling.html文件(这个比较重要,建议看看,例如kill -10Android2.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 透视图,同时确认 DevicesHeap 和 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)方法就可以请求虚拟机dumphprof到你指定的文件中。例如我们之前为了排查应用进程主线程中发生的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转换格式之后,第二步就可以用MemoryAnalyzerToolMAT)工具来分析内存使用情况了。

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,并将对象addArrayList中,直到抛出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文件的,你也可以使用ADTDDMS工具来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)(虚拟机dumphprof文件)

D/dalvikvm(1238): GC_HPROF_DUMP_HEAP freed <1K, 13% free 20992K/23879K, external 716K/1038Kpaused 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”即可。然后就可以看到如下的初步分析报告:

 

MATOverview视图中用饼图展示了内存的使用情况,列出了占用内存最大的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 RootsA 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

classpackage分类表示占用内存比较多的对象。

(4) Leak Suspects

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

(5) OQL

MAT提供了一种叫做对象查询语言(Object Query LanguageOQL)的工具,方便用于按照自己的规则过滤对象数据。例如想查询我的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引用而没有被释放,然后再通过检查代码逻辑找到问题原因。

原文地址:http://rayleeya.iteye.com/blog/1956638

你可能感兴趣的文章
voxel 与 pixel
查看>>
vector3.forward和transform.forward的区别!
查看>>
HOLOLENS的空间管理
查看>>
unity3d 的Quaternion.identity和transform.rotation区别是什么
查看>>
【Unity3d】Ray射线初探-射线的原理及用法
查看>>
迄今最深入、最专业的Hololens评测结果,美国AR大咖艾迪·奥夫曼现身说法
查看>>
全息眼镜HoloLens可快速捕捉真人3D图像
查看>>
copy-paste component
查看>>
【Unity】矩阵运算
查看>>
理解向量运算
查看>>
正弦 sin 余弦 cos
查看>>
微积分
查看>>
Vector3 *2 ,ToString()自动四舍五入
查看>>
2016年秋季的我,work with hololens
查看>>
叉积与点积
查看>>
λ怎么 读
查看>>
Rect 和 Bounds
查看>>
HOLOLENS不适合加天空盒
查看>>
Unity UI on hololens
查看>>
Unity 下载存档
查看>>