android内存泄漏分析与检测

对于一个android开发来说,内存优化是一个亘古不变的话题。很多东西看过了当时理解了,但是过一段时间会忘,今天就系统的记录一下。

先画个思维导图,免得写起来没有章法。

image.png

想要做内存优化,首先必须得jvm的内存模型有一定的了解。

JVM内存模型

java的内存管理是交给GC(Garbage Collection)来处理的,GC会帮助我们回收不再使用的内存资源。但是也有些时候由于程序员的疏忽大意,导致GC无法回收,此时便造成了内存泄漏.

懒得画了,借用网上的图片大致说明下


1592745-3f82fafa66408ed9.png

jvm运行时模型包括

1.方法区(Method Area)
2.堆(Heap)
3.虚拟机栈(VM Stack也叫JavaStack)
4.本地方法栈(Native Method Stack)
5.程序计数器(Program Counter Register)

1和2也就是方法区和堆是线程间共享的,而345是线程私有的!

那么为什么堆和方法区是线程间共享的而其他区域却是线程间隔离的呢?

首先我们的理解方法区和堆都负责存放哪些信息

  • 堆 (Heap)

    用于存储对象实例,内存不连续,生命周期不确定,是GC重点回收区域,当内存不足时会触发OutOfMemory异常
  • 方法区(Method Area)

    主要负责存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,当内存不足时同样会触发OutOfMemory一样\color{green}{其实方法区只是逻辑上的独立,实际上方法区还是属于堆的一部分,因此方法区也是GC负责回收的区域}!
    \color{red}{方法区的垃圾回收主要有两种,分别是对废弃常量的回收(常量池的回收)和对无用类的回收(类的卸载)}
    20170329213804490.png

\color{red}{当一个常量对象不再任何地方被引用的时候,则被标记为废弃常量,这个常量可以被回收。}

  • 虚拟机栈(JavaStack)

    存储局部变量表、操作栈、动态链接、方法出口等信息,Java每个方法被执行时都会创建一个栈帧,并压入JavaStack中
    画张图帮助理解
JavaStack

那么我现在知道Java栈中存放局部变量等信息了
此时我们来思考上面的问题(为什么java栈要线程隔离呢?)
试想下如果在并发多线程时,如果A线程可以访问B线程中调用的方法中的局部变量,那么程序是不是就乱了套了
栈可能触发两种异常
1.在单线程时,如果线程申请的栈深度超出了虚拟机允许的最大深度时,会抛出StackOverflow异常!比如递归遍历的时候不断压栈就可能会爬出StackOverflow异常
2.虚拟机栈可以动态扩展,在多线程时,当栈空间超出了虚拟机分配的大小时,此时当线程申请开辟栈空间时会触发OutOfMemory异常

  • 本地方法栈(Native Heap)

    不做过多讲解,其实作用和java栈一样,只不过存储的是c/c++等JNI调用的方法中的信息
  • 程序计数器(Program Counter Register)

    存储正在执行的虚拟机字节码的指令的地址
    如果当前线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是一个Native方法,这个计数器值null。

    那么程序计数器为什么是线程间私有呢?
    我们知道java并发多线程时,其实时轮换执行,其实同一时间只有一个线程能够得到执行!
    假如A线程执行到M方法的第8行此时切换到B线程执行, 此时程序计数器会记录下M方法第8行的字节码指令地址,等到再次轮换到A线程执行的时候,就接着从第8行往下执行。设计成线程间私有,每个线程拥有一个自己的程序计数器,彼此之间不会干扰!

什么是内存泄漏,什么情况下会发生内存泄漏?

上面说了,java/kotlin new出来的对象放在 "堆"中的,那么GC是如何判断一个对象是否应该被回收了呢?

判断java对象是否该被回收通常有两种方式

1.引用计数
比如A被B引用,计数加一,A又被C引用计数加一,B对象释放了,那么A的引用计数就减一。当引用计数为0时,则该对象可以被回收!但是无法解决循环引用的问题

