首页 > 代码库 > Android内存泄露调试

Android内存泄露调试

版权声明:本文为博主原创文章,未经博主允许不得转载。

版权声明:本文为兄弟连原创文章,未经博主允许不得转载。

Android 内存泄漏调试 
一、概述 
如果我们编写的代码当中有太多的对内存使用不当的地方,难免会使得我们的设备运行缓慢,甚至是死机。为了能够使得 Android 应用程序安全且快速的运行, Android 的每个应用程序都会使用一个专有的 Dalvik 虚拟机实例来运行,即每个应用程序都是在属于自己的进程中运行的。一方面,如果程序在运行过程中出现了内存泄漏的问题,仅仅会使得自己的 
进程被 kill 掉,而不会影响其他进程(如果是 system_process 等系统进程出问题的话,则会引起系统重启)。另一方面 Android 为不同类型的进程分配了不同的内存使用上限,如果应用进程使用的内存超过了这个上限,则会被系统视为内存泄漏,从而被 kill 掉。

Android 为应用进程分配的内存上限如下所示: 
位置: /ANDROID_SOURCE/system/core/rootdir/init.rc 部分脚本

```
# Define the oom_adj values for the classes of processes that can be killed by the kernel.
# These are used in ActivityManagerService.
setprop ro.FOREGROUND_APP_ADJ 0
setprop ro.VISIBLE_APP_ADJ 1
setprop ro.SECONDARY_SERVER_ADJ 2
setprop ro.BACKUP_APP_ADJ 2
setprop ro.HOME_APP_ADJ 4
setprop ro.HIDDEN_APP_MIN_ADJ 7
setprop ro.CONTENT_PROVIDER_ADJ 14
setprop ro.EMPTY_APP_ADJ 15
# Define the memory thresholds at which the above process classes willbe killed.
# These numbers are in pages (4k).
setprop ro.FOREGROUND_APP_MEM 1536
setprop ro.VISIBLE_APP_MEM 2048
setprop ro.SECONDARY_SERVER_MEM 4096
setprop ro.BACKUP_APP_MEM 4096
setprop ro.HOME_APP_MEM 4096
setprop ro.HIDDEN_APP_MEM 5120
setprop ro.CONTENT_PROVIDER_MEM 5632
setprop ro.EMPTY_APP_MEM 6144
# Write value must be consistent with the above properties.
# Note that the driver only supports 6 slots, so we have HOME_APP at the same memory level as
services.
write /sys/module/lowmemorykiller/parameters/adj 0,1,2,7,14,15
write /proc/sys/vm/overcommit_memory 1
write /proc/sys/vm/min_free_order_shift 4
write /sys/module/lowmemorykiller/parameters/minfree 1536,2048,4096,5120,5632,6144
# Set init its forked children‘s oom_adj.
write /proc/1/oom_adj -16
  • 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
  • 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

二、常见的内存使用不当的情况

(一)查询数据库没有关闭游标 
描述: 
程序中经常会进行查询数据库的操作,但是经常会有使用完毕 Cursor 后没有关闭的情况。 
如果我们的查询结果集比较小,对内存的消耗不容易被发现,只有在常时间大量操作的情况 
下才会复现内存问题,这样就会给以后的测试和问题排查带来困难和风险。

示例代码:

Cursor cursor = getContentResolver().query(uri ...);
if (cursor.moveToNext()) {... ...}
  • 1
  • 2
  • 1
  • 2

修正示例代码:

Cursor cursor = null;
try {
cursor = getContentResolver().query(uri ...);
if (cursor != null && cursor.moveToNext()) {... ...}
}
finally {
if (cursor != null) {try {cursor.close();}catch (Exception e) {} }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

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

示例代码:

public View getView(int position, View convertView, ViewGroup parent) {
View view = new Xxx(...); ... ...
return view;
}
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

修正示例代码:

public View getView(int position, View convertView, ViewGroup parent) {
View view = null;
if (convertView != null) { view = convertView; populate(view, getItem(position)); ...}
else { view = new Xxx(...); ...}
return view;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

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

/* Free up the memory associated with this bitmap‘s pixels, and mark the * bitmap as "dead",
meaning it will throw an exception if getPixels() or * setPixels() is called, and will
draw nothing. This operation cannot be * reversed, so it should only be called if you are sure there
are no further uses for the bitmap. This is an advanced call, and normally need * not be called,
since the normal GC process will free up this memory when * there are no more
references to this bitmap. */
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

(四)释放对象的引用 
描述: 
举两个例子进行说明。 
示例 A : 
假设有如下操作

public class DemoActivity extends Activity {
... ...
private Handler mHandler = ... private Object obj;
public void operation() {
obj = initObj(); ...
mHandler.post(new Runnable() {
public void run() {useObj(obj);}});
}}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

我们有一个成员变量 obj ,在 operation() 中我们希望能够将处理 obj 实例的操作 post 到 
某个线程的 MessageQueue 中。在以上的代码中,即便是 mHandler 所在的线程使用完了 
obj 所引用的对象

,但这个对象仍然不会被垃圾回收掉,因为 DemoActivity.obj 还保有这个对象 的引用。所

以如果在 DemoActivity 中不再使用这个对象了,可以在 [Mark] 的位置释放对象的

用,而代码可以修改为: … …

public void operation() {
obj = initObj(); ...
final Object o = obj;
obj = null;
mHandler.post(new Runnable() {
public void run() {useObj(o);} }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

示例 B: 
假设我们希望在锁屏界面 (LockScreen) 中,监听系统中的电话服务以获取一些信息 ( 如信号强度等 ) ,则可以在 LockScreen 中定义一个 PhoneStateListener 的对象,同时将它注册到 TelephonyManager 服务中。对于 LockScreen 对象,当需要显示锁屏界面的时候就会创 
建一 个 LockScreen 对象,而当锁屏界面消失的时候 LockScreen 对象就会被释放掉。 但是如果在释放 LockScreen 对象的时候忘记取消我们之前注册的 PhoneStateListener 对 象,则会导致 LockScreen 无法被垃圾回收。如果不断的使锁屏界面显示和消失,则最终会 由于大量的 LockScreen 对象没有办法被回收而引起 OutOfMemory, 使得 system_process 进程 挂掉。总之当一个生命周期较短的对象 A ,被一个生命周期较长的对象 B 保有其引用的情况下,在 A 的生命周期结束时,要在 B 中清除掉对 A 的引用。

(五)其他 
Android 应用程序中最典型的需要注意释放资源的情况是在 Activity 的生命周期中,在onPause() 、 onStop() 、 onDestroy() 方法中需要适当的释放资源的情况。由于此情况很基础, 在此不详细说明,具体可以查看官方文档对 Activity 生命周期的介绍,以明确何时应该释放 哪些资源。

三、不健壮代码的特征及解决办法

1、尽早释放无用对象的引用。好的办法是使用临时变量的时候,让引用变量在退出活动域 
后,自动设置为 null,暗示垃圾收集器来收集该对象,防止发生内存泄露。 
对于仍然有指针指向的实例,jvm 就不会回收该资源,因为垃圾回收会将值为 null 的对象 
作为垃圾,提高 GC 回收机制效率;

2、我们的程序里不可避免大量使用字符串处理,避免使用 String,应大量使用

StringBuffer,每一个 String 对象都得独立占用内存一块区域。
String str = "aaa";
String str2 = "bbb";
String str3 = str + str2;//假如执行此次之后 str ,str2 以后再不被调用,那它
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

就会被放在内存中等待 Java 的 gc 去回收,程序内过多的出现这样的情况就会报上面的那 
个错误,建议在使用字符串时能使用 StringBuffer 就不要用 String,这样可以省不少开 
销;

3、尽量少用静态变量,因为静态变量是全局的,GC 不会回收的;

4、避免集中创建对象尤其是大对象,JVM 会突然需要大量内存,这时必然会触发 GC 优化 
系统内存环境;显示的声明数组空间,而且申请数量还极大。 
使用 jspsmartUpload 作文件上传,运行过程中经常出现 java.outofMemoryError 的 
错误,检查之后发现问题:组件里的代码

m_totalBytes = m_request.getContentLength();
m_binArray = new byte[m_totalBytes];//问题原因是 totalBytes 这个变量得
  • 1
  • 2
  • 1
  • 2

到的数极大,导致该数组分配了很多内存空间,而且该数组不能及时释放。

5、尽量运用对象池技术以提高系统性能;生命周期长的对象拥有生命周期短的对象时容易 
引发内存泄漏,例如大集合对象拥有大数据量的业务对象的时候,可以考虑分块进行处理, 
然后解决一块释放一块的策略。

6、不要在经常调用的方法中创建对象,尤其是忌讳在循环中创建对象。可以适当的使用 
hashtable,vector 创建一组对象容器,然后从容器中去取那些对象,而不用每次 new 
之后又丢弃。

7、一般都是发生在开启大型文件或跟数据库一次拿了太多的数据,造成 Out Of MemoryError 的状况,这时就大概要计算一下数据量的最大值是多少,并且设定所需最小及最大的内存空间值。

四、内存泄露监测工具 DDMS(Dalvik Debug Monitor Service:Dalvik 虚拟机调试监控服务)

这里我使用 eclipse 的 ADT 插件,并以真机为例,在模拟器中的情况类似。用 Heap 监测应用进程使用内存情况的步骤如下:

  1. 启动 eclipse 后,切换到 DDMS 透视图,并确认 Devices 视图、 Heap 视图都是打开的。
  2. 将手机通过 USB 连接电脑,链接时需要确认手机是处于“ USB 调试”模式,而不是作 
    为“ Mass Storage ”。
  3. 链接成功后,在 DDMS 的 Devices 视图中将会显示手机设备的序列号,以及设备中正 
    在 运行的部分进程信息。
  4. 点击选中想要监测的进程,例讯飞输入法 com.iflytek.inputmethod 进程。
  5. 点击选中 Devices 视图界面中最上方一排图标中的“ Update Heap ”图标。
  6. 点击 Heap 视图中的“ Cause GC ”按钮。
  7. 此时在 Heap 视图中就会看到当前选中的进程的内存使用量的详细情况,如图所示: 
    技术分享

说明: 
a) 点击“ Cause GC ”按钮相当于向虚拟机请求了一次 GC 操作; 
b) 当内存使用信息第一次显示以后,无须再不断的点击“ Cause GC ”, Heap 视图界面会定 时刷新,在对应用的不断的操作过程中就可以看到内存使用的变化。

如何才能知道我们的程序是否有内存泄漏的可能性呢。这里需要注意一个值: Heap 视 图 
中部有一个 Type 叫做 data object ,即数据对象,也就是我们的程序中大量存在的类类型的对象。在 data object 一行中有一列是“ Total Size ”,其值就是当前进程中所有 Java 数据对 象的内存总量,一般情况下,这个值的大小决定了是否会有内存泄漏。可以这样判断: 
a) 不断的操作当前应用,同时注意观察 data object 的 Total Size 值; 
b) 正常情况下 Total Size 值都会稳定在一个有限的范围内,也就是说由于程序中的的代码良 好,没有造成对象不被垃圾回收的情况,所以说虽然我们不断的操作会不断的生成很多对象 , 而在虚拟机不断的进行 GC 的过程中,这些对象都被回收了,内存占用量会会落到一个稳定 的水平; 
c) 反之如果代码中存在没有释放对象引用的情况,则 data object 的 Total Size 值在每次GC 后不会有明显的回落,随着操作次数的增多 Total Size 的值会越来越大,直到到达一个上限后导致进程被 kill 掉。

五、内存泄露分析工具 MAT(Memory AnalyzerTool)

MAT 是一个 Eclipse 插件,官方下载地址: http://download.eclipse.org/mat/1.1/update-site/。 
技术分享

(一)生成 .hprof 文件

  1. 打开 eclipse 并切换到 DDMS 透视图,同时确认 Devices 、 Heap 和 LogCat 视图已经打开。
  2. 将手机设备链接到电脑,并确保使用“ USB 调试”模式链接,而不是“ Mass Storage “模式。
  3. 链接成功后在 Devices 视图中就会看到设备的序列号,和设备中正在运行的部分进程。
  4. 点击 选 中 想 要 分 析 的 应 用 的 进 程 , 在 Devices 视图 上 方 的 一 行 图标 按 钮 中 , 同 时 选中 “ Update Heap ”和“ Dump HPROF file ”两个按钮。此时 DDMS 工具将会自动生成当前选中进程的 .hprof 文件,如图所示: 
    技术分享

(二)使用 MAT 导入 .hprof 文件

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

(三)使用 MAT 的视图工具分析内存

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

总之使用 MAT 分析内存查找内存泄漏的根本思路,就是找到哪个类的对象的引用没有 被释放,找到没有被释放的原因,也就可以很容易定位代码中的哪些片段的逻辑有问题了。 
技术分享

如上图所示,“Problem Suspect”均有可能是代码中存在内存泄露的地方。

Android内存泄露调试