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块中关闭的资源,以确保即使在发生异常的情况下也始终关闭该资源。
总结
内存溢出
内存溢出
当程序运行时需要的内存超过剩余的内存时,就会内存溢出的错误,通过概念可知,内存泄露是内存溢出的可能原因之一
内存溢出产生原因
系统中存在中无法回收的内存或者使用的内存过多,最终使程序运行大于能提供的最大内存,或者也可以理解为,泄露是过程,溢出是结果
内存溢出影响
程序崩溃,即OOM error
常见的内存溢出原因及解决方法
溢出原因
- 内存中加载的数据过于庞大,如一次从数据库中取出多个数据;
- 集合类中有对象的引用,使用后未对其进行清空,使JVM无法回收;
- 代码中存在死循环或循环产生过多重复的对象实体;
- 使用第三方的软件的中BUG;
- 内存设置过小
解决方法
- 修改内存,直接增加内存
- 检查错误日志,查看OOM之前是否还有其他的错误。
- 对代码进行走查和分析
- 使用内存查看工具动态查看内存使用情况
内存分析工具
哪怕完全了解 内存泄露的原因,但难免还是会出现内存泄露的现象
下面将简单介绍内存泄露的分析工具
具体工具
- Android Studio的 Monitor
- Eclipse的MAT(Memory Analysis Tools) :www.eclipse.org/mat/
- YourKit:www.yourkit.com/
开源库
- 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的耗时和执行次数 尽量使用静态内部类 尽量使用基础数据类型 合适的时候使用软/弱引用
线上监控方式
-
常规监测
- 当内存使用超过80%,使用 Debug.dumpHprofData(String fileName) 获取dump文件回传至服务器,而后手动分析
- LeakCanary集成并带到线上
-
Probe线上监测工具
-
LeakInspector
-
ResourceCanary
内存相关的基础知识
四种引用类型的介绍
- 强引用(StrongReference):JVM 宁可抛出 OOM ,也不会让 GC 回收具有强引用的对象;
- 软引用(SoftReference):只有在内存空间不足时,才会被回的对象;
- 弱引用(WeakReference):在 GC 时,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存;
- 虚引用(PhantomReference):任何时候都可以被GC回收,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否存在该对象的虚引用,来了解这个对象是否将要被回收。可以用来作为GC回收Object的标志。