2.GCRoot可达性
从一个对象不断的向上追溯引用,如果无法追溯到GCRoot的时候,那么这个对象就可以被回收了!
比方说A引用B,B又引用A,但是此时A和B都没有被其他对象引用,那么A和B其实是已经没有用的垃圾对象了,但是他们的引用计数并不为0,那么此时就得通过GCRoot可达性来判断它是否可以被回收了
上图吧,文字解释太苍白无力


image.png

GC Root有哪些?

  • 虚拟机中引用的对象(局部变量引用的对象)
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用对象
  • 本地方法栈中JNI引用对象
  • 活着的线程

那么现在来想想什么是内存泄漏呢?

通俗点来说就是,某个对象明明已经没有用了,但是它却可达GCRoot,那么这时候,GC就无法对这个没有用的对象进行回收,就导致了内存泄漏。

什么情况下会发生内存泄漏呢?

举两个例子

我们知道内部类会持有外部类的引用,当我们使用Handler时,如果MyHandler不是静态内部类,那么可以看到AS会发出一个警告!加入TestActivity已经关闭了,但是此时MyHandler的MessageQueue中还有未被执行的Message,那么MyHandler并不会被销毁,而且它还持有者TestActivity的引用,那么此时就造成了TestActivity的内存泄漏

解决办法
要么将MyHandler声明为static的,要么将TestActivity使用WeakReference包裹起来!


leak.png


Singleton.java

package com.taylor.androidinterview;

import android.content.Context;

/**
 * Copyright:AndroidInterview
 * Author: liyang 
* Date:2019-10-07 20:19
* Desc:
*/ public class Singleton { private static Singleton sInstance; private static Context mContext; private Singleton(Context context) { mContext = context; } public static Singleton getInstance(Context context) { if (sInstance == null) { synchronized (Singleton.class) { if (sInstance == null) { sInstance = new Singleton(context); } } } return sInstance; } }

TestActivity.java

package com.taylor.androidinterview;

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;

import androidx.appcompat.app.AppCompatActivity;

public class TestActivity extends AppCompatActivity {
    private MyHandler myHandler;
    private Singleton mSingleton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        mSingleton = Singleton.getInstance(this);
        myHandler=new MyHandler();

        myHandler.postDelayed(() -> System.out.println("我被执行了!!!"),100000000);

        findViewById(R.id.btnClose).setOnClickListener(v-> finish());
    }

    class MyHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    }
}

SIngleton是个单例类,他的内部的mContext变量是static的,所以mContext是一个GCRoot,那么此时当TestActivity关闭时,由于Singleton的mContext是GCRoot并且持有了TestActivity,那么TestActivity并不能得到回收,同样造成了内存泄漏.
解决办法,使用ApplicationContext代替Context,因为ApplicationContext是和App生命周期一致的

其实AS已经很强大了,如果你的代码可能会导致内存泄漏,大部分情况下AS都会给出警告的,只是很多人忽视警告而已

当然android中可能导致内存泄漏的情况还有很多,比如Activity销毁了但是没有解除监听,Activity销毁了但是还有活着的线程等等

Android内存泄漏如何检测?

  • adb观察法
  • LeakCanary等三方检测库
  • 使用Profiler分析检测
  • 使用MAT进行检测

adb观察法

ttt.gif

image.png

我在MainActivity中打开了刚才写的TestActivity,然后就关闭了TestActivity,为了避免JVM没有及时回收TestActivity,我又使用Profiler主动进行了一个垃圾回收!
此时我们使用adb shell dunpsys meminfo com.taylor.androidinterview -d命令查看下app的内存情况

Objects:
中罗列的就是当前app中现在存活的实例
Views有36个,AppContext有4个,Activity有两个

我们明明关闭了TestActivity,此时app中应该只有一个Activity,但是由于TestActivity中使用Singleton和MyHandler导致了TestActivity内存泄漏了

adb观察法只能粗略的查看下内存情况,如果想要追溯内存泄漏的具体位置就得借助LeakCanary、Profiler和MAT这些工具了

