android 内存泄漏

准备知识

我们需要先了解下jvm虚拟机的一定知识,这样对我们后面对android内存泄露或者优化才会有更好的理解

简单说下,jvm把 class文件 通过 jvm的 类加载器classloader
给放入到 jvm 的 运行时数据区(也就是内存),然后放到内存之后呢,在通过 jvm 的 执行引擎 ,对这个class 文件进行 解释执行 调用 操作系统给的接口 让操作系统去运行。
内存又分为几块区域,对这个class 文件各种信息进行区分,不同信息存放在内存不同的区域。

在这里插入图片描述

又对这些区域划分为
线程私有的 和线程共享的

虚拟机栈 本地方法栈,程序计数器是 线程私有的
方法区 和堆 事线程共享的

什么是线程私有的,也就是说 ,如果你有5个线程,那么就每个线程 就会有 虚拟机栈 本地方法栈,程序计数器 3个区域
也就是每个线程 各自都有各自 的 虚拟机栈 本地方法栈,程序计数器。
而线程共享的就是 5个线程共同 分享 一个 方法区 一个堆区。

下面我们先对 线程私有的 3区域进行简单的解释

程序计数器
记录存储着 当前线程下,当前跑到哪一行代码的地址
你就简单理解为 当前线程下,跑到了哪个一行代码。对这个位置 记录了地址。为什么需要记录呢,因为 多线程执行时间片轮转,记录好当前线程跑到了哪里,下次轮到这个线程跑才知道从哪里跑起。

虚拟机栈(也称java栈)
存储当前线程的 一个个 栈帧,栈帧其实就是方法
栈帧 的内容包含 局部变量表,操作数栈,动态连接,完成出口
栈帧入栈其实就是方法被调用,栈帧出栈其实就是方法执行完了
栈的特点 就是 先进后出
那么这些 栈帧 入 虚拟机栈 跟 出 虚拟机栈 就会符合 先进后出
出栈后栈帧的数据就会被回收清除

本地方法栈
本地方法栈 保存的方法 入栈出栈的 栈帧 是 native 方法

线程私有区域讲完我们再讲线程共享的区域

方法区

这个方法区保存的是类的信息,常量,静态变量等信息,

堆区
几乎所有的对象都存放在堆区。虚拟机为创建这个对象分配空间采用的2种技术,
一个叫做 指针碰撞,一个叫做空闲列表
采用哪种是根据 这个堆里面的数据是否规整 ,而堆里的数据是否整齐又取决于gc回收采用的什么算法。

jvm是怎么判断对象是否存活

我们知道 jvm 会进行垃圾回收 ,那么它是怎么判断 对象 是否是垃圾的 这里有两种方式:

第一种是引用计数法。
也就是 jvm 有个引用计数器,当一个对象被引用 ,引用计数器+1,当回收的时候,判断你这个对象的引用计数器是否为0,如果是就会被回收,
这种方法有个问题就是对象相互引用:
比如:
一个 A 对象引用对象 B,而B对象也引用A对象,但是他们两个是被孤立的。也就是两个对象都没有任何事需要去操作,那么A,B 对象的引用计数器都不为1,不会被回收,但是其实是应该被回收的

第二种就是可达性分析,也叫做根搜索算法

可达性分析是 会把一个对象 的当做 gc roots ,然后以这个gcroots对象作为起始点,像下搜索,如果目标对象不是gcroots连着的,就判断 它是垃圾

可作为GC Roots的对象包括下面几种:
虚拟机栈(栈帧中的局部变量表)中引用的对象。
方法区中常量池引用的对象。
方法区中类静态属性引用的对象。
运行中的线程
由引导类加载器加载的对象
GC控制的对象

我们以一个例子来讲解上面的两个方式:

在这里插入图片描述
那么当引用AB设置null的时候,如果依据引用计数法,
对象AB的引用计数器还是为1,那么就判断它不是垃圾不被回收。这就是引用计数法的问题。

而采用可达性算法就是,
在这个栈帧方法中
引用A B 是局部变量,可达性算法把 引用A B 这两个局部变量 当中 对象 AB各自的 GCRoots
当把 引用A B 设置null 的时候 ,根据可达性分析算法认为 GCRoots都没法到达 对象 A B,所以就判断是垃圾

引用的分类

四种引用 强引用、软引用、弱引用和虚引用

引用从高到低分为:

强引用 = 简单说就是gc回收绝对不会被回收,宁可抛出oom。
只有我们把对象引用 设置null,根据可达性算法,让gc判断他是垃圾所以回收

软引用 SoftReference

