Android 性能优化 -- 内存

APP 进程一般只分配 16M,32M,64M 的内存空间,C和C++会自己分配内存(malloc)和释放内存(free),而Java不是,So….

内存分配策略:

  • 静态存储区:内存空间在程序编译(类加载)的时候就分配好,这块内存在程序整个运行都一直存在。主要存放静态数据,全局static数据和一些常量。

  • 栈式存储区:在执行方法时,方法内部变量存储放在栈上创建,方法执行结束后背释放。栈内存分配速度快,容量有限(由操作系统决定),数据先进后出,进出不产生内存碎片,运行效率高且稳定。

  • 堆式存储区:动态内存分配,有时候可以用malloc或者new()来申请分配一个内存。类里的对象引用也是在堆内存中。(堆是一个不连续的存错区域)

JAVA 内存回收机制(GC)

  • 基本原理:JAVA 虚拟机通过算法必须追踪运行程序中有用的对象,而且最终释放没用的对象,垃圾回收期从根开始可达对象都是活动对象,包括间接可达。不可达就是垃圾回收的对象。
    • 标记-清除法:标记需要清除的对象,然后清除。效率不高,产生不连续内存碎片。
    • 复制算法:将内存区域分为两块,每次使用一块,当这一块用完了,将存活的复制到另外一面墙,清理完一面墙。代价内存缩小一半,没有内存碎片,效率低。
    • 标记-压缩算法:标记过程与“标记-清除”一样,但后续不是直接清除,而是所有对象移动到一端,然后清理掉边界以外的内存。
    • 分代收集法:分为新生代(生成的对象首先都是放在年轻代的),老年代(生命周期长的对象,年轻代经历N次回收还存活的对象),持久代(存放静态文件,java类,方法等)。新生代有大批对象死亡,采用复制算法。老年代存活率高,就必须采用标记-清理或标记-压缩来回收。

内存泄露与内存溢出

  • 内存泄露:指程序已经终止,但是一些临时对象,对象不会被GC所回收,它始终占用内存。即被分配的对象可达但已无用。

  • 内存溢出:指程序运行过程中无法申请到足够的内存而导致的一种错误。内存溢出通常发生于OLD段或Perm段垃圾回收后,仍然无内存空间容纳新的Java对象的情况。或者说,当应用占用的堆内存资源超过了Dalvik虚拟机分配的内存就会内存溢出.

堆内存&栈内存

Java 中数据存储的位置其实还包括寄存器(无法人为控制,自动分配),常量池(字符串常量和基本类型常量),非RAM存储区(流对象和持久化对象)。堆内存和栈内存都是RAM(内存)上存储。

  • 栈内存速度大于堆内存
  • 堆内存灵活性大于栈内存
  • 栈可以实现数据共享。 (int a = 100 ,int b =100)
  • new 出来的都是堆内存,其余都是栈内存(不是很严谨)BirthDate d1= new BirthDate(1,1,1); d1对象为引用在栈中,new BirthDate()在堆中。

Java 常见的引用

  在Java中,虽然不需要程序员手动去管理对象的生命周期,但是如果希望某些占用内存大对象具备一定的生命周期的话,可以使用SoftReference和WeakReference就能很好防止内存溢出

引用名称 回收时机 生命周期 使用
StrongReference(强) 从不回收 JVM停止的时候终止 用于对象的保存
SoftReference(软) 内存不足时 内存不足时终止 结合ReferenceQueue构造有效期短
WeakReference(弱) 垃圾回收时 GC后终止 同软引用
PhatomReference(虚) 垃圾回收时 GC后终止 结合ReferenceQueue跟踪对象被回收活动
  • LRU 算法和SoftReference区别:SoftReference回收量比较大,而且不能控制回收哪个对象,LRU 算法是回收最近不常使用的对象.

Android 找内存泄露的一般步骤

  • 确定是否存在内存泄露

    • Android monitor 某个操作前,GC完后与动作发生后的内存大小是否一致;
    • MAT 中Total Size值都会稳定在一个有限的范围内,随着操作次数的增大,Total Size变大,证明操作导致内存泄露
  • 找出怀疑对象

    • MAT对比操作前后的hprof来定位内存泄露是泄露了什么数据对象
  • MAT分析hprof定位内存泄露

    • 一般性步骤Histogram -> List Objects -> with incoming references -> Merge Shortest Paths to GC Roots -> exclude all phantom/weak/soft etc.references

Android 中常见到内存泄露案例

  • 单例模式Context的使用不正常

    如果传入的是Activity的Context,当Activity退出时候,因为单例对象持有该Activity的引用,所以Activity退出时内存并不会回收.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class AppManager {
    private static AppManager instance;
    private Context context;
    private AppManager(Context context) {
    this.context = context.getApplicationContext();
    }

    public static AppManager getInstance(Context context) {
    if (instance != null) {
    instance = new AppManager(context);
    }
    return instance;
    }
    }
  • 非静态内部类直接在类中创建实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class MainActivity extends AppCompatActivity {
    private static TestResource mResource = null;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    if(mManager == null){
    mManager = new TestResource();
    }

    class TestResource {
    }
    }

    这样非静态内部类会默认持有外部类的引用,该实例生命周期和应用一样长,导致了该实例会一直持有Activity的引用,导致Activity内存资源不能回收泄露。

  • 匿名内部类线程造成的泄露

    匿名内部类异步任务AsyncTask和Thread,在Activity销毁之前,任务还未完成,将导致Activity内存资源无法回收,内存泄露,正确做法是使用静态内部类。

  • 资源未及时关闭回收

    在用到如下功能时候BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap,自定义属性attribute。记得Activity销毁时或者不适用的时候回收资源或者注销。

  • 监听未移除

    最常见的就是addOnWindowFocusChangeListener,一定要在onWindowFocusChanged()回调中移除掉。

  • 无限轮播的动画

    没有在onDestory()中停止动画,导致Activity变成泄露的对象

常见内存优化工具

Heap Viewer工具 , Heap Snapshot工具 , MAT , TraceView , Allocation Tracker工具。

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器