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

Android面试被问到内存泄漏?

时间:2023-03-16 01:35:35 科技观察

内存泄漏是指本应释放的内存没有及时释放,被某个或某些实例持有但不再使用,导致GC无法回收。Java内存分配策略Java程序运行时的内存分配策略分为三种,分别是静态分配、栈分配和堆分配。对应的三种策略使用的内存空间分别是静态存储区(也叫方法区)、栈区、堆区。静态存储区(方法区):主要存放静态数据、全局静态数据和常量。这块内存是在程序编译时分配的,并在整个程序运行时都存在。栈区:方法执行时,方法内部的局部变量都是在栈内存中创建的,分配的内存在方法结束后自动释放。因为栈内存分配在处理器的指令集中,效率很高,但分配的内存容量有限。堆区:又称动态内存分配,通常是指程序运行时直接new的内存。这部分内存在不适用的时候会被Java垃圾回收器回收。栈和堆的区别:方法体中定义的一些基本类型的变量和对象引用变量(局部变量)是在方法的栈内存中分配的。当在方法块中定义变量时,Java会在栈上为其分配内存。当超出变量的作用域时,变量将失效。这时候占用的内存就会被释放,然后重新使用。.堆内存用于存放所有新对象(包括对象中的所有成员变量)和数组。堆上分配的内存由Java垃圾回收管理器自动管理。要在堆中创建一个对象或数组,可以在栈中定义一个特殊的变量。该变量的值等于数组或对象在堆内存中的首地址。这个特殊变量就是我们上面提到的引用变量。我们可以通过引用变量来访问堆内存中的对象或数组。例如:publicclassSample{ints1=0;SamplemSample1=newSample();publicvoidmethod(){ints2=0;SamplemSample2=newSample();}}SamplemSample3=newSample();上面的局部变量s2和mSample2存放在栈内存中,mSample3指向的对象存放在堆内存中,包括对象的成员变量s1和mSample1也存放在堆中,而自身存放在堆内存中堆。结论:局部变量的基本类型和引用存放在栈内存中,引用的实体存放在堆中。——因为它们存在于方法中,所以随着方法的生命周期结束。成员变量全部存储在堆中(包括基本数据类型、引用和被引用的对象实体)。——因为属于类,所以类对象最终会被new使用。了解了Java的内存分配之后,我们再来看看Java是如何管理内存的。Java如何管理内存程序分配内存,GC释放。内存释放的原则是对象或数组不再被引用,JVM会在合适的时候回收内存。内存管理算法:1.引用计数法:引用变量定义在对象内部。当对象被引用变量引用时,计数加1。当对象的引用变量超过生命周期或引用新变量时,计数减1。任何引用计数为0的对象实例可以进行GC。这种算法的优点是引用计数收集器可以执行得非常快,并且穿插在程序的运行中。更有利于程序不需要长时间中断的实时环境。缺点:无法检测到循环引用。引用计数无法解决的循环引用问题如下:publicvoidmethod(){//Samplecount=1Sampleob1=newSample();//Samplecount=2Sampleob2=newSample();//Samplecount=3ob1.mSample=ob2;//样本数=4ob2。mSample=ob1;//Samplecount=3ob1=null;//Samplecount=2ob2=null;//count为2,不能GC}Java可以作为GCROOT的对象有:虚拟机栈中引用的对象(局部变量table),方法区静态属性引用的对象,方法区常量引用的对象,本地方法栈(Nativeobjects)引用的对象2.标记和清除方法:从根节点集合开始扫描,标记存活的对象,然后扫描整个空间以回收未标记的对象。在很多存活对象的情况下,效率很高,但是会造成内存碎片。3、标记和排序算法:同mark和clear方法,只是在回收对象时将存活的对象移走。虽然解决了内存碎片的问题,但是增加了内存的开销。4.复制算法:这种方法是为了克服句柄的开销,解决堆碎片。将堆分成一个对象平面和多个自由平面。将存活的物体复制到自由表面,主自由表面成为物体表面,原物体表面成为自由表面。这会增加内存开销,并且程序会在交换期间暂停执行。5.分代算法:分代垃圾回收策略是基于不同的对象有不同的生命周期。因此,不同生命周期的对象可以采用不同的回收算法来提高回收效率。年轻代:1.所有新生成的对象首先存放在年轻代中。年轻一代的目标是尽快收集那些短命的对象。2、新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个伊甸区,两个幸存者区(一般来说)。大多数对象生成在伊甸园区域。回收时,先将eden区存活的对象复制到一个survivor0区,然后清除eden区。当survivor0区也满了,将eden区和survivor0区的存活对象复制到另一个survivor1区,然后清除eden区和这个区。survivor0区,此时survivor0区为空,然后survivor0区和survivor1区交换,即survivor1区保持为空,以此类推。3、当survivor1区域不足以存放eden和survivor0的存活对象时,直接将存活对象存放到老年代。如果老年代也满了,就会触发FullGC,即新生代和老年代都会被回收。4.新生代发生的GC也称为MinorGC。)老年代:1.在新生代中存活N次垃圾回收的对象将被放入老年代。因此,可以认为老年代存储的所有对象都是长生命周期的对象。2.内存比新生代大很多(大概1:2)。当老年代内存满了,就会触发MajorGC,即FullGC。FullGC的频率比较低,对象在老年代的存活时间比较长,存活率标记高。持久代:用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有太大影响,但有些应用可能会动态生成或调用一些类,如Hibernate等。此时,运行时需要设置一个比较大的永久代空间来存放这些新加入的类。Android常见内存泄漏汇总集合类泄漏先看一段代码ListobjectList=newArrayList<>();for(inti=0;i<10;i++){Objecto=newObject();objectList.add(o);o=null;}在上面的例子中,虽然引用o在循环中被释放了,但是它被添加到objectList中,所以objectList中也持有对象的引用,此时对象不能被GC。因此,如果一个对象被添加到集合中,它也必须从集合中删除。最简单的方法是释放objectListobjectList.clear();对象列表=空;单例引起的内存泄漏。周期比较长,使用不当很容易造成内存泄漏。举下面的典型例子。publicclassSingleInstanceClass{privatestaticSingleInstanceClassinstance;privateContextmContext;privateSingleInstanceClass(Contextcontext){this.mContext=context;}publicSingleInstanceClassgetInstance(Contextcontext){if(instance==null){instance=newSingleInstanceClass(context);}returninstance;}}前面提到了静态变量lifecycleof等于应用的生命周期,这里传入的Context参数就是诅咒。如果传入一个Activity或Fragment,由于单例一直持有它们的引用,即使Activity或Fragment被销毁,其内存也不会被回收。尤其是一些巨大的activity,非常容易造成OOM。正确的写法应该是传递Application的Context,因为Application的生命周期就是整个应用的生命周期,所以没有问题。publicclassSingleInstanceClass{privatestaticSingleInstanceClassinstance;privateContextmContext;privateSingleInstanceClass(Contextcontext){this.mContext=context.getApplicationContext();//使用Application的context}publicSingleInstanceClassgetInstance(Contextcontext){if(instance==null){instance=newSingleInstancennstance}Class(;}}or//在Application中定义获取全局上下文的方法/***获取全局上下文*@return返回全局上下文对象*/publicstaticContextgetContext(){returncontext;}publicclassSingleInstanceClass{privatestaticSingleInstanceClassinstance;privateContextmContext;privateSingleInstanceClass(){mContext=MyApplication.getContext;}publicSingleInstanceClassgetInstance(){if(instance==null){instance=newSingleInstanceClass();}returninstance;}}匿名内部类/非静态内部类和异步线程非静态内部类创建静态实例内存泄漏我们都知道非静态内部类持有外部类的引用默认情况下。如果在内部类中定义了单例实例,则无法释放外部类。比如下面的代码:publicclassTestActivityextendsAppCompatActivity{publicstaticInnerClassinnerClass=null;@OverrideprotectedvoidonCreate(@NullableBundlesavedInstanceState){super.onCreate(savedInstanceState);if(innerClass==null)innerClass=newInnerClass();}privateclassInnerClass{//...}}时销毁TestActivity,因为innerClass的生命周期相当于应用程序的生命周期,但是它还持有TestActivity的引用,从而导致内存泄漏。正确的做法应该是将内部类设置为静态内部类或者将内部类抽取出来封装成单例。如果需要使用Context,请使用上面推荐的ApplicationContext。当然,Application的context不是唯一的,不能随便用。有些地方,必须要用到Activity的Context。Application、Service、Activity的Context的应用场景如下:匿名内部类android开发经常继承ImplementActivity/Fragment/View。这时候如果使用匿名类,被异步线程持有,就要小心了。如果没有措施,这肯定会导致泄漏。如下代码:publicclassTestActivityextendsAppCompatActivity{//...privateRunnablerunnable=newRunnable(){@Overridepublicvoidrun(){}};@OverrideprotectedvoidonCreate(@NullableBundlesavedInstanceState){super.onCreate(savedInstanceState);//...}}runnable引用的类持有对TestActivity的引用。当传入异步线程时,线程与Activity生命周期的不一致会导致内存泄漏。Handler造成内存泄漏的根本原因是Handler的生命周期与Activity或View的生命周期不一致。Handler属于TLS(ThreadLocalStorage)生命周期,与应用程序周期相同。看下面的代码:@Overridepublicvoidrun(){//doyourthings}},60*1000*10);finish();}}在TestActivity中,声明一条延迟10分钟的消息消息,mHandler将其推入消息队列MessageQueue。当Activityfinish()时,延迟任务执行的Message会继续存在于主线程中,它持有Activity的Handler引用,所以此时finished()的Activity不会被回收,导致内存泄漏(因为Handler是一个非静态内部类,它会持有一个外部类的引用,这里指的是TestActivity)。修复方法:采用内部静态类和弱引用方案。代码如下:publicclassTestActivityextendsAppCompatActivity{privateMyHandlermHandler;privatestaticclassMyHandlerextendsHandler{privatefinalWeakReferencemActivity;publicMyHandler(TestActivityactivity){mActivity=newWeakReference<>(activity);}@OverridepublicvoiddispatchMessage(Messagemsg){super.dispatchTestActivity.Activity();;//doyourthings}}privatestaticfinalRunnablemRunnable=newRunnable(){@Overridepublicvoidrun(){//doyourthings}};@OverrideprotectedvoidonCreate(@NullableBundlesavedInstanceState){super.onCreate(savedInstanceState);mHandler=newMyHandler(this);mTime(mHandler.postAtable,1000*60*10);finish();}}需要注意的是:使用静态内部类+WeakReference的方法,注意每次使用前清空。前面我们提到了WeakReference,所以这里简单说说Java对象的几种引用类型。Java中的引用分为四类:强引用、软引用、弱引用和PhatomReference。ok,继续回归主题。前面说过,创建一个静态的Handler内部类,然后对Handler持有的对象使用弱引用,这样在回收的时候Handler持有的对象也可以被回收,不过这样可以避免Activity泄露,但是Looperthread队列中可能还有待处理的消息,所以我们应该在Activity被Destroyed或者Stopped的时候移除消息队列MessageQueue中的消息。以下方法可以移除消息:publicfinalvoidremoveCallbacks(Runnabler);publicfinalvoidremoveCallbacks(Runnabler,Objecttoken);publicfinalvoidremoveCallbacksAndMessages(Objecttoken);publicfinalvoidremoveMessages(intwhat);publicfinalvoidremoveMessages(intwhat;Objectobject)如果是静态的,那么我们都知道它的生命周期会和整个app进程的生命周期一样。这会导致一系列问题。如果你的app进程被设计成驻留在内存中,即使app切到后台,这部分内存也不会被释放。根据目前的手机app内存管理机制,占用内存大的后台进程会先被回收,也就是说如果app一直保持存活,会导致app在后台频繁重启。会出现手机在电量和流量上一夜消耗殆尽,只会被用户抛弃。这里的解决方法是:不要在初始化类时初始化静态成员。考虑惰性初始化。在架构设计上,需要思考是否真的有必要这样做,尽量避免。如果架构需要这样设计,那么你就要负责管理这个对象的生命周期。避免覆盖finalize():finalize方法在不确定的时间执行,不能依赖它来释放稀缺资源。时间不确定的原因是:虚拟机调用GC的时间不确定,Finalize守护线程调度的时间不确定。finalize方法只会执行一次。即使对象复活了,如果已经执行过finalize方法,再次GC时也不会再执行。原因是:包含finalize方法的对象是虚拟机在new的时候生成的一个finalizereference引用了这个Object,当finalize方法执行的时候,对象对应的finalizeReference会被释放,即使此时对象被复活(即对象被强引用引用),第二次GC时,因为没有对应的finalize引用,所以不会再次执行finalize方法。具有Finalize方法的对象至少需要经过两轮GC才能被释放。其他内存泄漏检测工具强烈推荐squareup的LeakCannary,但需要注意Android版本4.4+,否则会崩溃。