软引用就是 ,内存够就不会被gc回收,内存不够才会,
所以这点就可以利用它来做缓存,你想如果有的数据,你不希望 经常去加载获取,你可以做成软引用,然后加上判断

  // 如果内存够数据还在
    if(softReference.get() != null) {
        // 内存充足,还没有被回收,直接获取内存中的数据
        data = softReference.get();
    } else {
        // 内存不足,软引用的对象已经回收,那就在根据逻辑重新获取一遍
        data = new data;
        // 重新构建软引用
        softReference = new SoftReference(data);
    }



弱引用 WeakReference
一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

虚引用 PhantomReference
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

gc垃圾回收机制

知道哪些是垃圾后,gc是什么机制怎么回收这些垃圾的呢

新生代
复制算法

打个比方,新生代有100个位置,那么这个 复制算法 会把这个 100个位置 给对等划分为 2部分,一个部分各50个位置,假设 左边的部分 ,里面已经放入了50个对象,这个时候还有个对象要进来 ,那么 就需要进行一次gc 回收 ,经过前面的可达性分析知假设这50个只有一个1对象不是垃圾, 就会把这个对象 复制到右边的区域,然后把刚要进来的也放到右边的区域,最后对左边的区域进行格式化,等着下次 右边gc的回收 右边 对象复制过来的时候用。

这个算法 实现简单,运行高效,内存复制,没有内存碎片,但是利用率只有一半,它把原本的 100的全部位置 给一分为2,也就是只有一半可以用,另外一半 留着复制用的 ,所以后面就 有了分代收集算法
新生代的绝大部分对象生命周期很短,所以适合采用 复制算法

老年代
标记清除算法
假设这块区域都满了,那么经过标记清除算法后,就会出现内存碎片

内存碎片就是说它这快区域它并不是连续的,零零散散的,像上面的图,如果我一个对象需要占据 5个空格,而我们说对象的物理地址是连续的,那么上面的区域就放不下,这里就很浪费了,这种内存碎片就是提前出发再一次gc。

标记整理算法
这个算法就是上面的标记清除算法多做了一步整理,也就是清除完数据后,在进行一次整理,也就是对对象进行移动整理

这里算法特点就是,,没有内存碎片,对象移动,那么对象移动就会造成 引用的更新,也就是性能速度会慢一点。
因为在老年代对象存活率高,有很多存活的对象,如果用到复制算法,那么对象复制肯定不合适, 所以老年代适合 标记整理算法