LeakCanary 就不做过多介绍了

具体可以查看官方文档http://square.github.io/leakcanary
,其实LeakCanary的原理和我们接下来要讲的使用Profiler和MAT分析差不多,但是即便是使用LeakCanary还是会有一些情况它无法检测到

LeakCanary原理:
LeakCanary会在Activity的onDestroy方法中,主动调用 GC,并利用ReferenceQueue+WeakReference,来判断是否有释放不掉的引用,然后结合dump memory的hpof文件, 用HaHa分析出泄漏地方。

使用Profiler分析检测

接下来我们使用Profiler来进行分析
我又反复打开TestActivity然后又关闭掉两次,
可以看到我选取的app内存信息的时间点是在TestActivity销毁之后,但是此时可以看到App中任然有3个TestActivity实例\color{blue}{(通常点击右侧InstanceView中的对象实例,下方应该会展示出该实例的Call Stack调用栈信息的,你基本上可以在调用栈信息中找到是哪个类中的哪一行代码导致的这个TestActivity的内存泄漏!但是我点击TestActivity下方并没有出现调用栈信息,不知道是不是我这个版本AS的bug???)}
Profiler的功能非常强大,你可以用它来分析网络,分析性能,分析内存情况等,绝对是分析查找问题的好帮手
profiler的具体使用请查看google的官方文档https://developer.android.google.cn/studio/profile/memory-profiler

image.png

使用MAT进行检测分析

MAT是Memory Analysis Tools的缩写,从名称就可以看出他是专门用来分析内存的工具

  • 第一步:接下来我们使用profiler dump内存快照并保存为memory-mat.hprof
    image.png
image.png

MAT可以解析Java SE HPROF 格式的.hprof文件,但是无法解析android格式的.hprof文件,所以我们需要android_sdk/platform-tools/ hrpof-conv工具将.hprof文件转换为MAT可以分析的格式

如果你没有将hprof-conv工具配置到全局变量里那么就需要到它所在的文件夹内才能使用hprof-conv命令

  • 第二步 转换hprof文件格式
    hprof-conv <原hprof文件名 ><转换后的hprof文件名>
liyangdeMacBook-Pro:~ liyang$ cd /Users/liyang/Library/Android/sdk/platform-tools
liyangdeMacBook-Pro:platform-tools liyang$ hprof-conv /Users/liyang/Downloads/work/AndroidInterview/memory-mat.hprof /Users/liyang/Downloads/work/AndroidInterview/memory-mat-conv.hprof

接下来我们就可以使用MAT对堆存快照进行分析,精确内存泄漏的位置了

  • 第三步 使用MAT打开转换后的memory-mat-conv.hprof文件


    image.png

打开后,我们先点击Overview概览这个tab


image.png

Overview界面的下方actions这一列中有四个选项

功能名 作用
histogram 罗列出每个class类中的实例个数等情况(我们分析内存泄漏使用的就是这个功能)
DominatorTree 罗列出那些一直存活的占用内存较大的对象(如果你的app发生的OutOfMemory可以使用这个功能进行分析,从那些过于庞大的对象中找出元凶)
Top Consumer 可以根据class或者package进行分组,打印出那些最耗费性能的对象(如果你觉得你的项目运行时比较卡顿,可以使用这个功能进行分析查找)
Duplicate Classes 检测被多个ClassLoader加载的类
  • 第四步 接下来我们使用Histogram功能查看下我们的app中的实例对象情况
image.png

打开Histogram后,它默认是group by class,我们选择group by package 找到我们的app

image.png

通过按照package来分组,我们可以很快找到自己的app,从上图可以看到,何Profiler中的情况一样TestActivity仍然有3个实例,也就是我们每打开一次TestActivity都会有一个实例内存泄漏了

上图中被我圈住的Shallow Heap和Retained Heap是代表什么意思呢?

