天天看点

jvm垃圾收集机制详解(中)二、垃圾收集算法(仅算法思想)

上一篇传送门:jvm垃圾收集机制详解(上)

二、垃圾收集算法(仅算法思想)

1.标记清除算法

标记清除算法是另外两种垃圾回收算法的基础,之所以说是基础是因为这种算法仅仅是简简单单地把标记了需要清除的对象进行了回收而已,除此之外没有任何其它操作。这种算法有很多不足,例如标记和清除的效率不高,清楚之后明明空闲内存的总和足够,但是就是无法容纳下大对象从而必须提前触发下一次垃圾回收或者干脆就报内存溢出异常了。

下面是这种算法的示意图:

  • 绿色:存活的对象
  • 黑色:要被回收的对象
  • 白色:剩余的空闲内存空间

垃圾回收前:

jvm垃圾收集机制详解(中)二、垃圾收集算法(仅算法思想)

垃圾回收后:

jvm垃圾收集机制详解(中)二、垃圾收集算法(仅算法思想)

2.复制算法

为了解决标记清除算法的问题,复制算法应运而生,这种算法的思想是把内存分成两块,我们先使用其中的一块,当使用的这一块需要进行垃圾回收的时候,就把这块上面还存活着的对象复制到另一块上面,然后把原来的这块上面的所有对象都回收掉。这种算法的代价是浪费了一半的内存。

图示:

回收前:

jvm垃圾收集机制详解(中)二、垃圾收集算法(仅算法思想)

复制对象:

jvm垃圾收集机制详解(中)二、垃圾收集算法(仅算法思想)

回收左半区所有对象:

jvm垃圾收集机制详解(中)二、垃圾收集算法(仅算法思想)

回收后:

jvm垃圾收集机制详解(中)二、垃圾收集算法(仅算法思想)

由于这种算法浪费了一半的内存,因此我们现在的垃圾回收机制做了一些改进,我们用这种算法去回收新生代对象,新生代对象在内存中存活的时间往往很短,IBM公司的研究表名,新生代中几乎98%的对象都很“短命”,也就是说活着的新生代对象较少,死去的新生代较多,也就是说我们每次复制的对象并不多,占用的空间并不大。那么,我们干嘛还要1:1地去划分两块内存呢?

于是,现在的虚拟机都不去等大小地去划分两块内存,而是划分三块内存,一块大的,两块小的,我们每次使用那块大的和其中一块小的,当需要使用复制算法回收对象的时候,就把大的和小的中存活的对象一起复制到未使用的小块中,然后一并回收大块和原来使用的小块中的对象,接下来继续使用一块大的和我们最后复制到的小块,以此类推。这里的大块叫做Eden,两个小块叫做Survivor。以我们现在最常用的HotSpot虚拟机为例,划分一大两小的比例是8:1:1,如下图所示:

jvm垃圾收集机制详解(中)二、垃圾收集算法(仅算法思想)

垃圾回收过程图示:

回收前:

jvm垃圾收集机制详解(中)二、垃圾收集算法(仅算法思想)

复制到Survivor2:

jvm垃圾收集机制详解(中)二、垃圾收集算法(仅算法思想)

回收Eden和Survivor1区所有对象:

jvm垃圾收集机制详解(中)二、垃圾收集算法(仅算法思想)

回收后:

jvm垃圾收集机制详解(中)二、垃圾收集算法(仅算法思想)

接下来继续使用Eden区和Survivor2区,再需要回收时就该轮到向Survivor1区复制了

使用这种算法有一个问题,就是我们没法保证每次Eden区和一个Survivor区中存活的对象加起来一定小于等于我们总存储空间的10%,也就是一定小于等于另一个Survivor区的大小,那么要是该复制的时候发现另一个Survivor区装不下了怎么办?

这就需要我们有一个可以担保的空间,就好比我们去银行贷款需要担保人或者抵押房产之类的一样,当Survivor空间不够用的时候,我们需要依靠老年代进行分配担保。

另外注意这种算法的使用场景,一定要在存活的需要复制的对象比较少的情况使用,否则复制大量的对象也是很大的一笔开销,所以这就解释了为什么我们用这种算法去回收新生代对象。

3.标记整理算法

复制算法在存活对象较多的时候不太适合,原因上面说过了,所以老年代对象的回收不适合使用这种算法,根据老年代的特点,我们选择了“标记整理”算法进行回收。标记过程没什么可说的,而后续的步骤时将存活的对象都向一侧去移动,覆盖掉前面的将被回收对象,然后直接清理掉另一侧剩余的所有被回收对象。算法图示如下:

回收前:

jvm垃圾收集机制详解(中)二、垃圾收集算法(仅算法思想)

将存活对象向一侧移动,如果遇到将被回收的对象,就覆盖它:

jvm垃圾收集机制详解(中)二、垃圾收集算法(仅算法思想)

回收后:

jvm垃圾收集机制详解(中)二、垃圾收集算法(仅算法思想)

下一篇传送门:jvm垃圾收集机制详解(下)

  • 参考书籍:《深入理解Java虚拟机》