开发中内存泄漏的问题一直是比较棘手的,写代码只要稍不经心就会出现侧漏,自己都不知道在哪侧漏的,最后导致翻车。
app做完了,一经过大量测试,不知不觉就崩溃,一看日志-----OOM(噗~~)。
最近看了一些博客和书籍还有视频,简单总结了一下,侧漏的发生和原因。
下面我先举个前些天看视频的小栗子,然后下面再贴出一些概念。
下面的方法可以粗略的检查出activity是否有侧漏。
我新建了一个小工程,里面就两个activity和一个utils类,通过两个界面间的跳转看看出了什么问题。
首先运行项目,打开下面工具栏中的Android Monitor:
可以看到项目运行起来了,稳定的内存8.32MB。
此时我们手机上按下回车回到手机的系统中,即我们的程序进入后台了,这时按下下图中选项的按钮。
Android studio中Android Monitor --> System Infomation --> MemoryUsage
点完之后稍等,应该会出现下图中的文件:
仔细看文件里下面这部分:
Views是0,activitys也是0。
好!此时我们手机回到应用,跳转activity,在返回,重复几次之后,我们再通过上面的操作 MemoryUsage得到上面的新文件:
发现有好多view。
而且切换之后现在的内存也增加了。刚开始的8.32MB,现在已经到了9.21MB:
我们点击GC按钮:
内存回到了8.75MB,为什么没有回到最开始的?
上面我们用MemoryUsage看的是有多少View或者Actvity存活,下面我们看看更详细的定位:
GC之后点击下图中的按钮:
就是GC旁边的按钮,点击之后等会就会出现下图的文件,没出现也不要紧在右侧Captures选项中的Heap Snapshot目录下:
按照上图切换到你自己包名目录下,我找到了我的两个activity。
上图中列表项对应的意义:
Total Count --> 内存中该类的对象个数
Heap Count --> 堆内存中该类的对象个数
Sizeof --> 物理大小
Shallow size --> 该对象本身占内存大小
Retained Size --> 释放该对象后,节省内存大小
手机上我已经回到了第一个activity了,第二个activity已经关闭了。但是从图中可以看出我的Main2Activity还存活着。
第一行蓝色代码,context被CommUtils持有着。
先看看Main2Activity中都干了什么:
public class Main2Activity extends AppCompatActivity {
private CommUtils commUtils;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
commUtils = CommUtils.getInstance(this);
}
}
初始化了一个CommUtils对象。
比较幸运这么快就能找到,现在我们可以去看看Commutils都干了什么事情:
/**
* Created by ge on 2017/9/26.
*/
public class CommUtils {
private static CommUtils instance;
private Context context;
private CommUtils(Context context){
this.context = context;
}
public static CommUtils getInstance(Context context){
if (instance == null){
instance = new CommUtils(context);
}
return instance;
}
}
一个单例,相信大家一眼就可以知道为什么了,getInstance的时候context我传入的是Activity的context,当我们要销毁Main2Activity的时候,CommUtils一直持有着Activity的实例。GC的时候,不能被销毁,所以这就造成了内存的泄漏。
并且,当我们重新打开界面时,context还是上次创建的那个,如果我们在activity中有使用commUtils实例的地方,那么就会出错了。
细思极恐。。。。
我们下面将getInstance代码稍微修改一下:
instance = new CommUtils(context.getApplicationContext());
这样是不是就好了。
上面只是一个简单的栗子,简单的定位内存泄漏的方法。
写的挺长的,主要是截图多。。。。
关于内存泄漏,发生的原因有很多,下面说一下一些概念,有助于理解内存泄漏,都是我平时看文章记下来的。
Java的四种引用:
1.强引用
强引用的对象,java宁愿oom也不会回收他。
2.软引用
比强引用弱一点的引用,在java gc的时候,如果软引用所引用的对象被回收,首次gc失败的话会继而回收软引用的对象。
软引用适合做缓存处理,可以和引用队列(ReferenceQueue)一起使用,当对象被回收之后保存它的软引用会放入引用队列。
3.弱引用
比软引用更弱的引用,当java执行gc的时候,如果弱引用的对象被回收,无论它有没有用都会回收掉弱引用的对象,不过gc是一个比较低优先级的线程,不会那么及时的回收你的对象。可以和引用队列一起使用,当对象被回收之后保存它的弱引用会放入引用队列。
4.虚引用
虚引用和没有引用是一样的,他必须和引用队列一起使用,当java回收一个对象的时候,如果发现它有虚引用,会在回收对象之前将他的虚引用加入到与之关联的引用队列中。
可以通过这个特性在一个对象被回收之前采取措施。
Java GC
目前oracle jdk和open jdk的虚拟机都是Hotsport。
android为Dalvik和Art。
曾经的GC算法:引用计数
简单说引用计数就是对每一个对象的引用计算数字,如果引用就+1,不引用就-1,回收掉引用计数为0的对象。来达到垃圾回收。
弊端:
如果两个对象都应该被回收,但是他俩却相互依赖,那么他俩的引用永远都不会为0,那么永远无法回收,却无法解决循环引用的问题。
现代GC的算法:
1.标记回收算法(Mark and Sweep GC)
从GC Root集合开始,将内存整个遍历一次,保留所有可能被GC Roots直接或间接引用到的对象,
剩下的对象都被当作垃圾对待并回收,这个算法需要中断进程内其它组件的执行并且可能产生内存碎片。
2.复制算法(Copying)
将现有内存分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到违背使用的内存块中,
之后,清除正在使用的内存块中的所有的对象,交换两个内存角色,完成垃圾回收。
3.标记压缩算法(Mark Compact)
先从根结点开始对所有可达对象做一次标记,但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端。
之后清理边界外所有的空间。这种方法避免了碎片的产生,又不需要两块相同的内存空间,因此其性价比较高。
4.分代
将所有新建对象都放入称为年轻代的内存区域,年轻代的特点是对象会很快回收,因此在年轻代就选择效率较高的复制算法。
当一个对象经过几次回收后依然存活,对象就会被放入称为老年代的内存空间。对于新生代适用于复制算法,而对于老年代则采取
标记---压缩算法。
并发GC和非并发GC
非并发GC:
虚拟机在执行GC的时候进行Stop the world,也就是挂起其它所有的线程,通常会持续上百毫秒,一次mark,然后直接清理。
初始化 --> stop the world --> Mark --> 回收 --> 执行GC结束操作
并发GC:
跟非并发的简单GC来比较,一般非并发GC需要耗费上百ms的时间来进行,而并发的GC只需要10ms左右,效率大幅提升。
但并发GC由于需进行重复的处理改动的对象,所以需要更多的cpu资源。
平时可能会造成内存泄漏的地方:
1.非静态的内部类匿名类会隐式的持有外部类的引用。
修改思路:
将Handler和Runnable改成static
在外部定义,内部使用。
2.静态变量:使用静态变量来引用一个事物,在不使用之后没有下掉,那么引用存在就会一直泄漏。
3.单例: 使用单例中保存了不应该被一直持有的对象。
4.由第三方库使用不当:例EventBus,activity销毁时没有反注册就会导致引用一直被持有。