Android内存抖动及内存泄漏的发现、定位和解决

内存抖动是指在短时间内有大量的对象被创建或者被回收的现象,内存抖动出现原因主要是频繁(很重要)在循环里创建对象(导致大量对象在短时间内被创建,由于新对象是要占用内存空间的而且是频繁,如果一次或者两次在循环里创建对象对内存影响不大,不会造成严重内存抖动这样可以接受也不可避免,频繁的话就很内存抖动很严重),内存抖动的影响是如果抖动很频繁,会导致垃圾回收机制频繁运行(短时间内产生大量对象,需要大量内存,而且还是频繁抖动,就可能会需要回收内存以用于产生对象,垃圾回收机制就自然会频繁运行了)。综上就是频繁内存抖动会导致垃圾回收频繁运行。

内存泄漏是指某一段内存在程序里功能上已经不需要了,但是垃圾回收机制回收内存时检测那段内存还是被需要的,不能被回收,这种在程序中在没有使用的但是又不能被回收的内存就是被泄漏的内存,那为什么会这样呢?正常的话应该是程序里不需要的内存就可以被回收,这是垃圾回收机制做的事呀,如果垃圾回收机制正常运行的情况下,不应该这样啊,但是实际就是垃圾回收机制正常的情况下发生的内存泄漏。其实到这里Java程序员就得知道垃圾回收机制中,判断一段内存是否是垃圾,是否可回收的条件,这个条件是通过检查这段内存是否存在引用和被引用关系,不存在这关系时,就认为可回收,若还存在引用或被引用关系,就认为不可回收,现在就可以知道导致内存泄漏的原因是程序员没有将不用的内存去掉引用关系(因为程序中大多内存石油对象指向的,所以去掉引用关系就是置空)。内存泄漏会导致一些内存没法被正常利用,话句话就是可以使用内存变少了,这样轻则增加垃圾回收机制运行频率,重则内存溢出(当系统需要分配一段内存,但是现有内存在垃圾回收运行后任然不足时,就会内存溢出);为避免内存泄漏,在写程序时已经确定不需要的引用型变量,就置空;虽然即使内存没泄露,也有可能出现内存溢出,这时的内存溢出就是有别的问题导致的。

1) Memory Churn and performance(内存抖动和性能)

虽然Android有自动管理内存的机制,但是对内存的不恰当使用仍然容易引起严重的性能问题。在同一帧里面创建过多的对象是件需要特别引起注意的事情。

Android系统里面有一个Generational Heap Memory的模型,系统会根据内存中不同的内存数据类型分别执行不同的GC操作。例如,最近刚分配的对象会放在Young Generation区域,这个区域的对象通常都是会快速被创建并且很快被销毁回收的,同时这个区域的GC操作速度也是比Old Generation区域的GC操作速度更快的。

 

除了速度差异之外,执行GC操作的时候,任何线程的任何操作都会需要暂停,等待GC操作完成之后,其他操作才能够继续运行(所以垃圾回收运行的次数越少,对性能的影响就越少)

 

通常来说,单个的GC并不会占用太多时间,但是大量不停的GC操作则会显著占用帧间隔时间(16ms)。如果在帧间隔时间里面做了过多的GC操作,那么自然其他类似计算,渲染等操作的可用时间就变得少了。

导致GC频繁执行有两个原因:

·Memory Churn内存抖动,内存抖动是因为大量的对象被创建又在短时间内马上被释放。

·瞬间产生大量的对象会严重占用Young Generation的内存区域,当达到阀值,剩余空间不够的时候,也会触发GC。即使每次分配的对象占用了很少的内存,但是他们叠加在一起会增加Heap的压力,从而触发更多其他类型的GC。这个操作有可能会影响到帧率,并使得用户感知到性能问题(帧率是Android渲染机制中的概念,导致卡顿慢的直接原因,就是渲染机制受阻,关于渲染机制有另一篇博客特别说了,想了解的可以点击这里)。

 

解决上面的问题有简洁直观方法,如果你在Memory Monitor里面查看到短时间发生了多次内存的涨跌,这意味着很有可能发生了内存抖动。

 

同时我们还可以通过Allocation Tracker来查看在短时间内,同一个栈中不断进出的相同对象。这是内存抖动的典型信号之一。

当你大致定位问题之后,接下去的问题修复也就显得相对直接简单了。例如,你需要避免在for循环里面分配对象占用内存,需要尝试把对象的创建移到循环体之外,自定义View中的onDraw方法也需要引起注意,每次屏幕发生绘制以及动画执行过程中,onDraw方法都会被调用到,避免在onDraw方法里面执行复杂的操作,避免创建对象。对于那些无法避免需要创建对象的情况,我们可以考虑对象池模型,通过对象池来解决频繁创建与销毁的问题,但是这里需要注意结束使用之后,需要手动释放对象池中的对象。

2) Garbage Collection in Android(Android垃圾回收)

JVM的回收机制给开发人员带来很大的好处,不用时刻处理对象的分配与回收,可以更加专注于更加高级的代码实现。相比起JavaCC++等语言具备更高的执行效率,他们需要开发人员自己关注对象的分配与回收,但是在一个庞大的系统当中,还是免不了经常发生部分对象忘记回收的情况,这就是内存泄漏。

