性能优化之内存优化

2021年04月04日 99 字 性能优化


内存

JAVA是在JVM所虚拟出的内存环境中运行的,JVM的内存可分为三个区:

堆(heap)、栈(stack)和方法区(method)。

栈(stack)

是简单的数据结构,但在计算机中使用广泛。栈最显著的特征是:LIFO(Last In, First Out, 后进先出),栈中只存放基本类型和对象的引用

堆(heap)

堆内存用于存放由new创建的对象和数组。在堆中分配的内存,由java虚拟机自动垃圾回收器来管理。JVM只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身。

方法区(method)

又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量

内存泄漏

是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,导致一直占据该内存单元,直至程序结束,都无法再使用该内存单元。

内存泄漏常见原因

1. Context的不当引用

当持有Context的对象生命周期较长时,持有的Context即使被销毁也不会被回收掉。

  • 单例对象直接或间接持有Context:
单例直接持有Activity对象,或通过持有Fragment、View等间接持有Context对象。
  • Activity/Fragment中的非静态内部类、匿名内部类

    Activity中使用非静态内部类/匿名内部类,如Handler及各类事件监听。当内部类生命周期较长(耗时)时,若Activity被销毁,易造成内存泄漏或崩溃。

  • 多线程异步回调(崩溃)

    子线程执行后回调主线程方法,若持有主线程Handler或Context,当Activity销毁,此时易出现内存泄漏或崩溃。

解决办法:

  • 非必要情况下,不建议这样编程;
  • 单例生命周期敏感且不持有生命周期较短的Context对象强引用:若需使用Activity对象,应对Activity做弱引用封装,且应提供解除引用的方法,在Activity销毁时解除引用;必须持有context的,能使用ApplicationContext则尽量使用Application;
  • 对于Handler,建议不使用匿名声明的方式创建,在Activity销毁时调用Handler.removeCallback,解除对Activity的引用;
  • 使用内部类的情况,可使用静态内部类,需持有Activity的应对Activity做弱引用封装
  • 对于内部类或线程操作耗时/异步的情况,在Activity销毁时应通知对应内部类解除对Activity的引用,清理未执行完毕的线程。

2. 资源未回收

  • bind/unbind、register/unregister、start/stop、run/cancel

    在一些组件使用过程中,应注意其绑定和解绑、执行和取消等操作应成对存在,否则若其持有Activity的引用,将会在Activity被销毁时产生内存泄漏或崩溃,如属性动画、广播、bind服务等。

  • Closeable对象未调用close()

    IO流/数据库游标cursor应在使用完毕后调用close()方法,即使它执行失败/错误也应调用关闭,否则将造成内存泄漏甚至崩溃。

  • bitmap

    bitmap资源在使用完成后,未主动调用recycle()并置空,在系统未主动回收时会造成大量内存占用。

解决办法:

  • 对于组件的使用,应注意规范,Activity销毁时及时释放资源:例如EventBus.getDefault().register()/unregister(); ButterKnife.bind(this)/unbind(); bindService()/unbindService()等;
  • Closeable对象在使用完毕后应及时调用close();可使用 try(IO对象)的方式避免遗忘;特别的对于具有缓存(Flushable)功能的流,应注意调用flush()保证数据完整性;
  • bitmap资源在使用完成后,主动调用recycle()并置空

3. 创建/保存大量对象

  • 频繁调用的方法中创建对象,例如onDraw()、onResume()

    在频繁调用的方法中创建对象(即使是局部变量),会导致一定时间内,内存堆积,若GC回收不及时会占用大量内存空间,若GC频繁回收则会产生内存抖动,导致卡顿。

  • 声明的全局List或Map(只进不出/未及时清理)

    全局声明的List或Map,若存放大量对象且对象不常使用,将占用大量内存且无法回收。

  • 未及时清理的缓存对象

    在用户体验优化上我们常常使用缓存提升加载速度,若缓存并非经常使用的情况下,将会白白占用内存空间。

解决办法:

  • 尽量避免在频繁调用的方法中创建对象,必要情况下使用享元模式,控制对象生成数量;
  • 全局List和Map等集合容器,不使用的元素应及时移除;
  • 缓存应根据设备内存状况设置最大存储容量,并建立LRU等缓存机制,在保证体验的情况下最大程度优化内存;

4. WebView、MapView

原生WebView存在已知的内存泄漏风险,针对这类性能消耗较大的组件(控件),若未处理好其内存问题,将会是应用内存管理的灾难。

解决办法:

  • 对于WebView、MapView等,应主动管理其创建/销毁/回收流程,让内存尽快的释放出来;
  • 有条件的,可以为其申请单独的进程,提升主进程的稳定性,减小主进程的内存压力;

