**通过一波一波的淘汰与洗牌,剩下的都是技术的金子。**就像大浪褪去,裸泳的会慢慢上岸。而真正坚持下来的必定会取得不错成绩。毕竟Android市场是如此之大。从Android高级的蓬勃的就业岗位需求来看,能坚信咱们每一位Android开发者的梦想 。java
接下来咱们针对Android高级展开的完整面试题 2019Android74道高级面试题合集目录(含BAT 字节跳动等等)面试
从Jvm内存模型中入手对于理解GC会有很大的帮助,不过这里只须要了解一个大概,说多了反而混淆视线。objective-c
Jvm(Java虚拟机)主要管理两种类型内存:堆和非堆。 堆是运行时数据区域,全部类实例和数组的内存均今后处分配。 非堆是JVM留给本身用的,包含方法区、JVM内部处理或优化所需的内存(如 JIT Compiler,Just-in-time Compiler,即时编译后的代码缓存)、每一个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法的代码。算法
简言之,Java程序内存主要(这里强调主要二字)分两部分,堆和非堆。你们通常new的对象和数组都是在堆中的,而GC主要回收的内存也是这块堆内存。数组
配一张示意图总结一下:缓存
堆内存(Heap Memory): 存放Java对象 非堆内存(Non-Heap Memory): 存放类加载信息和其它meta-data 其它(Other): 存放JVM 自身代码等bash
既然重点是堆内存,咱们就再看看堆的内存模型。并发
堆内存由垃圾回收器的自动内存管理系统回收。 堆内存分为两大部分:新生代和老年代。比例为1:2。 老年代主要存放应用程序中生命周期长的存活对象。 新生代又分为三个部分:一个Eden区和两个Survivor区,比例为8:1:1。 Eden区存放新生的对象。 Survivor存放每次垃圾回收后存活的对象。优化
看晕了吧,关注这几个问题:spa
1.为何要分新生代和老年代? 2.新生代为何分一个Eden区和两个Survivor区? 3.一个Eden区和两个Survivor区的比例为何是8:1:1?
如今还不能解释为何,但这几个问题都是垃圾回收机制所采用的算法决定的。 因此问题转化为,是何种算法?为何要采用此种算法?
讲算法以前,咱们先要搞清楚一个问题,什么样的对象是垃圾(无用对象),须要被回收? 目前市面上有两种算法用来断定一个对象是否为垃圾。
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任什么时候刻计数器为0的对象就是不可能再被使用的
objective-c用的就是这种算法。 缺点是很难处理循环引用,好比图中相互引用的两个对象则没法释放。 这个缺点很致命,有人可能会问,那objective-c不是用的好好的吗? 我我的并无以为objective-c好好的处理了这个循环引用问题,它实际上是把这个问题抛给了开发者。
为了解决上面的循环引用问题,Java采用了一种新的算法:可达性分析算法。 从GC Roots(每种具体实现对GC Roots有不一样的定义)做为起点,向下搜索它们引用的对象,能够生成一棵引用树,树的节点视为可达对象,反之视为不可达。 [图片上传失败...(image-756421-1562504082698)] OK,即便循环引用了,只要没有被GC Roots引用了依然会被回收,完美! 可是,这个GC Roots的定义就要考究了,Java语言定义了以下GC Roots对象:
虚拟机栈(帧栈中的本地变量表)中引用的对象。 方法区中静态属性引用的对象。 方法区中常量引用的对象。 本地方法栈中JNI引用的对象。
有了上面的垃圾对象的断定,咱们还要考虑一个问题,请你们作好内心准备,那就是Stop The World。 由于垃圾回收的时候,须要整个的引用状态保持不变,不然断定是断定垃圾,等我稍后回收的时候它又被引用了,这就全乱套了。因此,GC的时候,其余全部的程序执行处于暂停状态,卡住了。 幸运的是,这个卡顿是很是短(尤为是新生代),对程序的影响微乎其微 (关于其余GC好比并发GC之类的,在此不讨论)。 因此GC的卡顿问题由此而来,也是情有可原,暂时无可避免。
有了上面两个大基础,咱们的GC才能开始。 那么问题来了,已经知道哪些是垃圾对象了,怎么回收呢?目前主流有如下几种算法。 PS:你们能够先猜猜Java虚拟机(这里默认指Hotspot)采用的是那种算法,…,答对了,是分代回收算法,如今是否是明白了前面堆内存为何要分新生代和老年代了吧。可是即便猜对了,也要看其余几种算法哦,否则不要说我没提醒你,你会直接看不懂分代回收算法的。
标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出全部须要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。 优势是简单,容易实现。 缺点是容易产生内存碎片,碎片太多可能会致使后续过程当中须要为大对象分配空间时没法找到足够的空间而提早触发新的一次垃圾收集动做。 示意图以下(不用我解说了吧):
复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,而后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。 优缺点就是,实现简单,运行高效且不容易产生内存碎片,可是却对内存空间的使用作出了高昂的代价,由于可以使用的内存缩减到原来的一半。 从算法原理咱们能够看出,Copying算法的效率跟存活对象的数目多少有很大的关系,若是存活对象不少,那么Copying算法的效率将会大大下降。 示意图以下(不用我解说了吧):
该算法标记阶段和Mark-Sweep同样,可是在完成标记以后,它不是直接清理可回收对象,而是将存活对象都向一端移动,而后清理掉端边界之外的内存。 因此,特别适用于存活对象多,回收对象少的状况下。 示意图以下(不用我解说了吧):
分代回收算法其实不算一种新的算法,而是根据复制算法和标记整理算法的的特色综合而成。这种综合是考虑到java的语言特性的。 这里重复一下两种老算法的适用场景:
复制算法:适用于存活对象不多。回收对象多 标记整理算法: 适用用于存活对象多,回收对象少
恰好互补!不一样类型的对象生命周期决定了更适合采用哪一种算法。 因而,咱们根据对象存活的生命周期将内存划分为若干个不一样的区域。通常状况下将堆区划分为老年代(Old Generation)和新生代(Young Generation),老年代的特色是每次垃圾收集时只有少许对象须要被回收,而新生代的特色是每次垃圾回收时都有大量的对象须要被回收,那么就能够根据不一样代的特色采起最适合的收集算法。 这就是分代回收算法。 如今回头去看堆内存为何要划分新生代和老年代,是否是以为如此的清晰和天然了?
咱们再说的细一点:
1.对于新生代采起Copying算法,由于新生代中每次垃圾回收都要回收大部分对象,也就是说须要复制的操做次数较少,采用Copying算法效率最高。可是,可是,可是,实际中并非按照上面算法中说的1:1的比例来划分新生代的空间的,而是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,比例为8:1:1.。为何?下一节深刻分析。 2.因为老年代的特色是每次回收都只回收少许对象,通常使用的是Mark-Compact算法。
对于这个算法,我相信不少人仍是有疑问的,咱们来各个击破,说清楚了就很简单。
这里涉及到一个新生代和老年代的存活周期的问题,好比一个对象在新生代经历15次(仅供参考)GC,就能够移到老年代了。问题来了,当咱们第一次GC的时候,咱们能够把Eden区的存活对象放到Survivor A空间,可是第二次GC的时候,Survivor A空间的存活对象也须要再次用Copying算法,放到Survivor B空间上,而把刚刚的Survivor A空间和Eden空间清除。第三次GC时,又把Survivor B空间的存活对象复制到Survivor A空间,如此反复。 因此,这里就须要两块Survivor空间来回倒腾。
新建立的对象都是放在Eden空间,这是很频繁的,尤为是大量的局部变量产生的临时对象,这些对象绝大部分都应该立刻被回收,能存活下来被转移到survivor空间的每每很少。因此,设置较大的Eden空间和较小的Survivor空间是合理的,大大提升了内存的使用率,缓解了Copying算法的缺点。 我看8:1:1就挺好的,固然这个比例是能够调整的,包括上面的新生代和老年代的1:2的比例也是能够调整的。 新的问题又来了,从Eden空间往Survivor空间转移的时候Survivor空间不够了怎么办?直接放到老年代去。
这里原本简单的Copying算法被划分为三部分后不少朋友一时理解不了,也确实很差描述,下面我来演示一下Eden空间和两块Survivor空间的工做流程。
如今假定有新生代Eden,Survivor A, Survivor B三块空间和老生代Old一块空间。
// 分配了一个又一个对象
放到Eden区
// 很差,Eden区满了,只能GC(新生代GC:Minor GC)了
把Eden区的存活对象copy到Survivor A区,而后清空Eden区(原本Survivor B区也须要清空的,不过原本就是空的)
// 又分配了一个又一个对象
放到Eden区
// 很差,Eden区又满了,只能GC(新生代GC:Minor GC)了
把Eden区和Survivor A区的存活对象copy到Survivor B区,而后清空Eden区和Survivor A区
// 又分配了一个又一个对象
放到Eden区
// 很差,Eden区又满了,只能GC(新生代GC:Minor GC)了
把Eden区和Survivor B区的存活对象copy到Survivor A区,而后清空Eden区和Survivor B区
// ...
// 有的对象来回在Survivor A区或者B区呆了好比15次,就被分配到老年代Old区
// 有的对象太大,超过了Eden区,直接被分配在Old区
// 有的存活对象,放不下Survivor区,也被分配到Old区
// ...
// 在某次Minor GC的过程当中忽然发现:
// 很差,老年代Old区也满了,这是一次大GC(老年代GC:Major GC)
Old区慢慢的整理一番,空间又够了
// 继续Minor GC
// ...
// ...
复制代码
从这段流程中,我相信你们应该有了一个清晰的认识了,固然为了说明原理,这只是最简化版本。
了解这些是为了解决实际问题,Java虚拟机会把每次触发GC的信息打印出来来帮助咱们分析问题,因此掌握触发GC的类型是分析日志的基础。
GC_FOR_MALLOC: 表示是在堆上分配对象时内存不足触发的GC。 GC_CONCURRENT: 当咱们应用程序的堆内存达到必定量,或者能够理解为快要满的时候,系统会自动触发GC操做来释放内存。 GC_EXPLICIT: 表示是应用程序调用System.gc、VMRuntime.gc接口或者收到SIGUSR1信号时触发的GC。 GC_BEFORE_OOM: 表示是在准备抛OOM异常以前进行的最后努力而触发的GC