Android内存泄漏思考 - 编程学习网

标签: android 内存泄漏 思考 | 发表时间:2017-08-20 16:19 | 作者:
出处:http://www.phpxs.com

Android内存泄漏是一个经常要遇到的问题,程序在内存泄漏的时候很容易导致OOM的发生。那么如何查找内存泄漏和避免内存泄漏就是需要知晓的一个问题,首先我们需要知道一些基础知识。

Java的四种引用

强引用: 强引用是Java中最普通的引用,随意创建一个对象然后在其他的地方引用一下,就是强引用,强引用的对象Java宁愿OOM也不会回收他

软引用: 软引用是比强引用弱的引用,在Java gc的时候,如果软引用所引用的对象被回收,首次gc失败的话会继而回收软引用的对象,软引用适合做缓存处理 可以和引用队列(ReferenceQueue)一起使用,当对象被回收之后保存他的软引用会放入引用队列

弱引用: 弱引用是比软引用更加弱的引用,当Java执行gc的时候,如果弱引用所引用的对象被回收,无论他有没有用都会回收掉弱引用的对象,不过gc是一个比较低优先级的线程,不会那么及时的回收掉你的对象。 可以和引用队列一起使用,当对象被回收之后保存他的弱引用会放入引用队列

虚引用: 虚引用和没有引用是一样的,他必须和引用队列一起使用,当Java回收一个对象的时候,如果发现他有虚引用,会在回收对象之前将他的虚引用加入到与之关联的引用队列中。可以通过这个特性在一个对象被回收之前采取措施

下面是一个例子:

public class Main {

    public static void main(String[] args) throws InterruptedException {
        ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
        String sw = "虚引用";
        switch (sw) {
            case "软引用":
                Object objSoft = new Object();
                SoftReference<Object> softReference = new SoftReference<>(objSoft, referenceQueue);
                System.out.println("GC前获取:" + softReference.get());
                objSoft = null;
                System.gc();
                Thread.sleep(1000);
                System.out.println("GC后获取:" + softReference.get());
                System.out.println("队列中的结果:" + referenceQueue.poll());
                break;
                /*
                 * GC前获取:[email protected]* GC后获取:[email protected]* 队列中的结果:null
                 * */
            case "弱引用":
                Object objWeak = new Object();
                WeakReference<Object> weakReference = new WeakReference<>(objWeak, referenceQueue);
                System.out.println("GC前获取:" + weakReference.get());
                objWeak = null;
                System.gc();
                Thread.sleep(1000);
                System.out.println("GC后获取:" + weakReference.get());
                System.out.println("队列中的结果:" + referenceQueue.poll());
                /*
                * GC前获取:[email protected]* GC后获取:null
                * 队列中的结果:[email protected]* */
                break;
            case "虚引用":
                Object objPhan = new Object();
                PhantomReference<Object> phantomReference = new PhantomReference<>(objPhan, referenceQueue);
                System.out.println("GC前获取:" + phantomReference.get());
                objPhan = null;
                System.gc();
                //此处的区别是当objPhan的内存被gc回收之前虚引用就会被加入到ReferenceQueue队列中,其他的引用都为当引用被gc掉时候,引用会加入到ReferenceQueue中
                Thread.sleep(1000);
                System.out.println("GC后获取:" + phantomReference.get());
                System.out.println("队列中的结果:" + referenceQueue.poll());
                /*
                * GC前获取:[email protected]* GC后获取:null
                * 队列中的结果:[email protected]* */
                break;
        }
    }

}

Java GC

目前oracle jdk和open jdk的虚拟机都为Hotspot,android 为Dalvik和Art

曾经的GC算法:引用计数

简短的说引用计数就是对每一个对象的引用计算数字,如果引用就+1,不引用就-1,回收掉引用计数为0的对象。来达到垃圾回收

弊端:如果两个对象都应该被回收但是他俩却互相依赖,那么他两者的引用永远都不会为0,那么就永远无法回收,无法解决循环引用的问题

