Android内存优化详解

672 阅读10分钟

Android内存优化详解

常见的内存问题

常见的内存问题有三个:

  • 内存抖动
  • 内存泄露
  • 内存溢出

内存抖动

内存抖动

指在短时间内有大量的对象被创建或者被回收的现象。

内存抖动产生原因

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

内存抖动影响

频繁内存抖动会导致垃圾回收频繁运行,造成系统卡顿。

排查方向

优先寻找循环或者频繁调用的地方进行排查。

优化方案

避免创建大量、临时的小对象。

内存泄露

内存泄露

对于 java 而言,就是存放在堆上的 Object 无法被 GC 正常回收

内存泄露产生原因

长生命周期的对象持有短生命周期对象强/软引用,导致本应该被回收的短生命周期的对象却无法被正常回收

内存释放

  • 给对象赋予了空值null,之后再没有调用过。
  • 给对象赋予了新值,这样重新分配了内存空间。

内存泄露影响

  • 应用可用的内存减少,增加了堆内存的压力
  • 降低了应用的性能,比如会触发更频繁的 GC
  • 严重的时候可能会导致内存溢出错误,即 OOM Error

常见的内存泄露及解决办法

静态属性导致内存泄露

问题:会导致内存泄露的一种情况就是大量使用static静态变量。在Java中,静态属性的生命周期通常伴随着应用整个生命周期(除非ClassLoader符合垃圾回收的条件)。

解决办法:

  • 进来减少静态变量;
  • 如果使用单例,尽量采用懒加载。
集合类

问题:集合类 添加元素后,仍引用着 集合元素对象,导致该集合元素对象不可被回收,从而 导致内存泄漏

解决方法:集合类 添加集合元素对象 后,在使用后必须从集合中删除

未关闭的资源

问题:无论什么时候当我们创建一个连接或打开一个流,JVM都会分配内存给这些资源。比如,数据库链接、输入流和session对象。忘记关闭这些资源,会阻塞内存,从而导致GC无法进行清理。特别是当程序发生异常时,没有在finally中进行资源关闭的情况。

解决办法:

  • 始终记得在finally中进行资源的关闭;
  • 关闭连接的自身代码不能发生异常;
  • Java7以上版本可使用try-with-resources代码方式进行资源关闭。
不当的equals方法和hashCode方法实现

问题:当我们定义个新的类时,往往需要重写equals方法和hashCode方法。在HashSet和HashMap中的很多操作都用到了这两个方法。如果重写不得当,会造成内存泄露的问题。

解决办法:

  • 如果创建一个实体类,总是重写equals方法和hashCode方法;
  • 不仅要覆盖默认的方法实现,而且还要考虑最优的实现方式;
外部类引用内部类

问题:这种情况发生在非静态内部类(匿名类)中,在类初始化时,内部类总是需要外部类的一个实例。每个非静态内部类默认都持有外部类的隐式引用。如果在应用程序中使用该内部类的对象,即使外部类使用完毕,也不会对其进行垃圾回收。

解决办法:如果内部类不需要访问外部类的成员信息,可以考虑将其转换为静态内部类。

finalize()方法

问题:使用finalize()方法会存在潜在的内存泄露问题,每当一个类的finalize()方法被重写时,该类的对象就不会被GC立即回收。GC会将它们放入队列进行最终确定,在以后的某个时间点进行回收。如果finalize()方法重写的不合理或finalizer队列无法跟上Java垃圾回收器的速度,那么迟早,应用程序会出现OutOfMemoryError异常。

解决办法:始终避免使用finalizer。

String的intern方法

问题:字符串常量池在Java7中从PermGen移动到了堆空间。在Java6及以前版本,我们使用字符串时要多加小心。如果读取了一个大字符串对象,并且调用其intern方法,intern()会将String放在JVM的内存池(PermGen),而JVM的内存池是不会被GC的。同样会造成程序性能降低和内存溢出问题。

解决办法:

  • 最简单的方式是更新JDK版到7及以上;
  • 如果无法避免,则可调整PermGen大小,避免OutOfMemoryErrors溢出。
使用ThreadLocal

问题:ThreadLocal提供了线程本地变量,它可以保证访问到的变量属于当前线程,每个线程都保存有一个变量副本,每个线程的变量都不同。ThreadLocal相当于提供了一种线程隔离,将变量与线程相绑定,从而实现线程安全的特性。

