Android---内存性能优化

目录

Android 内存回收机制

Java 的四种引用

垃圾回收算法

App 内存组成以及内存限制

内存抖动

内存泄漏

内存溢出

内存分析工具

Android 内存泄漏常见场景以及优化方案


Android 内存回收机制

1. 对象创建后在 Eden 区;

2. 执行 GC 后,如果对象仍然存活,则复制到 S0(Suvival0) 区;

3. 当 S0 区满时,该区域存活对象将复制到 S1 区,然后 S0 区清空,接下来 S0 和 S1 角色互换;

4. 当第3步达到一定次数(系统版本不同会有差异)后,存活对象将被复制到 Old Generation(老年代);

5. 当这个对象在 Young Generation 区域停留的时间达到一定程度时,它会被移动到 Old Generation, 最后累计一定时间再移动到 Permanent Generation 区域。

Java 的四种引用

1. 强引用(StrongReference)

如果对象被强引用了,即使 GC 扫描到了,无论如何也回收不了的,最终就会造成内存溢出。在代码中普遍使用的,类似 Person person = new Person(); 如果一个对象具有强引用,则无论在什么情况下,GC 都不会回收被引用的对象。当内存空间不足时,JAVA 虚拟机宁可抛出 OutOfMemoryError 终止应用程序也不会回收具有强引用的对象。

2. 软引用(SoftReference)

对于软引用,GC 扫描到了,对象不一定被回收。只有当内存不足的时候,才会被回收。表示一个对象处在有用但非必须的状态。如果一个对象具有软引用,在内存空间充足时,GC 就不会回收该对象;当内存空间不足时,GC 就会回收该对象的内存(回收发生在 OutOfMemorryError 之前)

软引用可以和一个引用队列(ReferenceQueue) 联合使用,如果软引用所引用的对象被 GC 回收, Java 虚拟机就会把这个软引用加入到与之关联的引用队列中,以便在恰当的时候将该软引用回收。但是由于 GC 线程的优先级较低,通常手动调用 System.gc() 并不能立即执行 GC,因此弱引用所引用的对象并不一定会被马上回收。

3. 弱引用(WeakRefefence)

对于弱引用,只要对象被 GC 扫描到了,就会被回收。用来描述非必须的对象。它类似软引用,但是强度比软引用更弱一些;弱引用具有更短的生命。GC 在扫描的过程中,一旦发现只具有被弱引用关联的对象,都会回收掉被弱引用关联的对象。换言之,无论当前内存是否紧缺,GC 都将回收被弱引用关联的对象。

4. 虚引用(PhantomReference)

虚引用等同于没有引用,这意味着在任何时候都可能被 GC 回收,设置虚应用的目的是为了被虚引用关联的对象在被垃圾回收器时,能够收到一个系统通知。(被用来跟踪对象被 GC 回收的活动)虚引用和弱引用的区别在于:虚引用在使用时必须和引用队列(ReferenceQueue)联合使用,其在 GC 回收期间的活动如下:

ReferenceQueue queue=new ReferenceQueue();
PhantomReference pr=new PhantomReference(object.queue);

也即是 GC 在回收一个对象时,如果发现该对象具有虚引用,那么在回收之前会首先把该对象的虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入虚引用来了解被引用的对象是否被 GC 回收。 

垃圾回收算法

1. 标记清楚算法

扫描两遍。第一遍扫描的时候,区分那些对象是存活的、那些是可回收的,然后把它们标记出来。第二次扫描时,就把那些标记为可回收的区域(下图中黑色)给全部回收掉。这样做有一个很大的缺点就是,多次这样做后会产生大量的内存碎片。

特点:1. 位置不连续,产生碎片;2. 效率略低;3. 两遍扫描;

2. 复制算法

复制算法:把内存空间分成上下两半,每次只使用其中的一半(即,如果内存空间大小为 4M, 那么真正使用的就只有 2M ) 。当其中一半的内存不够时,就对其进行回收,如果该部分有存活对象,会把这些对象复制到另外一半区域区,并在这里进行内存分配。

复制算法运行效率非常高,没有内存碎片。

特点:1. 实现简单、运行高效;2. 没有内存碎片;3. 利用率只有一半;

运用场景:新生代中对象(对象存活率非常低,容易产生垃圾,GC很频繁)

 3. 标记整理算法

扫描两边。第一遍扫描时,把存活对象、可回收对象标记出来。第二遍扫描时,整理内存,把存活对象放到一起,把可回收对象回收了,并与未使用的内存一起整理到另外一边。所以,它的效率是最低的

特点:1. 没有内存碎片;2. 效率偏低;3. 两遍扫描、指针需要调整;

运用场景:老年代中对象(对象存活久,不容易产生垃圾,使用频率没那么高)

App 内存组成以及内存限制

Android 给每个 App 分布一个 VM,让 App 运行在 dalvik 虚拟机上,这样即使 App 崩溃也不会影响到系统。系统给 VM 分配了一定的内存大小,App 可以申请使用的内存大小不能超过此硬性逻辑限制,就算物理内存富余。如果应用超出 VM 最大内存,就会出现内存溢出 crash。

由程序控制操作的内存空间在 'heap' 上,分 ' java heapsize ' 和 ' native heapsize '。

Java 申请的内存在 ' vm heap ' 上,所以如果 ' java ' 申请的内存大小超过 ' VM ' 的逻辑内存限制,就会出现内存溢出的异常。 

native 层内存申请不受其限制,native 层受 native process 对内存大小的限制。