这个算法只在很少数的虚拟机中使用过

现代的GC算法

  • 标记回收算法(Mark and Sweep GC):从"GC Roots"集合开始,将内存整个遍历一次,保留所有可以被GC Roots直接或间接引用到的对象,而剩下的对象都当作垃圾对待并回收,这个算法需要中断进程内其它组件的执行并且可能产生内存碎片。
  • 复制算法(Copying):将现有的内存空间分为两快,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
  • 标记-压缩算法(Mark-Compact):先需要从根节点开始对所有可达对象做一次标记,但之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。
  • 分代:将所有的新建对象都放入称为年轻代的内存区域,年轻代的特点是对象会很快回收,因此,在年轻代就选择效率较高的复制算法。当一个对象经过几次回收后依然存活,对象就会被放入称为老生代的内存空间。对于新生代适用于复制算法,而对于老年代则采取标记-压缩算法。

以上四种算法信息引用自QQ空间团队分享Android GC 那点事&version=11000003&pass_ticket=nhSGhYD4LC9FWvUPv26Y7AdIzqEDu8FTImf2AKlyrCk%3D) ,总结的特别棒

导致内存泄漏的原因

对象在GC Root中可达,也就是他的引用不为空,所以GC无法回收它也就会导致内存泄漏

GC Root起点

  • 虚拟机栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • JNI引用的对象

GC可以续一秒

当一个对象在引用链中失S#x53BB;了引用,那么他就真的要告别世界了吗,其实并不是,虚拟机会给他“缓刑”,每一个对象有一个finalize() 方法,虚拟机是否给他缓刑取决于这个对象的这个方法是否被执行,如果这个对象的这个方法没有被覆盖或者这个方法被执行过一次,那么就要“行刑”了。真的是“续一秒”

如果这个对象的finalize()方法应该被执行,那么虚拟机会将它放在F-Queue队列中,稍后虚拟机会自动创建一个Finalizer线程去执行这个队列中的对象的这个方法。如果对象在finalize()中成功自救,举个例子,把自己和一个存在的对象强引用,那么就不会被回收,否则就真的被回收了。

但是虚拟机并不会保证Finalizer线程执行结束再进行回收,因为如果在某一个对象的finalize()方法中执行了死循环或者超级耗时的操作,虚拟机等待这个执行结束的话就会导致整个Gc崩溃了

首先注意这个方法只能被执行一次,第二次就会标记了这个方法被执行过不会再执行了,其次,这个方法不一定会被执行到,所以不要依赖finalize()去自救。这不是好的做法。

并发GC和非并发GC

Android2.3之后支持了并发的GC。

  • 非并发GC: 虚拟机在执行GC的时候进行Stop the world,也就是挂起其他所有的线程,通常会持续上百毫秒,一次Mark,然后直接清理

  • 并发GC: 跟非并发的简单gc来比较,一般非并发GC需要耗费上百ms的时间来进行,而并发gc仅仅需要10ms左右的时间,效率大幅度提升(数据来自:技术小黑屋大大),但是并发gc由于需要进行重复的处理改动的对象,所以需要更多的CPU资源

两者的差别:

首先非并发GC简单粗暴,直接挂起所有的线程,此时Java堆中肯定不会有任何的添加和修改,此时去递归GC树,然后标记-清理。但是这样会造成很大的开销,大家都等着你岂不是很没面子= =

然而非并发的GC是一点一点来的,跟线程同步进行这样就不会有很长时间的等待,但是你要明白一个道理,想把地扫干净这段时间必须没人来踩,所以他要有挂起线程的过程。

那么并发是怎么实现的呢?首先有个知识点就是Jvm在分配内存的时候,有两种方式

  • 指针碰撞:一个指针,申请一块内存就指针挪动相应的距离,不会产生内存碎片,这要求内存是很规整的
  • 空闲列表:每次申请一块内存给需要的对象,然后有一个列表记录了哪些位置被申请了,下次申请的时候就不申请这个位置,这样适用于内存不是很规整的情况