原始JVM中的GC机制在Android中得到了很大程度上的优化。Android里面是一个三级Generation的内存模型,最近分配的对象会存放在Young Generation区域,当这个对象在这个区域停留的时间达到一定程度,它会被移动到Old Generation,最后到Permanent Generation区域。

 

每一个级别的内存区域都有固定的大小,此后不断有新的对象被分配到此区域,当这些对象总的大小快达到这一级别内存区域的阀值时,会触发GC的操作,以便腾出空间来存放其他新的对象。

 

前面提到过每次GC发生的时候,所有的线程都是暂停状态的。GC所占用的时间和它是哪一个Generation也有关系,Young Generation的每次GC操作时间是最短的,Old Generation其次,Permanent Generation最长。执行时间的长短也和当前Generation中的对象数量有关,遍历查找20000个对象比起遍历50个对象自然是要慢很多的。

虽然Google的工程师在尽量缩短每次GC所花费的时间,但是特别注意GC引起的性能问题还是很有必要。如果不小心在最小的for循环单元里面执行了创建对象的操作,这将很容易引起GC并导致性能问题。通过Memory Monitor我们可以查看到内存的占用情况,每一次瞬间的内存降低都是因为此时发生了GC操作,如果在短时间内发生大量的内存上涨与降低的事件(内存严重抖动),这说明很有可能这里有性能问题。我们还可以通过Heap and Allocation Tracker工具来查看此时内存中分配的到底有哪些对象。

到这里为止就简单介绍了内存抖动(概念及判断方法,方法是两个工具使用,一个用于判断有没有严重内存抖动Memory Monitor,一个用于确认抖动位置Heap and Allocation Tracker),及较详细的介绍了Android中垃圾回收机制。

2) Performance Cost of Memory Leaks(内存泄漏)

虽然Java有自动回收的机制,可是这不意味着Java中不存在内存泄漏的问题,而内存泄漏会很容易导致严重的性能问题。

内存泄漏指的是那些程序不再使用的对象无法被GC识别,这样就导致这个对象一直留在内存当中,占用了宝贵的内存空间。显然,这还使得每级Generation的内存区域可用空间变小,GC就会更容易被触发,从而引起性能问题。

寻找内存泄漏并修复这个漏洞是件很棘手的事情,你需要对执行的代码很熟悉,清楚的知道在特定环境下是如何运行的,然后仔细排查。例如,你想知道程序中的某个activity退出的时候,它之前所占用的内存是否有完整的释放干净了?首先你需要在activity处于前台的时候使用Heap Tool获取一份当前状态的内存快照,然后你需要创建一个几乎不这么占用内存的空白activity用来给前一个Activity进行跳转,其次在跳转到这个空白的activity的时候主动调用System.gc()方法来确保触发一个GC操作。最后,如果前面这个activity的内存都有全部正确释放,那么在空白activity被启动之后的内存快照中应该不会有前面那个activity中的任何对象了。


关于内存抖动和内存泄漏就到这里了,接下来就说一下Android studio 提供的内存优化方面的工具

Android Studio提供了工具来帮助开发者发现和解决内存抖动和内存泄漏。

 Tool - Memory Monitor(用于发现内存抖动及内存泄漏的)

Android Studio中的Memory Monitor可以很好的帮组我们查看程序的内存使用情况。


以下内容很重要,以下内容很重要,以下内容很重要重要的事情说三遍

·Memory Monitor:查看整个app所占用的内存,以及发生GC的时刻,短时间内发生大量的GC操作是一个危险的信号(用于发现有没有内存泄漏和严重内存抖动)。

后面两个是用于定位的内存抖动和内存泄漏发生的具体位置·

Allocation Tracker:使用此工具来追踪内存的分配,前面有提到过。

Heap Tool:查看当前内存快照,便于对比分析哪些对象有可能是泄漏了的.


现在就可以定位到某一段代码发生了内存泄漏或抖动。

如果是内存泄漏解决方法就很直接,在适当的时候把泄漏的对象置空就可以了。

但是如果只内存抖动的话就得分两种情况,由于内存抖动是在短时间内创建释放大量对象导致的(一般是循环内创建对象),直接办法就是不再短时间内创建大量对象,如果创建对象的过程可以拿到循环外而不影响功能,这种情况比较容易解决。但是更多的是另一种情况,就是不能拿到循环外,否则影响功能。对于第二种情况就要做到在循环内创建对象,但是又要控制对象个数,这个问题目前可以使用对象池的方法解决。

 3)Object Pools

在程序里面经常会遇到的一个问题是短时间内创建大量的对象,导致内存紧张,从而触发GC导致性能问题。对于这个问题,我们可以使用对象池技术来解决它。通常对象池中的对象可能是bitmapsviewspaints等等。关于对象池的操作原理,不展开述说了,请看下面的图示:

 

