面试问题
- 什么是内存泄露,什么是内存溢出
- 什么情况下会造成堆溢出、栈溢出
- 常见造成内存泄露的情况
- 常见造成内存溢出的情况
- 谈一下垃圾回收机制
什么是内存泄露,什么是内存溢出
内存溢出就是Out Of Memory:指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错OOM,即所谓的内存溢出。
内存泄漏Mempry Leak:是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。内存溢出分堆溢出,栈溢出。
什么情况下会造成堆溢出、栈溢出
堆溢出:不停地创建对象。
栈溢出:方法递归。
常见造成内存泄露的情况
内存泄露一般是由长生命周期引用短生命周期导致的。
- 单例引用Activity或Fragment。
- Handler引用Activity,Handler类使用static,使用弱引用持有外部Activity,在使用之前进行判断。
- 未取消注册广播或回调导致内存泄露。
- 集合中的对象未清理造成内存泄露,应该在不使用对象时从集合中删除。
- 关闭Activity时资源未关闭或释放导致内存泄露,比如WebView下面持有Activity的引用,需要销毁WebView之前先将WebView从父容器中移除,然后再销毁销毁WebView。
常见造成内存溢出的情况
- 生产者与消费者速率不一致。没有控制队列中最大size大小,导致堆内存溢出。
- JSONObject.toJSON(object) 来处理Javabean的,这样处理简单的对象是没有问题的,但是对象如果复杂的话就会发生一些问题。object对象过于复杂和大量时,用toJSOn解析就会出现CPU、内存一直飙升,JVM一直执行GC操作,但是无法回收内存,最后会报
java.lang.OutOfMemoryError: GC overhead limit exceeded
错误。 - 内存抖动,优化内存抖动,核心就是防止频繁创建对象。常见的反面教材就是:循环中创建对象,大量调用的api中创建对象。而优化的主要手段,就是对象复用,常见的手段是:对象池,像是Handler的Message单链表池,Glide的bitmap池等。
谈一下垃圾回收机制
对象是否会被回收的两个经典算法:引用计数法,和可达性分析算法。
引用计数法:
- 简单的来说就是判断对象的引用数量。实现方式:给对象共添加一个引用计数器,每当有引用对他进行引用时,计数器的值就加1,当引用失效,也就是不在执行此对象是,他的计数器的值就减1,若某一个对象的计数器的值为0,那么表示这个对象没有人对他进行引用,也就是意味着是一个失效的垃圾对象,就会被gc进行回收。
- 但是这种简单的算法在当前的jvm中并没有采用,原因是他并不能解决对象之间循环引用的问题。
- 假设有A和B两个对象之间互相引用,也就是说A对象中的一个属性是B,B中的一个属性时A,这种情况下由于他们的相互引用,从而是垃圾回收机制无法识别。
和可达性分析算法:
因为引用计数法的缺点有引入了可达性分析算法,通过判断对象的引用链是否可达来决定对象是否可以被回收。可达性分析算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,通过一系列的名为GCRoots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GCRoots 没有任何引用链相连(就是从 GC Roots 到这个对象不可达)时,则证明此对象是不可用的。
jvm会在什么时候进行回收:
- 会在cpu空闲的时候自动进行回收
- 在堆内存存储满了之后
- 主动调用System.gc()后尝试进行回收
如何回收:
如何回收说的也就是垃圾收集的算法。
算法又有四个:标记-清除算法,复制算法,标记-整理算法,分代收集算法。
标记-清除算法:
这是最基础的一种算法,分为两个步骤,第一个步骤就是标记,也就是标记处所有需要回收的对象,标记完成后就进行统一的回收掉哪些带有标记的对象。这种算法优点是简单,缺点是效率问题,还有一个最大的缺点是空间问题,标记清除之后会产生大量不连续的内存碎片,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而造成内存空间浪费。
复制算法:
复制将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况。只是这种算法的代价是将内存缩小为原来的一半。
标记-整理算法:
标记整理算法与标记清除算法很相似,但最显著的区别是:标记清除算法仅对不存活的对象进行处理,剩余存活对象不做任何处理,造成内存碎片;而标记整理算法不仅对不存活对象进行处理清除,还对剩余的存活对象进行整理,重新整理,因此其不会产生内存碎片。
分代收集算法:
分代收集算法是一种比较智能的算法,也是现在jvm使用最多的一种算法,他本身其实不是一个新的算法,而是他会在具体的场景自动选择以上三种算法进行垃圾对象回收。
了解过场景之后再结合分代收集算法得出结论:
- 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法。只需要付出少量存活对象的复制成本就可以完成收集。
- 老年代中因为对象存活率高、没有额外空间对他进行分配担保,就必须用标记-清除或者标记-整理。