Android 内存泄漏

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

Java的四种引用

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

软引用: 软引用是比强引用弱的引用,在Java gc的时候,若是软引用所引用的对象被回收,首次gc失败的话会继而回收软引用的对象,软引用适合作缓存处理 能够和引用队列(ReferenceQueue)一块儿使用,当对象被回收以后保存他的软引用会放入引用队列算法

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

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

下面是一个例子:oracle

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前获取:java.lang.Object@61bbe9ba
                 * GC后获取:java.lang.Object@61bbe9ba
                 * 队列中的结果: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前获取:java.lang.Object@61bbe9ba
                * GC后获取:null
                * 队列中的结果:java.lang.ref.WeakReference@610455d6
                * */
                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前获取:java.lang.Object@61bbe9ba
                * GC后获取:null
                * 队列中的结果:java.lang.ref.WeakReference@610455d6
                * */
                break;
        }
    }

}

Java GC

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

曾经的GC算法:引用计数工具

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

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

这个算法只在不多数的虚拟机中使用过

现代的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下来当前的内存路径,而后分析出来哪些对象目前的状态。很强

相关文章
相关标签/搜索