内存抖动

  \bullet 内存抖动是由于短时间内有大量对象进出新生区导致的,内存忽高忽低,有短时间内快速上升和下落的趋势,分析图呈锯齿状

  \bullet 它伴随着频繁的 GCGC 会大量占用 UI 线程和 CPU 资源,会导致 APP 整体卡顿(因为 GC 在回收内存时会停止工作线程),甚至有 OOM 的可能(原因:在新生代创建大量对象,而给新生代分配的空间又比较少,所以它就会占用一部分老年代的空间,然后此时有大对象满足了从新时代进入老年代的条件,而老年代里因为空间不够,再加上没有一块连续的空间分配给这个大对象,这时候就会报 OOM 错误)。

内存泄漏

  \bullet 程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费。

判断对象的存活---可达性分析

如果一个对象是根可达(从 GC roots 出发),那它就不会被回收;如果是根不可达,那么它就会被回收。 而内存泄漏这是,一个对象满足根可达,但它却不可用了,却又不能回收。

内存泄漏的典型案例:一个静态变量引用了一个 Activity,而这个静态变量是全局的,只要 APP 进程没有被杀死,这个静态变量则一直存在。而 Activity 已经执行了生命周期 onDestory(),但是这个 Activity 被静态变量所应用又根可达,那么这个 Activity 就发生了内存泄漏。

内存溢出

内存溢出(OOM):OOM 时会导致程序异常。Android 设备出厂以后,java 虚拟机对单个应用的最大内存分配就确定下来了,超出这个值就会 OOM

内存分析工具

1. Android Profiler

内存性能分析器是 Android Profiler 中的一个组件,可帮助我们识别可能会导致应用卡顿甚至崩溃的内存泄漏和内存抖动。它显示一个应用内存使用量的实时图表,让我们可以

  \bullet 捕获堆转储

  \bullet 强制执行垃圾回收

  \bullet 跟踪内存分配

2. MAT(Memory Analyzer Tool)

内存快照对比,为了更有效率的找出内存泄漏的对象,一般会获取两个堆转储文件(先 dump 一个hprof 文件,隔段时间再 dump 一个 hprof 文件),通过对比后的结果可以很方便定位。

Android 内存泄漏常见场景以及优化方案

1. 资源性对象未关闭

对于资源性对象不再使用时,应该立即调用它的 close() 函数将其关闭,然后再置为 null。例如 Bitmap 等资源未关闭会造成内存泄漏,此时我们应该在 Activity 销毁时及时关闭。 

2. 注册对象未注销

例如 BraodcastReceiverEventBus 未注销造成的内存泄漏,我们应该在 Activity 销毁时及时注销。 

3. 类的静态变量持有大数据对象

尽量避免使用静态变量存储数据,特别是大数据对象,建议使用数据库存储。 

4. 单例造成的内存泄漏

优先使用 Application 的 Context,如需使用 Activity 的 Context,可以在传入 Context 时使用弱引用进行封装,然后,在使用到的地方从弱引用中获取 Context,如果获取不到,则直接 return 即可。

5. 非静态内部类的静态实例

该实例的生命周期和应用一样长,这个就导致该静态实例一直持有该 Activity 的引用,Activity 的内存资源不能正常回收。此时,我们可以将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用到 Context,尽量使用 Application Context,如果需要使用 Activity Context,就记得用完后置空让 GC 可以回收,否则还是会内存泄漏。

6. Handler 临时性内存泄漏

Message 发出之后存储在 MessageQueue 中,在 Message 中存在一个 target,它是 Handler 的一个引用,Message 在 Queue 中存在的时间过长,就会导致 Handler 无法回收。如果 Handler 是 非静态的,则会导致 Activity 或者 Service 不会被回收。并且消息队列是在一个 Looper 线程中不断地轮询处理消息,当这个 Activity 退出时,消息队列中还有未处理的消息或者正在处理的消息,并且消息队列中的 Message 持有 Handler 实例的引用,Handler 又持有 Activity 的引用,所以导致该 Activity 的内存资源无法及时回收,引发内存泄漏。解决方案如下所示: 

1. 使用一个静态 Handler 内部类,然后对 Handler 持有的对象(一般是 Activity)使用弱引用,这样在回收时,也可以回收 Handler 持有的对象。

2. 在 Activity 的 Destory 或者 Stop 时,应该移除消息队列中的消息,避免 Looper 线程的消息队列中有待处理的消息需要处理。

需要注意的是,AsyncTask 内部也是 Handler 机制,同样存在内存泄漏的风险,但其一般是临时性的。对于类似 AsyncTask 或者线程造成的内存泄漏,我们也可以将 AsyncTask 和 Runnable 类独立出来或者使用静态内部类。

7. 容器中的对象没清理造成的内存泄漏

在退出程序之前,将集合里的东西 clear 掉,然后置为 null, 再退出程序。

8. WebView

WebView 都存在内存泄漏的问题,在应用中只要使用一次 WebView,内存就不会被释放掉。我们可以为 WebView 开启一个独立的进程,使用 AIDL 与应用的主进程进行通信,WebView 所在的进程可以根据业务的需要选择合适的时机进行销毁,达到正常释放内存的目的。

9. 使用 ListView 时造成的内存泄漏

在构造 Adapter 时,使用缓存的 convertView。

字符串的拼接每次都会创建一个新的字符串对象,所以尽量使用 StringBuilder 来完成,StringBuilder 内部是操作一个字符数组来完成的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

别偷我的猪_09

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值