使用对象池技术有很多好处,它可以避免内存抖动,提升性能,但是在使用的时候有一些内容是需要特别注意的。通常情况下,初始化的对象池里面都是空白的,当使用某个对象的时候先去对象池查询是否存在,如果不存在则创建这个对象然后加入对象池,但是我们也可以在程序刚启动的时候就事先为对象池填充一些即将要使用到的数据,这样可以在需要使用到这些对象的时候提供更快的首次加载速度,这种行为就叫做预分配。使用对象池也有不好的一面,程序员需要手动管理这些对象的分配与释放,所以我们需要慎重地使用这项技术,避免发生对象的内存泄漏。为了确保所有的对象能够正确被释放,我们需要保证加入对象池的对象和其他外部对象没有互相引用的关系。

其实对象池给笔者感觉与线程池相似,不同的是重心不同,线程池考虑的是运行速度提高(使用预先产生空闲线程的方式),对象池更侧重与数量(可能是分配对象内存时间是很短的,所以不需要预分配,导致对象池的预分配优势不明显)。

现在问题还没解决呢,关于解决内存抖动,对象池很好,但是仅仅是一个思想概念,没具体化。怎么实现呢,笔者推荐使用Java的一个LinkedHashMap 这个类,与普通hashmap有不同,就是可以控制数量关于LinkedHashMap 更详细的信息,笔者已转载一篇感觉很棒的关于LinkedHashMap的博客,点击这里可查看

在这里Android已提供了一个类可以解决控制数量问题

LRU Cache 通过使用LinkedHashMap实现了LRU Cache (最近最少使用)算法,这是操作系统的一个算法,具体的自己百度很多,在这里不祥细说明。

LRUCache 的实现和使用,笔者也转载了一篇博客,感觉很全点击这里可查看,看了妈妈再也不用担心我的Android程序出现内存抖动了。


一般情况下,常见发生内存泄漏定位到的地方及解决方法如下:

1.集合类

集合类如果仅仅有添加元素的机制,而没有相应删除元素机制,这样就会造成内存被占用,如果这个类是全局性变量(比如类中有静态属性,全局性的map等即有静态引用或final一直指向它)。那么没有相应删除机制,很可能导致集合所占内存只增不减。  解决办法:在使用集合类时,增加删除元素机制,并适当调用减少集合所占内存。

2.单例模式

不正确使用单例模式,也会引起内存泄漏单例对象在初始化后将在JVM的整个生命周期存在(以静态变量方式),如果单例对象持有外部对象的引用,那么这个外部对象就会一直占用着内存,可能导致内存泄漏(取决于这外部对象是否一致有用)。   解决办法:单例对象中避免含有不是一直都有用的外部对象引用。

3.Android组件或特殊集合对象的使用

BraodcastReceiver ,ContentObserver,fileObserver,Cursor,Callback等在Activity onDestory或者某类生命周期结束之后一定要unregistere或者close掉,否则这个Activity类会被system强引用,不会被回收。不要直接对Activity进行直接引用作为成员变量,如果不得不这么做,调用private WeakPeferense mActivity 来做,相同的,对与Service等其他有自己生命周期的对象来说,直接引用都需要考虑是否会存在内存泄露的可能。

4.Handler

要知道,只要Handler 发送的Message尚未被处理,则该Message及发送它的Handler对象将被线程MessageQueue一直持有。由于Handler属于TLSThread Local Storage)变量,生命周期和Activity是不一致的。因此这种实现方式一般很难保证跟view或者Activity的生命周期保持一致,故很容易导致无法正确释放。如上所述,Handler使用要特别小心,否则很可能内存泄漏。   解决办法:在view 或者Activity生命周期结束前,确保Handler已没有未处理的消息(特别是延时消息)。

5.Thread 内存泄漏

线程也是造成内存泄露的一个重要源头,线程产生内存泄露的主要原因在于线程生命周期不可控,比如线程是Activity的内部类,则线程对象中保存了Activity的一个引用,当线程的run函数耗时较长没有结束时,线程对象是不会被销毁的,因此它所引用的老的Activity就出现了内存泄漏问题。解决办法:1.简化线程run函数执行的任务,使他在Activity生命周期结束前,任务运行完。2.Thread增加撤销机制,当Activity生命周期结束时,将Thread的耗时任务撤销(笔者推荐这种)。

6.一些不良代码造成的内存压力  

有些代码并不造成内存泄漏,但是他们是对没使用的内存没进行有效及时的释放,或是没有有效的利用已有的对象而是频繁的申请新内存。

(1) Bitmap 没调用recycle()

Bitmap 对象在不使用时,我们应该先调用recycle()释放内存,然后才置空,因为加载bitmap对象的内存空间,一部分是java的,一部分是c的(因为Bitmap分配的底层是通过jni调用的,Android的Bitmap底层是使用skia图形库实现,skia是用c实现的)。这个recycle()函数就是针对c部分的内存释放。

2)构造Adapter时,没有使用缓存的convertView。   解决办法:使用静态holdview的方式构造Adapter


转载自:

http://blog.csdn.net/huang_rong12/article/details/51628264

http://blog.csdn.net/huang_rong12/article/details/51628750

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值