美团一面:说下JVM的垃圾回收算法?

#每天一道面试题# 55 #悟空拧螺丝# 2021-09-08

问题:说下JVM的垃圾回收算法?

下面介绍七种回收算法:

1、可达性分析算法(标记阶段)

原理: 可达性分析算法是以根对象集合(GCRoots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。 虚拟机栈、本地方法栈、方法区、字符串常量池 等地方对堆空间进行引用的,都可以作为GC Roots进行可达性分析。

2、标记-清除算法(年轻代清除阶段)

原理: 当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除 标记:Collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。 清除:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收

缺点

  • 标记清除算法的效率不算高。
  • 在进行GC的时候,需要停止整个应用程序,用户体验较差。
  • 这种方式清理出来的空闲内存是不连续的,产生内碎片,需要维护一个空闲列表。

3、复制算法(年轻代清除阶段)

因为标记-清除算法的缺点,由此发明了复制算法。 原理: 将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。

优点: 复制过去以后保证空间的连续性,不会出现“碎片”问题。

缺点: 需要多余的内存空间。

4、标记整理算法(老年代清除阶段)

背景: 复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。

原理: 第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象。 第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界外所有的空间。

标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记-清除-压缩(Mark-Sweep-Compact)算法。

优点

消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。 消除了复制算法当中,内存减半的高额代价。

缺点: 从效率上来说,标记-整理算法要低于复制算法。 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址 移动过程中,需要全程暂停用户应用程序。即:STW。

5、分代收集算法

背景: 不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。

年轻代:复制算法 老年代:由标记-清除或者是标记-清除与标记-整理的混合实现。

6、增量收集算法

原理: 如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。

缺点: 使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。

7、分区算法(G1 收集器)

原理: 分区算法将整个堆空间划分成连续的不同小区间。 每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。