ThreadLocal的实现中,每个Thread维护一个ThreadLocalMap映射表,key是ThreadLocal实例本身,value是真正需要存储的Object。

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统GC时,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value。

如果当前线程迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

解决办法:

  • 使用ThreadLocal提供的remove方法,可对当前线程中的value值进行移除;
  • 不要使用ThreadLocal.set(null) 的方式清除value,它实际上并没有清除值,而是查找与当前线程关联的Map并将键值对分别设置为当前线程和null。
  • 最好将ThreadLocal视为需要在finally块中关闭的资源,以确保即使在发生异常的情况下也始终关闭该资源。
总结

image.png

内存溢出

内存溢出

当程序运行时需要的内存超过剩余的内存时,就会内存溢出的错误,通过概念可知,内存泄露是内存溢出的可能原因之一

内存溢出产生原因

系统中存在中无法回收的内存或者使用的内存过多,最终使程序运行大于能提供的最大内存,或者也可以理解为,泄露是过程,溢出是结果

内存溢出影响

程序崩溃,即OOM error

常见的内存溢出原因及解决方法

溢出原因
  • 内存中加载的数据过于庞大,如一次从数据库中取出多个数据;
  • 集合类中有对象的引用,使用后未对其进行清空,使JVM无法回收;
  • 代码中存在死循环或循环产生过多重复的对象实体;
  • 使用第三方的软件的中BUG;
  • 内存设置过小
解决方法
  • 修改内存,直接增加内存
  • 检查错误日志,查看OOM之前是否还有其他的错误。
  • 对代码进行走查和分析
  • 使用内存查看工具动态查看内存使用情况  

内存分析工具

哪怕完全了解 内存泄露的原因,但难免还是会出现内存泄露的现象
下面将简单介绍内存泄露的分析工具

具体工具

开源库

  • LeakCanary( square出品)
  • BlockCanary

内存优化空间

不必要的自动装箱

自动装箱就是将基础数据类型转化为对应的复杂类型,在HashMap的增删改查中充满了自动装箱问题,所以尽量避免这中问题,如将HashMap替换为SparseArray和ArrayMap

内存复用

资源复用:通用字符串,颜色,布局 视图复用:类似于RecyclerView的优化复用 对象池:创建对象池,不用重复创建对象,类似于线程池,messae享元模式 Bitmap对象复用:使用inBitmap属性可以告知Bitmap解码器尝试使用已经存在的内存区域,新解码的bitmap会尝试使用之前那张bitmap在heap中占据的pixel data内存区域。

在App可用内存过低时主动释放内存 在App退到后台内存紧张即将被Kill掉时选择重写Application中 onTrimMemory/onLowMemory 方法去释放掉图片缓存、静态缓存来自保。

其他场景优化

item被回收不可见时释放掉对图片的引用

ListView:因此每次item被回收后再次利用都会重新绑定数据,只需在ImageView onDetachFromWindow的时候释放掉图片引用即可。 RecyclerView:因为被回收不可见时第一选择是放进mCacheView中,这里item被复用并不会只需bindViewHolder来重新绑定数据,只有被回收进mRecyclePool中后拿出来复用才会重新绑定数据,因此重写Recycler.Adapter中的onViewRecycled()方法来使item被回收进RecyclePool的时候去释放图片引用。

如果使用字符串拼接,尽量使用StringBuilder、StringBuffer(内存抖动) 自定义view减少onDraw的耗时和执行次数 尽量使用静态内部类 尽量使用基础数据类型 合适的时候使用软/弱引用

线上监控方式

  1. 常规监测

    1. 当内存使用超过80%,使用 Debug.dumpHprofData(String fileName) 获取dump文件回传至服务器,而后手动分析
    2. LeakCanary集成并带到线上
  2. Probe线上监测工具

  3. LeakInspector

  4. ResourceCanary

内存相关的基础知识

四种引用类型的介绍

  • 强引用(StrongReference):JVM 宁可抛出 OOM ,也不会让 GC 回收具有强引用的对象;
  • 软引用(SoftReference):只有在内存空间不足时,才会被回的对象;
  • 弱引用(WeakReference):在 GC 时,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存;
  • 虚引用(PhantomReference):任何时候都可以被GC回收,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否存在该对象的虚引用,来了解这个对象是否将要被回收。可以用来作为GC回收Object的标志。

JVM内存管理

请参考:www.jianshu.com/p/26f7f597c…