分代收集算法
前面的算法有些不完善的地方缺点,分代收集算法会结合不同的收集算法来处理不同的空间, Java堆区的空间划分在 Java虚拟机中,各种对象 的生命周期会有着较大的差别,大部分对象生命周期很短暂,少部分对象生命周期很长, 有的甚至与应用程序以及 Java虚拟机的运行周期一样长。因此,应该对不同生命周期的对 象采取不同的收集策略,根据生命周期长短将它们分别放到不同的区域,并在不同的区域 采用不同的收集算t去,这就是分代的概念。现在主流的 Java虚拟机的垃圾收集器都采用分代收集算法(也就是把 堆分为 新生代 其中包括 Eden ,From To,区域,以及老年代。。
Eden ,From To,区域 比例是 8:1:1.

新生代采用的 利用 垃圾回收器的 young gc 去回收

老年代用的是 标记清除算法或者 标记整理算法 ,是利用 垃圾回收器的 old gc 去回收

而 垃圾回收器的 Full gc 能回收 堆里面所以的区域,以及 方法区的数据

前面说了 在新生代的时候 运用的是复制算法,但是它的利用率太低了,而在这里的分代收集算法根据区域再次划分,
就对新生代复制算法进行了改进。

Eden 区 From 区 To区,他们的比例就是 8:1:1。

这里的 From 区 或者 To区就是用来着预留的给复制用的。
打个比方,Eden区80个都以及满了, 那么经过可达信分析只有一个对象是活的,那么就把这个对象 就放在from区并且这个对象年龄+1,而To区的10个位置就是留着用来预留的复制的,也就是后面Eden 区 80个再次满的时候,经过可达信分析80个只有一个对象是活的,那么就把这个对象给放在 to 区,并且这个对象的年龄是1. 并且刚刚那个在from 区的对象也会被复制放到to区,并且他的年龄是2.这回就是轮到from区是留着给复制放的了。
这个时候你就会发现,从原来的百分之50的空间利用变成了百分之90.剩下 10的,From 区 或者 To区就是用来着预留的给复制用的。

什么情况会进入到老年代呢,
一种是,当 Eden区和From 区域,或者Eden区和To 区域 的 对象年龄超过一定阈值,就会进入到 老年代,
另外一种是 当From 区 或者 To区就是用来着预留的给复制用的区域不够放的时候,就会根放入到老年代的区域。

而老年代用就是还是常用的 标记整理和 标记清除算法,正式因为这两个算法的特点,所以在 为对象 在堆内存分配的时候,才会有指针碰撞和空闲列表

进入正题什么是内存泄露

对象都会占用内存,gc会进行垃圾回收内存,但是 如果一个对象它 应当被 gc回收释放内存,也就是 我不需要它的,想要它被gc回收释放内存,但是它确被人引用,也就是有指针指向它的时候,这个时候,gc没办法回收,这个时候就叫内存泄漏,如果一直内存泄露,内存就没有的剩余多少空间可以利用,就会慢慢的造成卡顿,高温发热等现象,如果内存占满了就会
导致内存溢出(oom)

在 Android 中 ,最明显的就是 ,avtivity , 我们知道 activity是一个占用大量内存的对象,activity的内存泄漏其实就是,activity在finish的时候,也就是我们希望它finish的时候,它本来应该被gc回收释放内存,但是activity内的某些引用,指向了activity这个对象,关键是这些引用的生命周期比activity还长,导致了activity该被回收的时候不能被gc回收,所以就造成了内存泄露

内存泄露的各种场景和解决方案

static静态变量引起的内存泄露

我们知道静态变量是在编译期就存放在内存的方法区中,这个好处就是它的加载速度很快,定义static的变量跟不定义的static的变量加载速度肯定是不一样
但是他定义的变量的是常驻在应用中的,不会被gc垃圾回收的,所以就不能随便用
静态变量的生命周期是很长很长的,跟应用的jvm的生命周期是一样的,也就是跟Application存在的时间是一样的。所以在我们使用静态变量的时候,一定要思考清楚,能不用静态变量就不要用静态变量。如果要用,需要思考这个静态变量会占用使用过程中多少内存,是否有替代方案。

在这里插入图片描述

资源对象泄露

对于list 集合,要记得先clear,然后在对引用置空
list.clear方法
在这里插入图片描述

Vector v = new Vector(10);
for (int i = 1; i < 100; i++) {
    Object o = new Object();
    v.add(o);
    o = null;   
}

就算上面o=null,对象Object没有被o指向了,其实本来就会被gc回收了,但是对象Object还被Vector 指向,所以还是会内存泄漏,所以解决办法就是 v也要置空 v=null;

资源性对象比如(Cursor,InputStream/OutputStream,File文件等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。它们的缓冲不仅存在于 java虚拟机内,还存在于java虚拟机外。如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄漏。

查询数据库时,一定要在finally里面调用cursor.close()保证cursor关闭掉;finally语句之前一定要用catch,要不然不能保证走到finally里面;

bimap对象记得调用bimap.recycle()先回收然后在对引用置空

注册对象未解注册

类似广播跟Eventbus动态注册再不需要的时候时机记得解注册

webview内存泄露
webview都存在内存泄露问题,应用只要使用过一次webview,内存就不会被释放掉,解决办法就是把webview给设置一个单独的独立进程,然后通过AIDL跟应用进程进行业务通信,然后再不需要的时机销毁webview进程

Context引起的内存泄漏
Activity的Context,fragment也可以拿到Context,Application也Context,第三方的架构经常传一个Context进去,这个Context意味着我就是把这一个fragment或者application它的所有的上下文资源全部传给了这些第三方的库,如果这些第三方的库如果不能自动的去释放这些库,就产生了内存泄漏。所以当我们在第三方里面需要传一个上下文Context时,要注意,这一个Context的生命周期什么时候用,什么时候释放,只有掌握了这些,在第三方sdk里面Context应该怎么样使用的时候,我们才能传一个正确的生命周期所对应的Context,所以我们使用的时候一定是取短不取长,这是原则,哪怕有可能带来空指针异常的问题,这个问题是第三方sdk不严谨导致的,暴露了第三方sdk的问题,当然我们常用的glide已经规避了这些问题。

单例类造成的内存泄露

我们知道单例类是应用进程唯一的实例,并且他的生命周期也是跟应用共存亡,并且不会被gc回收,那么如果这个单例类需要个context的话,尽量传APPlication,而不是传activtiy,因为activtiy生命周期短,如果传activity,这个activity就不会被回收了,就会内存泄露。
如果说一定非要传activity,那么请对这个activity用弱引用包装,而不是直接强引用

动画内存泄漏:
活动中动画一直播放还没结束,而活动fiinish了,因为动画持有view的引用,而view又持有Activity的引用最终Activity无法释放,出现内存泄漏
解决问题就是在活动ondestory中:animator.cancel()来停止动画

内部类
首先先了解下内部类

public class MainActivity2 extends AppCompatActivity {

    public String name = "zjs";

    static Activity activity;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        activity = this;

        int money =  new A().money;

    }


    class A {

        public int money;
        public void  test(){
            name = "123";

        }
    }

}

非静态内部类可以直接访问外部类的成员,而外部类不可以直接访问非静态内部类的成员
也就是这里的A类可以直接获取外部的name字段,但是
MainActivity2不可以直接获取A的money。需要先创建A对象

原因:

非静态内部类没有被static修饰,所以这个内部类就不是类相关的,也就说不是类的,是实例的,

但是我们非静态内部类要创建实例,外部类一定会先创建一个外部类的实例,非静态内部类的实例就是寄生在外部类的实例上的。所以,非静态内部类的实例可以直接访问外部类的成员,因为,外部类已经创建一个实例的,内部类保留了外部类创建的实例的引用,也就是内部类持有对外部类的对象的引用,所以可以直接获取对象的字段

静态内部类不一样!

静态内部类是被static修饰的,所以是类的一员。根据静态成员不能访问非静态成员的原则,静态内部类是不能访问外部类的非静态成员的

注意:非静态内存类造成的内存泄露并不是因为用了非静态内部类,非静态内部类持有对外部类的对象的引用,单纯使用一个非静态内部类是没有问题,造成内存泄露的原因是因为,非静态内部类中的使用的对象的生命周期大于外部的对象而引起的

而解决办法就是非静态变为静态,但是变为静态之后,你想要对这个静态类里面的对象数据格外敏感,避免使用大对象,如果使用了大对象需要用弱引用去包装引用,而不是直接就强引用。这里以handle 为实例

public class MainActivity extends AppCompatActivity {

    MyHandler myHandler;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        myHandler= new MyHandler(this);
        myHandler.sendEmptyMessage(123);

    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        myHandler.removeCallbacksAndMessages(null);
    }

    private static class MyHandler extends Handler {
        private WeakReference<Activity> weakReference;

        public MyHandler(Activity activity) {
            weakReference = new WeakReference<>(activity);

        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            Log.e("zjs", "handleMessage: ");
            Activity activity = weakReference.get();
            switch (msg.what) {
                case 0:
//                    if (activity != null) {
//                        activity.tv.setText("我是更改后的文字");
//                    }
//                    activity.tv.setText("我是更改后的文字");
                    break;
            }
        }
    }
}
  • 以静态生命这个内部类
  • 对传入的activity用弱引用
  • 如果业务结束的时候记得 myHandler.removeCallbacksAndMessages(null);

