目录
1. 死亡对象的判断算法
1.1 引用计数算法
1.2 可达性分析算法
2. 垃圾回收算法
2.1 标记——清除算法
2.2 复制算法
2.3 标记——整理算法
2.4 分代算法
对于支持垃圾回收机制的编程语言来说,常见的死亡对象的判断方法有引用计数算法和可达性分析算法两种,JVM的垃圾回收判断算法就是基于可达性分析来实现的。为了能够更好的理解垃圾回收机制,我们先对这两种死亡对象的判断算法进行分析。
引用计数算法就是指给引用对象额外新增一小片存储空间,用来保存引用这个对象的引用变量的个数,任何时刻当计数器变为0时,就可视为对象已死,我们的垃圾回收器就可以对其进行回收释放其占用的内存空间。
这种引用计数来判断对象是否可回收的算法原理和实现都相对容易,判定效率也高,比如python语言就采用了这种垃圾回收判定算法。
但是这种判定算法也会带来许多问题,其中最致命的两个问题有:空间利用率低和循环引用问题!所谓空间利用率低是指当我们的对应所占用的空间本就不大时,还需要额外开辟空间来保存引用的个数,这就造成了很严重的空间浪费问题。其二,当两个对象中的成员变量分别引用这两个对象,并且这两个对象的初始引用改变指向时,它们的引用计数器并不会归0,就像下面这段程序中的引用关系一样:
class Test { Test refer; } public class TestDrive { public static void main(String[] args) { Test test1 = new Test(); Test test2 = new Test(); test1.refer = test2; test2.refer = test1; test1 = null; test2 = null; } }
这个时候垃圾回收器就无法对这部分空间进行回收,我们画张图来理解下引用计数判断算法带来的循环引用问题。
为了规避引用计数算法判断死亡对象的致命缺陷,于是有了可达性分析判断算法。这种算法的核心思想为通过一系列GCRoots的对象作为起始点,从这些起始点开始进行类深度有点遍历,对所有能够访问到的节点对象进行标记,这样进行下来,没有被标记的对象就被视为不可达或者说没有可供使用的引用指向,这些没有被标记的对象就被视为死亡对象,最终被垃圾回收器进行回收,释放其占用的内存空间。
通过死亡对象判断算法我们已经能够找出待回收的冗余对象了,接下来我们来了解下长阿金的垃圾回收算法来更好的理解JVM的垃圾回收机制。常见的垃圾回收算法有以下几种:
标记清除算法是最基础的算法,分为标记和清除两个阶段,但标记清除算法有以下两个不足:
- 标记和清除的效率都不高
- 标记后的清除会产生大量不连续的内存碎片,导致以后在程序运行过程中需要分别较大空间的对象内存时,可能无法找到足够的连续内存。
复制算法是为了解决“标记-清理”算法的效率问题,它将内存区域划分为两块,每次只是用其中的一半。当其中一半需要进行垃圾回收时,会首先将该区域中还存活的对象拷贝到另一半内存区域,然后对这片内存区域进行全释放,一次性清理。这样做的好处就避免了产生大量内存碎片的问题,同时实现起来也相对简单,效率较高。
但是当存活的对象比较多时,这种复制算法需要进行大量的复制操作,降低程序的运行效率。
该垃圾回收算法的标记过程与“标记-清除”算法相同,不过在“清除”阶段采用了将所有存活对象向一侧靠拢,然后将存活对象端外的内存进行集体释放,这个过程如下图所示:
上述的几种算法各有千秋,我们的当代的JVM为了重复优化垃圾回收机制的性能,对上述的集中垃圾回收算法进行整合形成了新的回收算法——分代算法。这种算法是被当代JVM虚拟机所采用的垃圾回收算法,下边我们依次来看下这种算法的实现原理和执行过程吧!
所谓分代垃圾回收算法,其实就是按照对象的“年龄”进行进行分类(这里的年龄指的是没经过GC的依次扫描,年龄就长大一岁),并针对不同“年龄”的对象采用不同的回收措施以达到最优化回收的一种算法。
分代回收算法将内存划分为“新生代”和“老年代”两部分,其中,”新生代“区域又被分为“伊甸区”和两个“幸存区”部分,如下图所示:
- 新创建出来的对象,就会被放在“伊甸区”;
- 经过一轮GC扫描后,伊甸区中仍然存活的对象会被拷贝到“幸存区”当中(复制算法);
- 后续的几轮GC扫描,会将仍然存活的对象在两个”幸存者区“之间进行来回拷贝,每一轮都会回收掉一部分的死亡对象(复制算法);
- 在若干论扫描之后,仍然存活的对象会被放到老年代当中,经过前几轮的筛选,我们通常认为老年代中对象存活的可能性更大,存活的时间更长,因此,老年代这片内存区域被GC扫描的频率会低于新生代区域;
- 由于老年代中对象的存活概率大,使用复制算法就会导致开销太大,因此老年代内存区域中采用的的回收算法是标记——整理算法