名称 代表什么意思呢
Shallow Heap 对象本身占用内存的大小,不包含其引用的对象。(单位是字节)
Retained Heap 是对象及被其引用的对象的总大小,如果该对象被释放了进而可以释放的大小(单位是字节)
  • 第五步 接下来我们查看下TestActivity都被谁引用而导致泄漏的呢?


    image.png

选中TestActivity,右键可以看到一系列的功能,有List Objects/Show objects by class/...

List Objects和Show objects by class功能差不多
前者是按照实例分组,比如这里TestActivity有3个实例,会罗列出三个实例
后者是按照类来分组,那么久会列出TestActivity这一个类

这里重点应该介绍with outgoing reference和with incoming reference
with outgoing reference看字面意思大概也可以知道是这个对象被谁引用了
with incoming reference 也就是这个对象引用了谁
这里因为TestActivity内存泄漏了,当然我们要看看是谁持有了它导致它内存泄漏的,选outgoing

image.png

选择了outgoing后,根据上图可以看到TestActivity被这么多对象引用了,那么怎么找出元凶呢?
\color{red}{上面我们讲过了,判断一个对象可不可以被GC回收,要看这个对象向上追溯引用能否一直追溯到GCRoot}
那么我们直接选Path To GC Roots,然后看到选择之后会出现有一些选项,排除弱引用, 排除软引用...

image.png

我们知道java中有四种引用类型

引用类型 介绍
强引用(我们直接new出来的对象都是强引用) 强引用有三个特点(一)强引用可以直接访问目标对象。(二)强引用所指向的对象在任何时候都不会被系统回收。JVM宁愿抛出OOM异常,也不会回收强引用所指向的对象。(三)强引用可能导致内存泄漏。
软引用(SoftReference) 软引用是除了强引用外,最强的引用类型。可以通过java.lang.ref.SoftReference使用软引用。一个持有软引用的对象,不会被JVM很快回收,JVM会根据当前堆的使用情况来判断何时回收。当堆使用率临近阈值时,才会去回收软引用的对象。因此,软引用可以用于实现对内存敏感的高速缓存。
弱引用(WeakReference) 弱引用是一种比软引用较弱的引用类型。弱引用对象的存在不会阻止它所指向的对象变被垃圾回收器回收。在系统GC时,只要发现弱引用,不管系统堆空间是否足够,都会将对象进行回收。在java中,可以用java.lang.ref.WeakReference实例来保存对一个Java对象的弱引用。
虚引用(PhantomReference) 虚引用是所有类型中最弱的一个。一个持有虚引用的对象,和没有引用几乎是一样的,随时可能被垃圾回收器回收。当试图通过虚引用的get()方法取得强引用时,总是会失败。并且,虚引用必须和引用队列一起使用,它的作用在于跟踪垃圾回收过程。

根据强度排个序
强引用>软引用>弱引用>虚引用

我们过滤掉软引用,弱引用和虚引用再看看
选择了exclude all phatom/weak/soft etc. reference之后

image.png

可以看到,TestActivity被两个GC Roots引用了,一个是Singleton中的mContext变量因为是静态的,另一个就是mHandler,是非静态内部类
罪魁祸首找到了

android中如何尽量避免内存泄漏呢?

大致可以总结一些情况

  • getSystemService的时候,应避免使用activity的context,而是使用application的context
  • 单例模式中尽量避免使用context,如果一定要使用应使用context.getApplicationContext来代替
  • 如果使用观察者模式时,在onCreate或者onStart中注册了监听,要记得在对应的生命周期中解除监听
  • 非静态内部类、匿名内部类会持有外部类的实例引用,导致泄漏。可以在不需要的时候主动置空或者使用弱引用
  • 在Runnable中做操作时一定要小心,如果Activity或Fragment销毁了,及时停止线程
  • 使用资源时(比如Cursor、File、Bitmap、视频、音频等)及时关闭
  • Glide.with(context)这个context不可以乱用
  • ...欢迎补充

你可能感兴趣的:(android内存泄漏分析与检测)