匿名类

类似的,匿名类同样会持有定义它们的对象的引用,跟非静态内部类一样,如果使用不当也会造成内存泄露。
比如说使用 AsyncTask 或者使用线程Timer Tasks 的时候,其实大致也都是因为,他们内部的生命周期太长,活一直没干完,activity一直被引用没法被回收

比如如果在 activity 内定义了一个匿名的 AsyncTask 对象,然后在里面不断whitle

void startAsyncTask() {
    new AsyncTask<Void, Void, Void>() {
            @Override protected Void doInBackground(Void... params) {
            while(true);
        }
    }.execute();
}

super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
View aicButton = findViewById(R.id.at_button);
aicButton.setOnClickListener(new View.OnClickListener() {
        @Override public void onClick(View v) {
            startAsyncTask();
        nextActivity();
    }
});

上面的AsyncTask一直开启循环不停,所以肯定会内存泄漏。

内存分析工具

有 LeakCanary Profiler 和 MAT
一般情况下可以通过 LeakCanary来看看有没有泄露, Profiler定位哪里泄露

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Android内存泄露是指应用程序在使用完对象后,没有及时将其释放,导致这些对象无法被垃圾回收器回收,最终导致应用程序的内存占用不断增加,直至崩溃。以下是一些可能导致Android内存泄露的情况及解决方法: 1. 静态变量:如果在应用程序中使用了静态变量,并且这些变量引用Activity或者Fragment等容器类,就可能导致内存泄露。解决方法是在Activity或者Fragment销毁时,将相关的静态变量设为null。 2. 匿名内部类:如果在应用程序中使用了匿名内部类,并且这些内部类引用Activity或者Fragment等容器类,就可能导致内存泄露。解决方法是在Activity或者Fragment销毁时,将相关的匿名内部类引用设为null。 3. Handler:如果在应用程序中使用了Handler,并且这些Handler引用Activity或者Fragment等容器类,就可能导致内存泄露。解决方法是在Activity或者Fragment销毁时,将相关的Handler引用设为null。 4. Bitmap对象:如果在应用程序中使用了Bitmap对象,并且没有及时释放,就可能导致内存泄露。解决方法是在使用完Bitmap对象后,调用recycle()方法释放内存。 5. 资源对象:如果在应用程序中使用了资源对象,并且没有及时释放,就可能导致内存泄露。解决方法是在使用完资源对象后,调用其对应的释放方法,比如close()。 希望以上这些解决方法可以帮助您避免Android内存泄露的问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值