• 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

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

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

    示例代码:

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

    修正示例代码:

    Cursor cursor = null;
    try {
    cursor = getContentResolver().query(uri ...);
    if (cursor != null && cursor.moveToNext()) {... ...}
    }
    finally {
    if (cursor != null) {try {cursor.close();}catch (Exception e) {} }
    }

    (二)构造 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;
    }

    修正示例代码:

    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;
    }

    (三)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. */

    (四)释放对象的引用
    描述:
    举两个例子进行说明。
    示例 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);}});
    }}

    我们有一个成员变量 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);} }
    }

    示例 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 以后再不被调用,那它

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

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

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

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

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

    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”均有可能是代码中存在内存泄露的地方。

  • 相关阅读:
    第二篇第十一章灭火救援设施
    第二篇第六章安全疏散
    第二篇第五章防火防烟分区于分隔
    第二篇第三章建筑分类与耐火等级
    applicationContext-solr.xml
    solrj 操作 solr 集群版
    centos solr 集群搭建
    org.apache.ibatis.binding.BindingException
    全文检索基础
    solrj 操作 solr 单机版
  • 原文地址:https://www.cnblogs.com/qwop/p/6637398.html
Copyright © 2020-2023  润新知