创建对象是一个频繁的操作,那么我们如何保证原子性呢?两种方案

  • CAS(Compare and Swap)策略配上失败重试来保证原子性
  • 每个线程分配一个TLAB: 很简单,每个线程自己有自己的一块内存,那么分配的时候自己锁自己的分区就行了,提高了效率

我们用的是第二种 233

所以获取Java堆锁的时候,重点来了,我们逐个线程去锁TLAB,而不是一次全锁住,当然提高了并发GC的效率,所以更快。但是引来的问题就是并发的问题,所以下一步要重复去修改在一个个探索时候被改的对象。也就需要更多的CPU资源。

我们为什么要关注GC

首先我们知道虚拟机如何去GC才能了解到如何让一个对象被正确的回收,这样才不能内存泄漏

其次无论是并发GC还是非并发GC都会导致挂起其他的所有线程,那么就会带来程序卡顿。

ART在GC上做到了更加细粒度的控制,可以更加流畅的GC

常见的内存泄漏案例:Handler内存泄漏

首先铺垫一句话:非静态的内部类和匿名类会隐式的持有外部类的引用

public class MainActivity extends AppCompatActivity {

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            Log.d("smallSohoSolo", "Hello Handler");
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                Log.d("smallSohoSolo", "Running");
            }
        }, 1000 * 60 * 10); //10分钟之后执行
        finish();
    }
}

这段代码有很明显的内存泄漏,首先Handler和Runnable都是匿名内部类的实例,他们都会持有MainActivity的引用,

  1. Handler发送的消息到了消息队列中
  2. Activity被结束掉
  3. 这个消息中包含了Handler的引用,Handler包含了Activity的引用,而且他还是个Runnable,也是匿名内部类,也间接包含了MainActivity引用
  4. 在Main Lopper中,当此消息被取出来,这未执行的10分钟里面,MainActivity没法回收
  5. 内存泄漏

有人可能会说短暂的内存泄漏又能怎样?这是错误的想法,因为只要发生内存泄漏,在这段时间只要进行了大内存的操作(比如加载一个照片墙),就有风险因为这个内存泄漏造成OOM(占用内存肯定剩下的少了)

上面这个如何修改呢?

将Runnable和Handler改成static 或者在外部定义内部使用。

其他常见的内存泄漏

  • 静态变量内存泄漏:使用静态变量来引用一个事物,在不使用之后没有下掉,那么引用存在就会一直泄漏
  • 单例导致的内存泄漏:使用的单例中保存了不应该被一直持有的对象,那么就会造成内存泄漏
  • 由第三方库使用不当导致的内存泄漏:比如EventBus,Activity销毁的时候没有反注册就会导致引用一直被持有无法回收
  • 还有很多。。。他们都是因为引用没有被清理造成的

如何查看内存泄漏

简单粗暴 —> LeakCanary: Square出品的库,当出现内存泄漏的时候会出现

精打细算 —> Android Studio 内存工具: 可以Dump下来当前的内存路径,然后分析出来哪些对象目前的状态。很强

参考文献

 

来自:https://techblog.toutiao.com/2017/08/16/untitled-4/

 

相关 [android 内存泄漏 思考] 推荐:

Android内存泄漏思考 - 编程学习网

- -
Android内存泄漏是一个经常要遇到的问题,程序在内存泄漏的时候很容易导致OOM的发生. 那么如何查找内存泄漏和避免内存泄漏就是需要知晓的一个问题,首先我们需要知道一些基础知识. 强引用: 强引用是Java中最普通的引用,随意创建一个对象然后在其他的地方引用一下,就是强引用,强引用的对象Java宁愿OOM也不会回收他.

Android 解析内存泄漏

