手把手讲解 性能优化案例(3)内存抖动和泄漏的优化
前言
手把手讲解系列文章,是我写给各位看官,也是写给我自己的。
文章可能过分详细,但是这是为了帮助到尽量多的人,毕竟工作5,6年,不能老吸血,也到了回馈开源的时候.
这个系列的文章:
1、用通俗易懂的讲解方式,讲解一门技术的实用价值
2、详细书写源码的追踪,源码截图,绘制类的结构图,尽量详细地解释原理的探索过程
3、提供Github 的 可运行的Demo工程,但是我所提供代码,更多是提供思路,抛砖引玉,请酌情cv
4、集合整理原理探索过程中的一些坑,或者demo的运行过程中的注意事项
5、用gif图,最直观地展示demo运行效果如果觉得细节太细,直接跳过看结论即可。
本人能力有限,如若发现描述不当之处,欢迎留言批评指正。
学到老活到老,路漫漫其修远兮。与众君共勉 !
正文大纲
- jvm内存管理常识
- 检测以及处理内存抖动
- 检测以及处理内存泄漏
正文
jvm内存管理常识
- LMK (LowMemoryKill)机制
android底层会在系统内存告急的时候,按照一定规则杀死一些进程来满足其他进程的内存需要。其中 消耗内存的高低就是其中一项指标,所以,优化app的内存占用,能够有效降低app被系统杀死的概率。
- GC STW机制
GC,垃圾回收进程,在GC线程执行任务的时候,会存在一个 STW (stop the world) 机制,他就会把其他所有线程都挂起。如果GC非常频繁地调用,那就会导致主线程不流畅,给用户的感觉就是卡顿。
- 内存抖动频繁引起OOM
内存抖动太频繁,导致大量对象频繁创建和销毁,会产生大量不连续的内存空间,如果此时有一个大对象需要申请内存,就有可能申请失败,导致OOM内存溢出。
- 一句话解释 内存泄漏
长生命周期的对象持有短生命周期对象的强引用,在短生命周期对象需要回收的时候发现不能被回收,视为泄漏
- GC回收 可达性分析
- GC线程判定 一个对象是不是可以回收,是根据可达性分析算法,计算
GcRoot,从GcRoot向下搜索,把GcRoot没有直接关联的对象全部作为垃圾来回收。
- 强软弱虚四大引用
强和虚自不必说。强 最常见,没有特殊处理的都是强引用(包括,匿名内部类会持有外部类的强引用)。虚引用没什么用,不予讨论。
软引用,用来定义一些还有用,但是不是必须的对象,使用SoftRefrence<T>修饰,在内存紧张的时候,GC回收之后,使用SoftRefrence<T>修饰,如果系统还有足够的内存可用,那么软引用关联的对象就不会被回收。如果不足,则回收软引用关联的对象。
弱引用(WeakRefrence<T>),比软引用更弱一些,只要GC触发,弱引用关联的对象就会被回收。
注意,使用软和弱引用,要判定关联对象是否为空。
检测以及处理内存抖动
我们使用s开发,平时我们运行app,一般会点 RunApp,但是还有另一个选择, 那就是 profileApp, 运行app起来之后,会在as下方看到profile 窗口
profileApp
点击之后,as下方会出现profile,图中会显示网络,内存和cpu使用情况
image.png
如果内存的图中抖动得非常明显,比如像这样的心电图一样:
模拟抖动
那就说明非常明显存在内存抖动,急需处理:
点击内存图形区域之后,就能看到详细的内存变化情况,以及内存分配情况:
image.png
这里有个坑:
如果你从图形中观察到,内存走势平稳,并没有出现上满模拟抖动的图中那么夸张,是不是就不存在内存抖动呢?并不是。因为我们的gc,是在内存不可用的情况下才会去回收内存,如果app占用内存一直比较少,没有触及gc的临界值,那么就不会出现
断崖式下跌. 那么这样就观察不出内存抖动了,怎么办呢?
解决方法
在8.0以下的安卓手机上,在下方的位置上会出现一个Record按钮(如果是8.0以上,你可以直接用拖拽的方式来截取一段内存record):
image.png
点击它,一段时间之后,再点一下:你就能在下方发现一张表格:image.png
这张表格代表的是,你Record这段时间之内创建的对象,点击一下第二列Allocations,对创建的数量进行排序,找出创建次数最多的对象:
image.png
然后,点击排行第一的String之后,会在右方看到:
image.png
然后点击其中的一个,又会看到一个新的窗口:
image.png
到此为止,就找到了创建对象的元凶,以这个为线索,找到你们自己包名下的类和方法,确定是我们自己的代码在不合理地创建对象.
再往后,就是根据各自的业务代码去做优化了,记住一个宗旨:不要让代码干多余的事。如果是我们调用了系统的api导致了不合理地大量对象的创建,那么就要考虑这个系统API为什么会这样创建对象,有没有其他方法避免吗,从业务代码层来合理使用这个api,实在不行再考虑自定义api或者换个系统api。
在我们做了一次优化之后,再profile运行一次app,再重复上面的过程。以此类推,直到内存抖动达到理想状态。
总结
优化内存抖动,核心就是防止频繁创建对象。常见的反面教材就是:循环中创建对象,大量调用的api中创建对象。而优化的主要手段,就是对象复用,常见的手段是:对象池,像是 Handler的Message 单链表池,Glide的bitmap池等。
检测以及处理内存泄漏
经典案例:处理
handler异步任务导致的内存泄漏方法
- 在Activity的onDestroy中移除所有的任务
@Override
protected void onDestroy() {
super.onDestroy();
handler.removeCallbacksAndMessages(null);//移除所有任务
}
- 使用静态内部类 + Activity弱引用的方式
MyHandler handler = new MyHandler(this);
private static class MyHandler extends Handler {
WeakReference<Activity> activityWeakReference;
MyHandler(Activity activity) {
activityWeakReference = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
//在执行任务的时候,判断弱引用所关联的对象是否为空,能在对象已经被回收的情况下,不去执行不必要的任务
switch (msg.what) {
case 1:
if (activityWeakReference.get() != null) {
//TODO
}
}
}
}
工具的使用
依然是profileApp,先用profile看出内存的变化情况。
- 问:如何判断内存泄漏?
答:内存泄漏是精细功夫,不能全盘观察,只能凭借profile的内存变化来推测。
比如,打开app之后内存一路飙升,直到超出app能够使用的最大内存,app崩溃,,这是最明显的。
又比如,你反复打开关闭某一个界面,发现内存的稳定线(内存稳定之后,内存占用值)随着每一次的打开关闭,都在提高,这说明,这一个界面上存在泄漏,有对象无法被回收。
上一章节使用profile 最多是了解到 哪些对象的创建和回收引起了内存抖动,但是,涉及到泄漏,只通过profile尚且不能知道是哪个类持有了希望被回收的对象的强引用.
这里就要借助另外一款工具,他的名字叫做 Eclipse Mat (自行百度)
先回到刚才的profile,
image.png
点一下,然后再点一下,界面会自动跳转:
image.png
image.png
点击上面的保存按钮,将文件存到本地;
然后:
image.png
但是这个文件是无法直接在mat打开的。
找到SDK目录下的要hprof-conv.exe:
image.png
使用cmd命令,对文件进行转换,命令为:hprof-conv [源文件名] [目标文件名]
如hprof-conv 1.hprof 2.hprof回车
将得到的2.hprof利用刚才下载的Mat工具打开:
这里有很多指标,但是检查内存泄漏,我们只需要关注这个直方图按钮即可:MAT
image.png
这个图中会列出你dump的这一段内存中的所有对象,包括framework层的,也包括我们自己代码创建的对象。image.png
案例模拟
我模拟了一个经典案例,也就是前面提到的
Handler延时任务导致Activity不能被释放,核心代码如下:image.png
我就用一个非常普通的方式创建了一个handler对象,并且用它来执行一段延时任务,只不过,延时任务的延时时间是Integer的最大值,也就是说,任务要很久以后才会执行。之后,我反复进出这一个Activity,然后按照上面的方式dump了一段hprof,经过hprof-conv转化,然后用Mat打开:
结果如下:image.png
我填写过滤信息:SecondActivity回车:在我们最终退出
SecondActivity之后,内存中依然保留了18个无用的对象。
那么是不是我们这18个都是泄漏的呢?
不一定。
image.png 前文讲过,只有不合理的强引用,才会导致内存泄漏,所以我们要按照上面的方式排除软弱虚引用。
之后我们能看到下面的界面,把能展开的信息尽数展开:image.png
了解Handler源码的同志们应该一眼就看明白了,handler引起了内存泄漏,是因为存在不合理地强引用链,
上图中可以看出,最终是callback对象持有了SecondActivity对象。
callback 任务的延时时间太长了,还没有执行完,所以强引用不会给你释放掉,而callback持有了Activity,导致Activity不能被释放。
如何优化内存泄漏
我们刚才已经看到了Handler的不合理使用导致了内存泄漏,那么如果在
onDestroy中移除所有的任务:image.png
执行同样的任务,dump下来的hprof 在mat:
image.png
触发了GC之后,SecondActivity数量变为了0,内存泄漏解决。
当然还有另一种做法,静态内部类+弱引用。
ps:静态内部类是为了防止内部类持有外部类的引用,弱引用是为了在GC触发之时,回收掉WeakRefrence中的对象。
public class SecondActivity extends AppCompatActivity {
Handler handler = new Handler();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
handler.postDelayed(runnable, Integer.MAX_VALUE);//依然是那个延时很久的任务
}
Runnable runnable = new MyRunnable(this);
private static class MyRunnable implements Runnable {// 静态内部类
WeakReference<Activity> activityWeakReference;//弱引用
MyRunnable(Activity activity) {
activityWeakReference = new WeakReference<>(activity);
}
@Override
public void run() {
}
}
}
image.png
image.png
但是排除之后,一个都没有了。
小技巧
上面的步骤虽然可行,但是如果有很多页面都需要排查泄漏,那么我们一个一个页面去点开关闭,整个过程将会非常冗长难受。其实有办法解决。
回到之前的直方图:
image.png
使用方法为:如果你想进行一个操作,你操作前后各dump一个hprof,命名为 before和after, 然后用hprof-conv转换一下,变为 before_ 和 after_ ,用eclipse mat同时打开这两个文件,然后切换到after_.hprof ,点击上图中的按钮:![]()
它会让你选择想要对比的文件,点击before_,然后过滤SecondActivity:
image.png
这种方式可以在处理泄漏之前,事先排查可能泄露的代码区域。简化我们的优化工作。
结语
内存抖动和泄漏优化涉及到Jvm很多知识点,除了我之前列出的几点之外,还有很多细枝末节。要做好 内存优化,需要扎实的JVM知识基础。
profileApp
image.png
模拟抖动
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
MAT
image.png
image.png
image.png
在我们最终退出
image.png
前文讲过,只有不合理的强引用,才会导致内存泄漏,所以我们要按照上面的方式排除软弱虚引用。
image.png
image.png
image.png
image.png
image.png
image.png
image.png