垃圾收集算法
1.标记清除算法
最基础的GC算法是“标记--清除”算法,顾名思义,这个算法分为“标记”和“清除”两个过程:先标记出所有需要被回收的对象,在标记完成后,统一进行回收,之所以叫做最基础的,是因为后续的算法是针对该算法不足的地方进行改进。首先,标记清除是个很繁琐的过程,效率较低,每当进行过一次标记清除之后,都会产生大量的不连续的空间碎片,当空间碎片太多时,每当程序运行时需要分配大对象的时候,都会因为无法找到一块较大的连续空间碎片而又提前进行一次GC。
2.复制算法
就刚才的标记清除之后,为了解决刚才的问题,提出了一种叫做复制算法的思想,他将可用的内存划分为两块大小相等的区域,每次我们都只使用其中一块。当这块用完之后,就将还存活的对象复制到另一块区域,然后再把之前使用过的区域一次性清理掉,这样的话,我们每次都只对一般区域进行回收,并且我们也可以不用去考虑内存碎片的情况,但是这样的缺点就是,空间会浪费掉一半......
其实在堆中,大多数的对象都是“朝生夕死”的,所以我们并不需要按照1:1的比例来对这块区域进行划分,这里就涉及到了Eden区和两块Survivor区的概念,Eden区一般都是分配刚出炉不久的对象,所以Eden区域所占的比例是很大的,Eden区和两块SurVivor区的比例是8:1:1,我们每次都使用其中的Eden区和一块Survivor区,每次GC时,都会把Eden和其中一块Survivor区的存活对象复制到另一个Survivor区,所以每次新生代可用的空间占整个新生代区的百分之90,只有百分之10会被浪费,这也解决了上面的1:1比例会浪费一半空间的问题!!
不过我们要注意的是,不是每次回收都会这么的“理想”.......会出现一种情况就是,我们根本无法保证每次回收后都只有不到百分之10的对象存活下来,当那块Survivor区域不够用时,我们需要把这些存活后的对象存储到其他的内存空间(这里一般指老年代区域),这种我们称之为分配担保(Handle Promotion)。
分配担保其实很好理解它的意思,如果我向张三借钱,并且我的信誉很好,在百分之98的情况下,我都可以按时还款,于是张三是乐意向我借钱的,但是如果说我万一手头特别紧,拿不出钱,这个时候张三肯定是不高兴的,于是每次我向他借钱时,我都会跟他说:“如果我不能按时还款,你可以找我的担保人李四,让他帮你还!”,这种机制,就是我们这里所说到的分配担保,而李四就相当于分配担保里的老年代。
3.标记整理算法
复制算法固然不错,但是,如果我们的运气贼尼玛背,每次我们new的对象在GC时,都要存活下来,那这个时候,每次都需要跳到老年代去做分配担保,虽然新生代的对象都是“朝生夕死”的,但是老年代的对象那可是老顽固啊,老年代如果采用这种算法,呵呵,那就准备天天迎接大量对象存活的情况吧!
既然老年代的对象都是老顽固,那么针对于这个特点,又出现了一种新的GC算法思想:标记整理算法。标记过程和标记清除算法一样的,但是后续步骤不是直接清除,而是把所有存活的对象都集中的往一个地方挪,然后直接清理掉那些需要清理的对象,这样看起来好像比标记清除算法多了一个步骤,更繁琐了些,但是却不会产生内存碎片。
4.分代收集
上面介绍了三种GC算法,但是这样看下来,感觉每个算法都有他们的优缺点,而这个分代收集,并不是什么新的算法,只是根据对象存活的生命周期将java堆划分为新生代和老年代,我们的虚拟机根据各个年代各自的特点采用最适合的算法。
新生代,每次回收都有大量对象死去,所以这里最好采用复制算法,因为每次回收都只需要付出少量的存活对象的复制成本就可以完成GC。
老年代,因为对象的存活率很高,不适合复制算法(因为复制大量对象需要的成本很大),而且老年代也没有额外的空间来做分配担保,所以可以采用标记清除或者是标记整理算法来进行回收。