- - CSDN博客移动开发推荐文章
1、引用没释放造成的内存泄露.        1.1、注册没取消造成的内存泄露.        这种 Android的内存泄露比纯 Java的内存泄露还要严重,因为其他一些Android程序可能引用我们的Anroid程序的对象(比如注册机制). 即使我们的Android程序已经结束了,但是别的引用程序仍然还有对我们的Android程序的某个对象的引用,泄露的内存依然不能被垃圾回收.

Android内存泄漏检测-LeakCanary

- - CSDN博客推荐文章
添加LeakCanary依赖包. 在主模块app下的build.gradle下添加如下依赖:. 添加Application子类. 首先创建一个ExampleApplication,该类继承于Application,在该类的onCreate方法中添加如下代码开启LeakCanary监控:. 在配置文件中注册ExampleApplication.

Android性能优化之内存泄漏

- - CSDN博客推荐文章
  内存泄漏(memory leak)是指由于疏忽或错误造成程序未能释放已经不再使用的内存. 那么在Android中,当一个对象持有Activity的引用,如果该对象不能被系统回收,那么当这个Activity不再使用时,这个Activity也不会被系统回收,那这么以来便出现了内存泄漏的情况. 在应用中内出现一次两次的内存泄漏获取不会出现什么影响,但是在应用长时间使用以后,若是存在大量的Activity无法被GC回收的话,最终会导致OOM的出现.

Android 轻松解决内存泄漏

- - IT瘾-jianshu
方法区(non-heap):编译时就分配好,在程序整个运行期间都存在. 栈区:当方法执行时,会在栈区内存中创建方法体内部的局部变量,方法结束后自动释放内存;. 堆区(heap):通常用来存放 new 出来的对象. Java四种不同的引用类型. 强引用(Strong Reference):JVM 宁愿抛出 OOM,也不会让 GC 回收存在强引用的对象.

Android性能优化之常见的内存泄漏

- - CSDN博客推荐文章
最近腾讯bugly也推出了三篇关于Android内存泄漏调优的文章,有兴趣的可以看看:. 1、 内存泄露从入门到精通三部曲之基础知识篇. 2、 内存泄露从入门到精通三部曲之排查方法篇. 3、 内存泄露从入门到精通三部曲之常见原因与用户实践. 当一个对象已经不需要再使用了,本该被回收时,而有另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏.

Android内存泄漏产生的6大原因

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

Android 性能篇 -- 带你领略Android内存泄漏的前世今生

- - SegmentFault 最新的文章
内存泄漏是当程序不再使用到的内存时,释放内存失败而产生了无用的内存消耗. 内存泄漏并不是指物理上的内存消失,这里的内存泄漏是指由程序分配的内存但是由于程序逻辑错误而导致程序失去了对该内存的控制,使得内存浪费. Java 程序运行时的内存分配策略有三种,分别是. 静态分配 、 栈式分配 和. 堆式分配 ,对应的三种存储策略使用的内存空间主要分别是.

内存泄漏

- - CSDN博客系统运维推荐文章
程序申请了堆空间,但是“忘记”释放,导致该块区域在程序结束前无法被再次使用导致的. 泄漏时间长了,就会导致用户空间内存不足,严重的导致死机. 如果泄漏比较严重,很容易察觉;但是有些泄漏很缓慢,不容易察觉,但是软件会运行很长时间后,会慢慢导致严重问题,而且当发现症状的时候,基本上已经是比较晚的时候了,想要识别泄漏,还是可以实现的,本篇文章来聊聊内存操作的原理.

java内存泄漏

- - 编程语言 - ITeye博客
不论哪种语言的内存分配方式,都需要返回所分配内存的真实地址,也就是返回一个指针到内存块的首地址. Java中对象是采用new或者反射的方法创建的,这些对象的创建都是在堆(Heap)中分配的,所有对象的回收都是由Java虚拟机通过垃圾回收机制完成的. GC为了能够正确释放对象,会监控每个对象的运行状况,对他们的申请、引用、被引用、赋值等状况进行监控,Java会使用有向图的方法进行管理内存,实时监控对象是否可以达到,如果不可到达,则就将其回收,这样也可以消除引用循环的问题.