内存溢出

系统会给每个APP分配内存也就是Heap Size值。当APP占用的内存加上我们申请的内存资源超过了Dalvik虚拟机的最大内存时就会抛出的Out Of Memory异常。

  • java.lang.OutOfMemoryError:Javaheapspace:堆内存不够或程序中有死循环
  • java.lang.OutOfMemoryError:GCoverheadlimitexceeded:当GC为释放很小空间占用大量时间时抛出;堆太小,没有足够的内存
  • java.lang.OutOfMemoryError:PermGenspace:P区内存不够
  • java.lang.OutOfMemoryError:Directbuffermemory:Directbuffer内存不足
  • java.lang.OutOfMemoryError:unabletocreatenewnativethread:Stack空间不足以创建额外的线程,创建的线程过多或Stack空间太小
  • java.lang.StackOverflowError:线程栈的溢出,方法调用层次过多或线程栈太小。

解决办法:

  • 对于启动参数内存值设定的过小的情况,可以使用修改启动参数的方法解决:
1
2
3
4
5
6
7
-Xms3062m // 提升堆内存
-XX:-UseGCOverheadLimit // 限制使用内存
// JVM的Perm区主要用于存放Class和Meta信息的,Class在被Loader时就会被放到PermGenspace,这个区域成为年老代,GC在主程序运行期间不会对年老区进行清理,
// 默认是64M大小,当程序需要加载的对象比较多时,超过64M就会报这部分内存溢出了,需要加大内存分配,一般128m足够
-XX:MaxPermSize=128m
-XXermSize=128m
-XX:MaxDirectMemorySize=128m // 调整Directbuffer内存大小
  • 检查代码中是否存在死循环或递归层次较深的场景,修复或优化此类代码;
  • 优化内存,减少内存泄漏,使内存保持在健康良好的状态。

内存优化

内存优化主要从减少内存占用、杜绝内存泄漏、提升应用内存限制这几个方面考量,内存优化是个没有止境的话题,当然不是最优就是最好的,它需要在内存和体验上找寻一个平衡点,当然,内存泄漏是越少越好。

减少内存占用

  • 对象

    • 避免重复创建对象,能使用对象池的尽量使用对象池,如Message.obtain();
    • 避免频繁创建对象,对于频繁调用的方法,尽量不在其中创建大量对象;
    • 及时回收对象,对于不使用或不频繁使用的对象应主动释放
  • 资源

    • 对于Bitmap,加载前应针对场景对其进行尺寸和数据长度压缩,使用后应及时释放;
    • 对于icon资源,应使用对应分辨率的切图,避免icon加载时系统缩放占用额外的内存空间;
    • 对于文件流和游标,使用完毕后应及时关闭,避免IO占用额外的内存空间;
    • 珍惜线程资源,使用线程池管理线程,减少线程的创建
  • 缓存

    • 对于内存消耗较大且不常使用的数据避免存储在内存中
    • 缓存应考虑设备及应用的内存限制,设定合理的缓存容量及缓存策略

杜绝内存泄漏

  • 编码规范

    • 依照前文内存泄漏相关说明,检查代码,减少内存泄漏
  • 检查工具

    • dumpsys meminfo package_name|pid
      1
      adb shell dumpsys meminfo com.company.project
    • %AndroidSDK%\platform-tools\systrace\systrace.py
    1
    2
    3
    4
    5
    // 指定版本python2.7
    // -o: 指定文件输出位置和文件名
    // -t: 抓取systrace的时间长度
    // -a: 指定特殊进程包名
    python %AndroidSDK%\platform-tools\systrace\systrace.py -a com.company.project -t 10 -o trace.html
    • LeakCanary
    • MAT(Memory Analyzer Tool)

提升应用内存限制

  • 创建子进程

    创建子进程的方法:使用android:process标签

    可以将内存消耗较大的模块放入子进程中执行,系统会为子进程分配新的内存空间,以达到减小主进程内存消耗地目的,例如Web模块、map模块等。

    当然,这样做的缺点也很明显:创建子进程会增加系统开销,进程间通信往往更加复杂。

  • 使用jni在native heap上申请空间

    nativeheap的增长并不受dalvik vm heapsize的限制,native heap size可以远远超过dalvik heap size的限制。

    只要RAM有剩余空间,程序员可以一直在native heap上申请空间,当然如果 RAM快耗尽,memory killer会杀进程释放RAM。大家使用一些软件时,有时候会闪退,就可能是软件在native层申请了比较多的内存导致的。