由浅入深理解 Android 虚拟机—内存模型,垃圾回收机制是如何实现的
#####2. 复制算法 (Copying)复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。 优缺点就是,实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。 从算法原理我们可以看出,Copying 算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么 Copying 算法的效率将会大大降低。 示意图如下(不用我解说了吧):
#####3. 标记整理算法 (Mark-Compact)该算法标记阶段和 Mark-Sweep 一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。 所以,特别适用于存活对象多,回收对象少的情况下。 示意图如下(不用我解说了吧):
#####4. 分代回收算法分代回收算法其实不算一种新的算法,而是根据复制算法和标记整理算法的的特点综合而成。这种综合是考虑到 java 的语言特性的。 这里重复一下两种老算法的适用场景:
复制算法:适用于存活对象很少。回收对象多 标记整理算法: 适用用于存活对象多,回收对象少
刚好互补!不同类型的对象生命周期决定了更适合采用哪种算法。 于是,我们根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Old Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。 这就是分代回收算法。 现在回头去看堆内存为什么要划分新生代和老年代,是不是觉得如此的清晰和自然了?
我们再说的细一点:
对于新生代采取 Copying 算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,采用 Copying 算法效率最高。但是,但是,但是,实际中并不是按照上面算法中说的 1:1 的比例来划分新生代的空间的,而是将新生代划分为一块较大的 Eden 空间和两块较小的 Survivor 空间,比例为 8:1:1.。为什么?下一节深入分析。
由于老年代的特点是每次回收都只回收少量对象,一般使用的是 Mark-Compact 算法。
四.深入理解分代回收算法
对于这个算法,我相信很多人还是有疑问的,我们来各个击破,说清楚了就很简单。
####为什么不是一块 Survivor 空间而是两块?
这里涉及到一个新生代和老年代的存活周期的问题,比如一个对象在新生代经历 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 空间的往往不多。所以,设置较大的 Eden 空间和较小的 Survivor 空间是合理的,大大提高了内存的使用率,缓解了 Copying 算法的缺点。 我看 8:1:1 就挺好的,当然这个比例是可以调整的,包括上面的新生代和老年代的 1:2 的比例也是可以调整的。 新的问题又来了,从 Eden 空间往 Survivor 空间转移的时候 Survivor 空间不够了怎么办?直接放到老年代去。
####Eden 空间和两块 Survivor 空间的工作流程
这里本来简单的 Copying 算法被划分为三部分后很多朋友一时理解不了,也确实不好描述,下面我来演示一下 Eden 空间和两块 Survivor 空间的工作流程。
现在假定有新生代 Eden,Survivor A, Survivor B 三块空间和老生代 Old 一块空间。
// 分配了一个又一个对象放到 Eden 区// 不好,Eden 区满了,只能 GC(新生代 GC:Minor GC)了把 Eden 区的存活对象 copy 到 Survivor A 区,然后清空 Eden 区(本来 Survivor B 区也需要清空的,不过本来就是空的)// 又分配了一个又一个对象放到 E
den 区// 不好,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// ...// ...
从这段流程中,我相信大家应该有了一个清晰的认识了,当然为了说明原理,这只是最简化版本。
##五.触发 GC 的类型
了解这些是为了解决实际问题,Java 虚拟机会把每次触发 GC 的信息打印出来来帮助我们分析问题,所以掌握触发 GC 的类型是分析日志的基础。
GC_FOR_MALLOC: 表示是在堆上分配对象时内存不足触发的 GC。 GC_CONCURRENT: 当我们应用程序的堆内存达到一定量,或者可以理解为快要满的时候,系统会自动触发 GC 操作来释放内存。 GC_EXPLICIT: 表示是应用程序调用 System.gc、VMRuntime.gc 接口或者收到 SIGUSR1 信号时触发的 GC。 GC_BEFORE_OOM: 表示是在准备抛 OOM 异常之前进行的最后努力而触发的 GC
关于我
更多信息可以点击[关于我](
)?, 非常希望和大家一起交流 , 共同进步也可以扫一扫, 目前是一名程序员,不仅分享 Android 开发相关知识,同时还分享技术人成长历程,包括个人总结,职场经验,面试经验等,希望能让你少走一点弯路。
评论