前言
相信熟悉Java语言的程序员,对JVM肯定不陌生,而JVM中的垃圾收集器对JVM来说,是非常重要的一个功能模块,也正是这个模块,让我们在写Java代码时,不需要考虑我们的Java程序内存"垃圾"如何回收的。
本文将介绍四种垃圾收集算法。
标记-清除算法
这是最简单的回收算法了,这种算法只有两个阶段:“标记"和"清除”,首先在内存空间里标记好要清除的对象,标记完成后统一清除。这种算法简单粗暴,但缺点有两个:
1.标记和清除这两个过程的运行效率都不高
2.标记清除后会剩余大量不连续的内存碎片,碎片太多可能会导致程序再次分配一段较大连续的内存空间时,无法找到足够的连续内存,从而需要提前触发一次垃圾收集动作
问题2感觉有点恶性循环了…先来看下垃圾回收前后的对比图吧
垃圾回收后,空间是清理出来了,但是内存空间碎片也变多了。所以这种算法并不实用。
复制算法
复制算法简单来说就是空间换时间的算法。
这种算法的思想是用将内存空间划分为大小相等的两块,程序使用时只用其中一块,当内存使用完时,就将活着的对象复制到另一块上面,然后直接将之前那块内存全部清理掉,真是简单又粗暴,这种算法只执行了一次复制和一次清除,所以效率很高,但是可用内存被缩小了一半。
使用复制算法进行垃圾回收后,内存布局大致如下。
这种算法有很大的优化和调整的空间。
据IBM公司调查,新生代中的98%的对象都是朝生夕死,存活率很低,现在的商业虚拟机都是这种算法回收新生代垃圾,但不是按照1:1的比例进行内存分划的。HotSpot虚拟机默认按照8:1:1的比例,把内存空间划分为一块比较大的Eden空间和两块比较小的Survivor空间,每次使用一块Eden和一块Survivor空间,在垃圾回收时,把Eden和Survivor中还存活着的对象一次性复制到另一块干净的Survivor空间中上,最后清除Eden空间和那块用过的Survivor空间。
这样分配内存空间的好处就是只有很少的内存空间被"浪费",也就是那10%保留的干净空间。
那么问题来了,我们在进行垃圾回收时,没办法保证每次回收的内存大小都小于等于10%,如果那块保留的干净Survivor空间不够用,这时该怎么办?
这时就需要依赖其他内存进行分配担保,当那块干净的Survivor空间没有足够空间存放上一次新生代清理存活下来的对象时,这些对象将直接进入老年代。
标记-整理算法
这种算法主要用在老年代对象,我们知道老年代对象存活率很高,如果使用复制算法来清理对象,就需要"浪费"10%的内存空间作为交换区域,在年轻代存活率100%的极端情况下,老年代会OOM的。
所以有人提出了"标记-整理"算法。
这种算法的思想跟"标记-清除"算法差不多,只不过在标记完后不是直接清除对象,而是把对象往一端移动,然后直接清理掉端边界以外的内存。
回收前后大致如下,图画得有点糙了…
分代收集算法
这种算法的核心思想是把对象按照存活周期划分为新生代和老年代,根据不同年代采用不同的算法。
在新生代中,内次垃圾收集时都有大量的对象死去,只有少量存活,则可以使用8:1:1的复制算法。
在老年代中,对象存活率高,没有额外的内存空间进行担保,则需使用"标记-清除"或者"标记-整理"算法进行回收。