Android面试被问到内存泄漏了咋整?

2019年04月08日 469点热度 1人点赞 1条评论

【转载】https://juejin.im/post/5c73b10ee51d455f1c31132c

内存泄漏即该被释放的内存没有被及时的释放,一直被某个或某些实例所持有却不再使用导致GC不能回收。
文末准备了一份完整系统的进阶提升的技术大纲和学习资料,希望对于有一定工作经验但是技术还需要提升的朋友提供一个方向参考,以及免去不必要的网上到处搜资料时间精力。

Java内存分配策略

Java程序运行时的内存分配策略有三种,分别是静态分配,栈式分配,和堆式分配。对应的三种策略使用的内存空间是要分别是静态存储区(也称方法区),栈区,和堆区。

  • 静态存储区(方法区):主要存放静态数据,全局static数据和常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。
  • 栈区:当方法执行时,方法内部的局部变量都建立在栈内存中,并在方法结束后自动释放分配的内存。因为栈内存分配是在处理器的指令集当中所以效率很高,但是分配的内存容量有限。
  • 堆区:又称动态内存分配,通常就是指在程序运行时直接new出来的内存。这部分内存在不适用时将会由Java垃圾回收器来负责回收。

栈与堆的区别:

在方法体内定义的(局部变量)一些基本类型的变量和对象的引用变量都在方法的栈内存中分配。当在一段方法块中定义一个变量时,Java就会在栈中为其分配内存,当超出变量作用域时,该变量也就无效了,此时占用的内存就会释放,然后会被重新利用。

堆内存用来存放所有new出来的对象(包括该对象内的所有成员变量)和数组。在堆中分配的内存,由Java垃圾回收管理器来自动管理。在堆中创建一个对象或者数组,可以在栈中定义一个特殊的变量,这个变量的取值等于数组或对象在堆内存中的首地址,这个特殊的变量就是我们上面提到的引用变量。我们可以通过引用变量来访问堆内存中的对象或者数组。

public class Sample {
    int s1 = 0;
    Sample mSample1 = new Sample();
    public void method() {
        int s2 = 0;
        Sample mSample2 = new Sample();
    }
}
Sample mSample3 = new Sample();

如上局部变量s2mSample2存放在栈内存中,mSample3所指向的对象存放在堆内存中,包括该对象的成员变量s1mSample1也存放在堆中,而它自己则存放在栈中。

结论:

局部变量的基本类型和引用存储在栈内存中,引用的实体存储在堆中。——因它们存在于方法中,随方法的生命周期而结束。

成员变量全部存储于堆中(包括基本数据类型,引用和引用的对象实体)。——因为它们属于类,类对象终究要被new出来使用。

了解了Java的内存分配之后,我们再来看看Java是怎么管理内存。

Java是如何管理内存

由程序分配内存,GC来释放内存。内存释放的原理为该对象或者数组不再被引用,则JVM会在适当的时候回收内存。

内存管理算法:

  1. 引用计数法:对象内部定义引用变量,当该对象被某个引用变量引用时则计数加1,当对象的某个引用变量超出生命周期或者引用了新的变量时,计数减1。任何引用计数为0的对象实例都可以被GC。这种算法的优点是:引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。缺点:无法检测出循环引用。

引用计数无法解决的循环引用问题如下:

public void method() {
    //Sample count=1
    Sample ob1 = new Sample();
    //Sample count=2
    Sample ob2 = new Sample();
    //Sample count=3
    ob1.mSample = ob2;
    //Sample count=4
    ob2.mSample = ob1;
    //Sample count=3
    ob1=null;
    //Sample count=2
    ob2=null;
    //计数为2,不能被GC
}
Java可以作为GC ROOT的对象有:虚拟机栈中引用的对象(本地变量表),方法区中静态属性引用的对象,方法区中常量引用的对象,本地方法栈中引用的对象(Native对象)
  1. 标记清除法:从根节点集合进行扫描,标记存活的对象,然后再扫描整个空间,对未标记的对象进行回收。在存活对象较多的情况下,效率很高,但是会造成内存碎片。
  2. 标记整理算法:同标记清除法,只不过在回收对象时,对存活的对象进行移动。虽然解决了内存碎片的问题但是增加了内存的开销。
  3. 复制算法:此方法为克服句柄的开销和解决堆碎片。把堆分为一个对象面和多个空闲面。把存活的对象copy到空闲面,主要空闲面就变成了对象面,原来的对象面就变成了空闲面。这样增加了内存的开销,且在交换过程中程序会暂停执行。
  4. 分代算法:

分代垃圾回收策略,是基于:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。

年轻代:

  1. 所有新生成的对象首先都是存放在年轻代。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
  2. 新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。
  3. 当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收
  4. 新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)

年老代:

  1. 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
  2. 内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。

持久代:

用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。

 

Horry

一个专门收集Android面试题的网站

文章评论

  • Android面试题

    Mark

    2019年04月08日
  • 您需要 登录 之后才可以评论