垃圾收集算法有哪些?以及它们各自的优缺点

4,572 阅读8分钟

人生一切难题,知识给你答案

温馨提示:阅读本文需要5-6分钟(无代码)
来源:《深入理解Java虚拟机》


今天,我们来解决一个问题:

垃圾收集算法有哪些?以及它们各自的优缺点

人生一切难题,知识给你答案。


任何垃圾收集算法都必须做两件事,首先必须检测出垃圾对象。其次,它必须回收垃圾对象所使用的堆空间并还给程序。

垃圾检测通常通过建立一个根对象的集合以及建立一个从这些根对象开始能够触及的对象集合来实现。如果正在执行的程序可以访问到根对象和某个对象之间存在引用路径,这个对象就是可触及的。对于程序来说,根对象总是可以访问的。从这些根对象开始,任何可以被触及的对象都被认为是“活动”的对象。无法被触及的对象被认为是垃圾。

虚拟机的根对象集合根据实现不同而不同,包含局部变量中的对象引用和栈帧的操作数栈(以及类变量中的对象引用)、被加载的类的常量池中的对象引用(比如字符串)、传递到本地方法中的没有被本地方法释放的对象引用。任何被根对象引用的对象都是可触及的,从而是活动的,任何被活动的对象引用的对象都是可触及的,程序可以访问任何可触及的对象,所以这些对象应用留在堆中,而对于那些不可触及的对象,程序没有办法访问它们,应该被收集和释放。

区分活动对象和垃圾的两个基本方法是引用计数和跟踪。引用计数垃圾收集器通过为堆中的每个对象保存一个计数来区分活动对象和垃圾对象。这个计数记录下这个对象的引用次数。跟踪垃圾收集器实际上追踪从根节点开始的引用图。在追踪中遇上的对象以某种方式打上标记,当追踪结束时,没有被打上标记的对象就被判定是不可触及的,可以被当作垃圾收集。

==引用计数算法==

给对象添加一个引用计数器,每当一个地方引用它时,数据器加1;当引用失效时,计数器减1;计数器为0的即可被回收。

  • 优点:实现简单,判断效率高
  • 缺点:很难解决对象之间的相互循环引用(objA.instance = objB; objB.instance = objA)的问题,所以java语言并没有选用引用计数法管理内存

==根搜索算法==

Java和C#都是使用根搜索算法来判断对象是否存活。通过一系列的名为“GC Root”的对象作为起始点,从这些节点开始向下搜索,搜索所有走过的路径称为引用链(Reference Chain),当一个对象到GC Root没有任何引用链相连时(用图论来说就是GC Root到这个对象不可达时),证明该对象是可以被回收的。

在Java中这些对象可以成为GC Root:

  • 虚拟机栈(栈帧中的本地变量表)中的引用对象
  • 方法区中的类静态属性引用的对象
  • 方法区中的常量引用对象
  • 本地方法栈中JNI(即Native方法)的引用对象

==标记-清除算法==

标记-清除算法是一种常见的基础垃圾收集算法,它将垃圾收集分为两个阶段

  • 标记阶段:标记出可以回收的对象。
  • 清除阶段:回收被标记的对象所占用的空间。

标记-清除算法主要有两个缺点,一个是标记和清除的效率不高,另一个从图中就可以看出,就是容易产生大量不连续的内存碎片,碎片太多可能会导致后续没有足够的连续内存分配给较大的对象,从而提前触发新的一次垃圾收集动作。

==复制算法==

为了解决标记-清除算法的效率不高的问题,产生了复制算法。它把内存空间划分为两个相等的区域,每次只使用其中一个区域。在垃圾收集时,遍历当前使用的区域,把存活对象复制到另一个区域中,最后将当前使用的区域的可回收的对象进行回收。

这种算法每次都对整个半区进行内存回收,不需要考虑内存碎片的问题,代价就是使用内存为原来的一半。复制算法的效率与存活对象的数目多少有很大的关系,如果存活对象很少,复制算法的效率就会很高。由于绝大多数对象的生命周期很短,并且这些生命周期很短的对象都存于新生代中,所以复制算法被广泛应用于新生代中。

==标记-压缩算法==

在新生代中可以使用复制算法,但是在老年代就不能选择复制算法,因为老年代对象存活率会较高,这样会有较多的复制操作,导致效率变低。标记-清除算法可以应用在老年代中,但是效率不高,在内存回收后容易产生大量内存碎片。因此就出现了一种标记-压缩算法,与标记-清除算法不同的是,在标记可回收的对象后将所有存活的对象压缩到内存的一端,使它们紧凑地排列在一起,然后对边界以外的内存进行回收,回收后,已用和未用的内存都各自一边。

标记-压缩算法解决了标记-清除算法效率低和容易产生大量内存碎片的问题,它被广泛应用于老年代中。

==分代收集算法==

分代收集算法会结合不同的收集算法来处理不同的空间,因此在学习分代收集算法之前我们首先要了解Java堆区的空间划分。Java堆区的空间划分在Java虚拟机中,各种对象的生命周期会有着较大的差别,大部分对象生命周期很短暂,少部分对象生命周期很长,有的甚至与应用程序以及Java虚拟机的运行周期一样长。因此,应该对不同生命周期的对象采取不同的收集策略,根据生命周期长短将它们放到不同的区域,并在不同的区域采用不同的收集算法,这就是分代的概念。现在主流的Java虚拟机的垃圾收集器都采用分代收集算法。Java堆区基于分代的概念,分为新生代和老年代,其中新生代再细分为Eden空间、From Survivor空间和To Survivor空间。因为Eden空间中的大多数生命周期很短,所以新生代的空间划分并不是均分的,HotSpot虚拟机默认Eden空间和两个Survivor空间的所占的比例为8:1。

根据Java堆区的空间划分,垃圾收集的类型分为两种,它们分别如下:

  • Minor Collection:新生代垃圾收集。
  • Full Collection:对老年代进行收集,又可以称作Major Collection,Full Collection通常情况下会伴随至少一次的Minor Collection,它的收集频率较低,耗时较长。

当执行一次Minor Collection时,Eden空间的存活对象会被复制到To Survivor空间,并且之前经过一次Minor Collection 并在From Survivor空间存活的仍年轻的对象也会复制到To Survivor空间。有两种情况Eden空间和From Survivor空间存活的对象不会复制到To Survivor空间, 而是晋升到老年代。一种是存活的对象的分代年龄超过-XX:MaxTenuringThreshold(用于控制对象经历多少次Minor GC 才晋升到老年代)所指定的阈值。另一种是To Survivor空间容量达到阈值。当所有存活的对象被复制到To Survivor空间,或者晋升到老年代,也就意味着Eden空间和From Survivor空间剩下的都是可回收对象。

这个时候GC执行Minor Collection,Eden空间和From Survivor空间都会被清空,新生代存活的对象都存放在To Survivor空间。接下来将From Survivor空间和To Survivor空间互换位置,也就是此前的From Survivor空间成为了现在的To Survivor空间,每次Survivor空间互换都要保证To Survivor空间是空的,这就是复制算法在新生代中的应用。在老年代则会采用标记-压缩算法或标记-清除算法。


838794-506ddad529df4cd4.webp.jpg