当前位置: 首页 > 科技观察

Android内存泄漏总结(附内存检测工具)

时间:2023-03-15 14:04:51 科技观察

Java中的内存分配主要分为三部分:静态存储区:在编译时分配,在程序运行过程中始终存在。它主要存储静态数据和常量。栈区:方法执行时,会在栈区内存中创建方法体内部的局部变量,方法结束后内存会自动释放。堆区:通常存放新的对象。由Java垃圾收集器收集。栈和堆的区别栈内存用于存放局部变量和函数参数。先进后出队列,入口和出口一一对应,不分片,运行稳定,效率高。当超出变量的作用域时,变量无效,分配给它的内存空间会被释放,内存空间可以被重新使用。堆内存用于存储对象实例。堆中分配的内存将由Java垃圾收集器自动管理。在堆内存中频繁new/delete会造成大量内存碎片,降低程序效率。对于非静态变量的存储位置,我们可以大致认为:局部变量位于栈中(引用变量指向的对象实体存在于堆中)。成员变量位于堆上。因为它们属于一个类,所以这个类最终会被new成一个对象,并作为一个整体存储在堆上。四种引用介绍GC释放一个对象的基本原理是该对象不再被引用(强引用)。那么什么是强引用呢?强引用(StrongReference)我们平时使用最多的就是强引用,如下:IPhotosiPhotos=newIPhots();JVM宁愿抛出OOM也不愿让GC回收强引用的对象。当不使用强引用时,可以通过obj=null显式地将对象的所有引用设置为null,这样就可以回收对象。至于什么时候回收,要看GC算法,这里就不细说了。软引用(SoftReference)SoftReferencesoftReference=newSoftReference<>(str);如果一个对象只有软引用,当内存空间足够时,垃圾回收器不会回收它;如果内存空间不足,就会回收这些对象的内存。只要垃圾收集器不收集它,就可以使用该对象。软引用通常用于图像缓存,但谷歌现在推荐使用LruCache,因为LRU效率更高。过去,流行的内存缓存实现是SoftReference或WeakReference位图缓存,但不推荐这样做。从Android2.3(API级别9)开始,垃圾收集器更积极地收集软/弱引用,这使得它们相当低效。此外,在Android3.0(API级别11)之前,位图的支持数据存储在本机内存中,不会以可预测的方式释放,这可能会导致应用程序短暂地超出其内存限制并崩溃。原文大致意思是:因为在Android2.3以后,GC会非常频繁,导致释放软引用的频率很高,会降低它的使用效率。而在3.0之前,Bitmap是存放在NativeMemory中的,其释放不受GC控制,所以使用软引用缓存Bitmap可能会导致OOM。弱引用(WeakReference)WeakReferenceweakReference=newWeakReference<>(str);与软引用的区别在于只有弱引用的对象才会有更短的生命周期。因为在GC的时候,一旦发现只有弱引用的对象,不管当前内存空间是否足够,它的内存都会被回收。然而,由于垃圾收集器是一个非常低优先级的线程,只有弱引用的对象不一定会很快找到--。幻影引用(PhantomReference),顾名思义,不过是一个假的。与其他类型的引用不同,虚引用不决定对象的生命周期,不能通过虚引用获取对象实例。幻象引用必须与引用队列(ReferenceQueue)结合使用。当垃圾回收器要回收一个对象时,如果发现它还有一个虚引用,就会把这个虚引用添加到与之关联的引用队列中,然后再回收该对象的内存。程序通过判断引用队列中是否存在该对象的幻象引用,就可以知道该对象是否会被回收。Android垃圾回收机制简介Android系统有一个GenerationalHeapMemory模型,系统会根据内存中不同的内存数据类型执行不同的GC操作。该模型分为三个区域:YoungGeneration1.eden2.SurvivorSpace1.S02.S1OldGenerationPermanentGenerationYoungGeneration大多数新对象都放在eden区。当eden区满了之后,执行MinorGC(轻量级GC),然后将存活的对象转移到Survivor区(有两个S0和S1)。MinorGC也会检查Survivor区的对象,将它们转移到另一个Survivor区,这样总会有一个Survivor区是空的。OldGeneration存放长期存活的对象(多次MinorGC后存活的对象)。当OldGeneration区域满了之后,就会执行MajorGC(大型GC)。在Android2.2之前,执行GC时,应用的线程会被挂起,2.3开始加入了并发垃圾回收机制。PermanentGeneration存放方法区。一般存储:要加载的类的信息静态变量Final常量属性和方法信息60FPS这里简单介绍一下帧率的概念,以便理解为什么大量的GC容易造成卡顿。App开发时,一般追求界面的帧率达到60FPS(每秒60帧),那么这个FPS是什么概念呢?在10-12FPS时,可以感受到动画的效果;24FPS时,可以感受到流畅连贯的动画效果,电影常用的帧率(不追求60FPS是为了节省成本);60FPS,达到最流畅的效果,对于更高的FPS,大脑已经难以察觉差异。Android每16ms发送一次VSYNC信号触发UI的渲染(即每16ms绘制一帧)。如果整个过程保持在16ms以内,那么就可以达到60FPS的流畅画面。如果超过16毫秒,将导致卡顿。那么如果在UI渲染的过程中发生大量的GC,或者GC时间过长,可能会导致绘制过程超过16ms而造成卡顿(FPS掉帧、丢帧等),而我们的大脑对这种情况是非常敏感的framedrop,所以如果内存管理没做好,会给用户带来很不好的体验。先介绍一下内存抖动的概念,本文后面可能会用到。内存抖动会在短时间内产生大量新对象。达到YoungGeneration阈值后,触发GC,导致新创建的对象被回收。这种现象会影响帧率,造成卡顿。内存抖动在Android提供的MemoryMonitor中大致表现如下:weakreferences),随着集合大小的增加,内存占用会不断上升,当Activity等被销毁时,集合中的这些对象无法回收,造成内存泄漏。比如我们喜欢通过静态HashMap来做一些缓存之类的事情。在这种情况下,我们应该小心。推荐使用弱引用访问集合中的对象,在不需要的时候考虑手动释放。单例引起的内存泄漏单例的静态特性导致它们的生命周期与应用程序一样长。有时候在创建单例的时候,如果我们需要一个Context对象,传入Application的Context就没有问题,如果传入Activity的Context对象,那么当Activity生命周期结束时,Activity的引用仍然被单例持有,所以不会被回收,而单例的生命周期和应用一样长,所以这就造成了内存泄漏。方案一:在创建单例的构造中,并没有直接使用传入的context,而是通过这个context获取到Application的context。代码如下:publicclassAppManager{privatestaticAppManagerinstance;privateContextcontext;privateAppManager(Contextcontext){this.context=context.getApplicationContext();//使用Application的context}publicstaticAppManagergetInstance(Contextcontext){if(instance!=null){instance=newAppManager(context);}returninstance;}}方案二:构造单例时不需要传入context,直接在我们的Application中写一个静态方法,方法中通过getApplicationContext返回context,然后直接调用这个静态方法在单例方法中获取上下文。非静态内部类引起的内存泄漏在Java中,非静态内部类(包括匿名内部类,如Handler、Runnable匿名内部类最容易引起内存泄漏)会持有对外部类对象(如Activity),而静态内部类不引用外部类对象。因为非静态内部类或匿名类持有外部类的引用,可以访问外部类的资源属性成员变量等;静态内部类不能。因为普通内部类或匿名类依赖外部类,所以必须先创建外部类,再创建普通内部类或匿名类;静态内部类可以随时在其他外部类中创建。Handler内存泄漏可以关注我的另一篇专门针对Handler内存泄漏的文章:链接WebViewleaksAndroid中的WebView存在很大的兼容性问题,有些WebView甚至会出现内存泄漏。所以通常解决这个问题的方法是为WebView另起一个进程,通过AIDL与主进程通信,WebView所在的进程可以根据业务需要在适当的时候销毁,从而达到完全释放的记忆。AlertDialognewAlertDialog.Builder(this).setPositiveButton("Baguette",newDialogInterface.OnClickListener(){@OverridepublicvoidonClick(DialogInterfacedialog,intwhich){MainActivity.this.makeBread();}}).show();DialogInterface引起的内存泄漏.OnClickListener的匿名实现类持有对MainActivity的引用;在AlertDialog的实现中,OnClickListener类会被包裹在一个Message对象中(详见AlertController类的setButton方法),Message会被复制到里面。(在AlertController类的mButtonHandler中可以看到),这两个Message只有一个会被回收,另一个(OnClickListener的成员变量引用的Message对象)会被泄露!解决方法:Android5.0及以上版本不存在该问题;Message对象Leakage无法避免,但如果它只是一个空的Message对象,它会被放入对象池中以备后用。没有问题;让DialogInterface.OnClickListener对象不持有对外部类的强引用,例如用静态类实现;Activity退出前dismissdialogDrawable导致的内存泄露Android在4.0之后已经解决了这个问题。你可以跳过这里。当我们的屏幕旋转时,默认会销毁当前的Activity,然后会创建一个新的Activity,并保持之前的状态。在这个过程中,Android系统会重新加载程序的UI视图和资源。假设我们有一个使用大Bitmap图像的程序。我们不想在每次屏幕旋转时都重新加载Bitmap对象。最简单的方法是在Bitmap对象上使用静态装饰。privatestaticDrawablesBackground;@OverrideprotectedvoidonCreate(Bundlestate){super.onCreate(state);TextViewlabel=newTextView(this);label.setText("Leaksarebad");if(sBackground==null){sBackground=getDrawable(R.drawable.large_bitmap);}label.setBackgroundDrawable(sBackground);setContentView(label);}但是上面的方法在屏幕旋转时可能会造成内存泄漏,因为当一个Drawable绑定到View上时,View对象实际上会成为Drawable的A回调成员多变的。在上面的示例中,静态sBackground持有对TextView对象的引用,而TextView持有对Activity的引用。当屏幕旋转时,Activity无法销毁,这就产生了内存泄漏问题。这个问题主要出现在4.0之前,因为2.3.7及以下版本对Drawable的setCallback方法的实现是直接赋值,但是从4.0.1开始,setCallback使用弱引用来处理这个问题,避免了内存泄漏。未关闭资源导致的内存泄漏BroadcastReceiver、ContentObserver等未注销Cursor、Stream等未关闭***Loop动画在Activity退出前未停止其他一些释放未释放,recycles未回收……等综上所述,不难发现大部分问题都是静电引起的!使用static时要小心,注意static变量持有的引用。必要时使用弱引用来持有一些引用。使用非静态内部类时要注意,毕竟它们持有对外部类的引用。(使用RxJava的同学在订阅的时候也要注意unSubscribe)生命周期结束时注意释放资源。使用属性动画时,请在不用时停止(尤其是循环动画),否则会出现内存泄漏(Activity无法释放)(View动画不会)几个内存检测工具介绍MemoryMonitorAllocationTrackerHeapViewerLeakCanaryMemoryMonitor位于在AndroidMonitor中,该工具可以:方便的显示内存使用情况和GC状态快速定位GC是否与快速定位Crash有关与快速定位内存占用高(内存占用一直在增长)的潜在内存泄漏有关,但不能准确定位问题AllocationTracker。这个工具的用途:可以定位代码中分配的对象类型、大小、时间、线程、栈等信息可以定位内存抖动问题,配合HeapViewer定位内存泄漏问题(可以找outwheretheleakedobjectwascreated,etc.)如何使用:MemoryMonitor中有一个StartAllocationTracking按钮可以开始跟踪。点击停止跟踪后,会显示统计结果。HeapViewer此工具用于:显示内存快照信息收集每次GC后的信息并查找内存泄漏。使用方法:内存监视器中有一个DumpJavaHeap按钮,点击它,在统计报告的左上角选择包分类。通过MemoryMonitor的initiateGC(executeGC)按钮,可以检测内存泄漏等情况。LeakCanary重要的事情说了三遍:for(inti=0;i<3;i++){Log.e(TAG,"检测内存泄漏的神器!");}LeakCanary具体就不细说了使用,自己谷歌。