性能优化(二):内存优化基础知识
1.Java的对象生命周期
-
创建(Create)
为对象分配存储空间
开始构造对象
从超类到子类对static成员进行初始化
超类成员变量按顺序初始化,递归调用超类的构造函数
子类成员变量按顺序初始化,子类构造方法调用 -
应用(In use)
对象至少被一个强引用持有。 -
不可见(Invisible)
当一个对象处于不可见阶段,说明程序本身不再持有该对象的任何引用,但是这些引用可能还存在着。一般具体指程序的执行已经超过该对象的作用域了。 -
不可达(Unreachable)
该对象不再被任何强引用所持有。 -
收集(Collected)
finalize()方法的调用时机。 -
终结(Finalized)
对象运行完finalize()方法后仍处于不可达状态,则进入终结阶段,等待垃圾回收器对该对象空间进行回收。 -
对象空间重新分配(Deallocated)
垃圾回收器对该对象所占用的内存空间进行回收或者再分配,则该对象彻底消失。
2.Android内存分配与回收机制
Android内存回收机制.jpg新生代区域分为eden区、S0和S1区,S1和S2(合称Survivor区)是两块大小相等并且可以互换角色的空间。(1)对象创建后在eden区;(2)执行GC后,如果对象还存活会进入S0或S1区;(3)每一个GC,存活的对象年龄会相应增加,达到一定年龄会进入老年代;(4)最后累积一定时间再移动到持久代区域。
3.Java的四种引用
-
强引用 如果一个对象具有强引用,它不会被垃圾回收器回收。即使当前内存不足也不会回收它,而是抛出OOM。
-
软引用
软引用在内存充足的时候,不会被垃圾回收器回收,只有在内存不足时,会被垃圾回收器回收。 -
弱引用
弱引用不管内存是否充足,都会被垃圾回收器回收。 -
虚引用
形同虚设,在任何时候都可能被垃圾回收器回收。
4.垃圾回收算法
-
标记清除算法
标记清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。- 位置不连续,产生碎片
- 效率略低
-
两遍扫描
标记清除算法.jpg
-
复制算法
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就是将还存活的对象复制到另一块上面,然后再把已使用的内存空间一次性清理掉。- 实现简单、运行高效
- 没有内存碎片
-
利用率只有一半
复制算法.jpg
-
标记整理算法
完成标记后,不是直接清理可回收对象,而是将存活对象都向一段移动,然后清理掉端边界以外的内存。- 没有内存碎片
- 效率偏低
-
两遍扫描、指针需要调整
标记整理算法.jpg
分代收集算法
核心思想是根据对象存活的生命周期将内存划分为若干不同区域。
新生代:复制算法(因为每次垃圾回收都要回收大部分对象,需要复制操作少)
老年代:标记整理算法(每次回收只回收少量对象)
5.App内存组成以及限制
Android给每个App分配一个VM,让App运行在Dalvik
上,这样即使App崩溃也不会影响到系统。系统给VM分配了一定的内存大小,App可以申请使用的内存大小不能超过此硬性逻辑限制,就算物理内存富余,如果应用超过VM最大内存,就会出现内存溢出crash。
由程序控制操作的内存空间在heap上,分为java heapsize和native heapsize
- Java申请的内存在
vm heap
上,所以如果java申请的内存大小超过VM的逻辑内存限制,就会出现内存溢出的异常 - native层内存申请不受其限制,受native process对内存大小的限制
6.Android进程优先级
进程的优先级从高到低,它们的oom_adj值也是从小到大:
- Foreground process(前台进程)
- Visible process(可见进程)
- Service process(服务进程)
- Background process(后台进程)
- Empty process(空进程)
7.内存三大问题
-
内存抖动
短时间内有大量的对象被创建和回收的现象。 -
内存泄漏
本该回收的对象因为某些原因不能回收。 -
内存溢出
OOM,使用内存大于申请内存。
8.Android内存泄漏常见常见及解决方案
-
资源对象未关闭
对于资源对象不再使用时,应该立即调用它的close()将其关闭,然后再置为null。例如Bitmap 等资源未关闭会造成内存泄漏,此时我们应该在Activity销毁时及时关闭。 -
注册对象未注销
BroadcastReceiver、EventBus等未注销导致内存泄漏,我们应该在Activity销毁时及时注销。 -
类的静态变量持有大对象
尽量避免静态变量存储数据,特别是大数据对象,建议使用数据库等存储。 -
单例造成的内存泄漏
优先使用Application的Context,如需使用Activity的Context,可以在传入Context时使用弱引用进行封装,然后在使用到的地方从弱引用中获取Context,如果获取不到直接return即可。 -
非静态内部类的静态实例
静态实例的生命周期和应用一样长,非静态内部类会持有外部类的引用,使得该静态实例一直持有Activity的引用,导致内存泄漏。我们可以将该内部类设为静态内部类。 -
Handler引起的内存泄漏
Handler允许我们发送延时消息,如果延时期间用户关闭了Activity,那么该Activity就会泄漏。这个泄漏是因为Message会持有handler,而又因为Java的特性,非静态内部类会持有外部类,使得 Activity会被Handler持有,这样最终导致了了Activity泄漏。
解决该问题的最有效方法是:将Handler定义为静态内部类,在内部类中持有Activity的弱引用,并及时移除所有消息。(被弱引用关联的对象只能存活到下一次垃圾回收之前,被销毁的Activity会被回收内存)
private static class SafeHandler extends Handler { private WeakReference<HandlerActivity> ref; public SafeHandler(HandlerActivity activity) { this.ref = new WeakReference(activity); } @Override public void handleMessage(final Message msg) { HandlerActivity activity = ref.get(); if (activity != null) { activity.handleMessage(msg); } } }
并且再在Activity.onDestory()前移除消息,加一层保障
@Override protected void onDestroy() { safeHandler.removeCallbacksAndMessages(null); super.onDestroy(); }
-
容器中的对象没清理造成的内存泄漏
退出程序之前,将集合里的东西clear并置为null。 -
WebView内存泄漏
WebView都存在内存泄漏的问题,在应用中只要使用一次WebView,内存就不会被释放掉。我们可以为 WebView开启一个独立的进程,使用AIDL与应用的主进程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,达到正常释放内存的目的。