12、JVM 调优实战 - 面试题:JVM中有哪些垃圾回收算法,每个算法各自的优劣?

主题: 新生代进行垃圾回收的时候,到底是采取一种什么样的算法进行的呢?

1.复制算法的背景引入

针对新生代的垃圾回收算法,叫做复制算法

新生代的内存分为两块,如下图:

 

假设有如下代码,在 “loadReplicasFromDisk()” 方法中创建了对象,此时对象就会分配在新生代其中一块内存空间里。

而且由“main线程” 的栈内存中的 “loadReplicasFromDisk()” 方法的栈帧内的局部变量来引用的,如下图所示:

 

 

假设此时,代码在不停的运行,然后大量对象都分配在了新生代内存的其中一块内存区域里,也只会分配在那块区域里。

而且分配过后,很快就失去局部变量或者类静态变量的引用,成为了垃圾对象。

 

接着这个时候,新生代内存那块被分配对象的内存区域基本都快满了,再次要分配对象的时候,发现里面内存空间不足了。

此时就会触发Minor GC去回收掉新生代那块被使用的内存空间的垃圾对象。

3. 垃圾回收的时候是怎么做的呢?

1、 不合理的垃圾回收思路

假设现在采用的垃圾回收思路,是直接对上图中被使用的那块内存区域中的垃圾对象进行标记。也就是标记出哪些对象是可以被垃圾回收的,然后就直接对那块内存区域中的对象进行垃圾回收,把内存空出来。

这种思路去回收,可能会在回收完毕后造成那块内存区域如下图:

 

在上图中,在那块被使用的内存区域里,虽然回收掉了大量的垃圾对象,但是保留了一些被人引用的存活对象。

并且如图所示,存活对象在内存区域里东一个西一个,非常的凌乱,在存活对象内存不连续的地方,就造成了大量的内存碎片

而内存碎片太多是会有很大问题的。

如内存浪费

由于内存碎片太多,而所有的内存碎片加起来其实是有很大的一块内存的,但由于碎片式分布,这就导致没有一块完整的足够的内存空间来分配新的对象。

而这就会造成大量的内存浪费。所以这种直接对一块内存空间回收掉垃圾对象,保留存活对象的方法,是不可取的

2、 合理的垃圾回收思路

比较合理的思路是:

首先对那块在使用的内存空间标记出里面哪些对象是不能进行垃圾回收的,也就是要存活的对象。

然后把那些存活的对象转移到另外一块空白的内存中,如下图:

 

上图中,通过把存活对象先转移到另外一块空白内存区域,就可以让被转移的那块内存区域几乎没有什么内存碎片,把这些对象都比较紧凑的按顺序排列在内存里。

此时,就可以将新对象分配在那块连续内存空间里了。

 

这时候,再一次性把原来使用的那块内存区域中的垃圾对象全部一扫而空,全部给回收掉,空出来一块内存区域,如下图:

 

这就是“复制算法” ,把新生代内存划分为两块内存区域,然后只使用其中一块内存。待那块内存快满的时候,就把里面的存活对象一次性转移到另外一块内存区域,保证没有内存碎片。

接着一次性回收原来那块内存区域的垃圾对象,再次空出来一块内存区域。两块内存区域就这么重复着循环使用

4. 复制算法的缺点

复制算法的缺点很明显,由于将新生代分为两块内存区域,其中一块用于内存空间在使用满了后,再复制转移到另外一块内存空间。

那么,这就意味着从始至终,就只有一半的内存可以用,对内存的使用效率太低了。

5. 复制算法的优化: Eden区和Survivor区

可能一次新生代垃圾回收过后,99%的对象都被垃圾回收了,就 1% 的对象存活了下来,可能就是一些长期存活的对象,或者还没使用完的对象。

所以真正的复制算法会做出如下优化,把新生代内存区域划分为三块

1个Eden区,2个Survivor区,其中Eden区占80%内存空间,每一块Survivor区各占10%内存空间,比如说Eden区有800MB内存,每一块Survivor区就100MB内存,如下图:

 

平时就使用 Eden区和其中一块 Survivor 区,相当于有 900MB的内存是可以使用的。

 

刚开始对象都是分配在 Eden区内的,如果 Eden区快满了,此时就会触发垃圾回收

接着会把Eden区中的存活对象都一次性转移到一块空着的 Survivor 区。接着 Eden区就会被清空,然后再次分配新对象到 Eden 区里。

如上图所示, Eden 区和一块 Survivor 区里是有对象的,其中 Survivor 区里放的是上一次 Minor GC存活的对象。

如果下次再次 Eden 区满,就再次触发 Minor GC,会把 Eden 区和放着上一次 Minor GC后存活对象的 Survivor 区内的存活对象,转移到另外一块 Survivor 区去。

这一过程,始终保持一块 Survivor 区是空着的,就这样一直循环使用这三块内存区域

这种优化后的好处是,只有10%的内存空间是被闲置的,90%的内存都被使用上了。无论是垃圾回收的性能,内存碎片的控制,还是说内存使用的效率,都非常的好。

6. 思考题

  • 万一垃圾回收过后,存活下来的对象超过了10%的内存空间,在另外一块 Survivor 区域中放不下咋整?
  • 万一突然分配了一个超大的对象,新生代找不到连续内存空间来存放,咋整?
  • 到底一个存活对象要在新生代里来回倒腾多少次后才会被转移到老年代去?

7. 扩展知识

 

在上述代码中,如果发生垃圾回收,是不会回收 ReplicaFetcher 对象的。

因为ReplicaFetcher 对象被 ReplicaManager 对象中的实例变量引用了,然后 ReplicaManager 对象被 Kafka类的静态变量引用了。

所以垃圾回收的时候,是不会回收掉 ReplicaFetcher对象的。