深入理解JVM & G1 GC:深度解析七种垃圾收集算法!它们是如何实现的?原理是什么?

目前有两种比较常见的垃圾标记算法,分别是 引用计数算法根搜索算法 。引用计数器在微软的 COM 组件技术 中、 Adodb 的 ActionScript3 中都有使用。

一、引用计数法

引用计数法(Refernce Counting 在 GC 执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象, 才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为 垃圾标记阶段

引用计数器的实现很简单,对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,则对象A就不可能再被使用。也就是说,引用计数器的实现只需要为每个对象配置一个整形的计数器即可。引用计数器算法的一大优势就是不用等待内存不够用的时候,才进行垃圾的回收,完全可以在赋值操作的同时检查计数器是否为0,如果是的话就可以立即回收。

但是引用计数器有一个严重的问题,即无法处理循环引用的情况。一个简单的循环引用问题的描述如下 有对象A和对象B,对象A中含有对象B的引用,对象B中含有对象A的引用。此时,对象A和对象B的引用计数器都不为0,但是在系统中却不存在任何第3个对象引用了 。也就是说,A和B是应该被回收的垃圾对象,但由于垃圾对象间相互引用,从而使垃圾回收器无法识别,引起 内存泄漏

如下图所示,构造了一个列表,将最后一个元素的 next 属性指向第一个元素,即引用第一个元素,从而构成循环引用。这个时候如果将列表的头 head 赋值为 null,此时列表的各个元素的计数器都不为0,同时也失去了对列表的引用控制,从而导致列表元素不能被回收。


深入理解JVM & G1 GC:深度解析七种垃圾收集算法!它们是如何实现的?原理是什么?_第1张图片

引用计数器拥有一些特性,首先它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。其次,每次赋值都需要更新计数器,这增加了时间开销。再者,垃圾对象便于辨识,只要计数器为0,就可作为垃圾回收。接下来它能方便及时地回收垃圾,没有延迟性。最后不能解决循环引用的问题。正是由于最后一条致命缺陷,导致在 Java 的垃圾回收器中没有使用这类算法。


参考资料:《深入理解JVM & G1 GC》
本文文字内容偏多,添加助理VX:发送简信“JVM资料”免费获取

二、根搜索算法

Hotspot 和大部分 JVM 都是使用 根搜索算法 作为垃圾标记的算法实现。前面介绍过的引用计数算法尽管实现简单,执行效率也不错,但是该算法本身却存在一个较大的弊端,甚至会影响到垃圾标记的准确性。由于引用计数算法会为程序中的每一个对象都创建一个私有的引用计数器,当目标对象被其他存活对象引用时,引用计数器中的值则会加1,不再引用时便会减 1,当引用计数器中的值为0的时候,就意味着该对象己经不再被任何存活对象引用,可以被标记为垃圾对象。采用这种方式看起来似乎没有任何问题,但是如果一些明显已经死亡了的对象尽管没有被任何的存活对象引用,但是它们彼此之间却存在相互引用时,引用计数器中的值则永远不会为0,这样便会导致 GC 在执行内存回收时永远无法释放掉这种无用对象所占用的内存空间,极有可能引发内存泄漏。

相对于引用计数算法而言,根搜索算法不仅同样具备 实现简单执行高效 等特点,更重要的是该算法可以有效地解决在引用计数算法中一些己经死亡的对象因相互引用而导致的无法正确被标记的问题,防止内存泄漏的发生。简单来说,根搜索算法是以根对象集合为起始点按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达(使用根搜索算法后,内存中的存活对象都会被根对象集合直接或间接连接着),如果目标对象不可达,就意味着该对象己经死亡,便可以在 instanceOopDesc Mark World 中将其标记为垃圾对象。在根搜索算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。在 Hotspot 中,根对象集合中包含了5个元素, Java 栈内的对象引用、本地方法栈内的对象引用、运行时常量池中的对象引用、方法区中类静态属性的对象引用以及与 个类对应的唯一数据类型的 Class 对象。

注解①Hotspot 代码中用 instanceOopDesc 来表示 Java 对象,而该类继承 oopDesc ,所以 Hotspot 中的 Java 对象 也自然拥有 oopDesc 所声明的头部。

注意,在根搜索算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程。如果对象在进行根搜索后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记井且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法。当对象没有覆盖 finalize() 方法,或者 finalize() 方法 已经被虚拟机调用过,虚拟机将这两种情况都视为 “ 没有必要执行 ” 。如果这个对象被判定为有必要执行 finalize() 方法,那么这个对象将会被放置在一个名为 F-Queue 的队列之中,并在稍后由一条由虚拟机自动建立的、低优先级的 Finalizer 线程 去执行。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。这样做的原因是,如果 个对象在 finalize()方法 中执行缓慢,或者发生了死循环(更极端的情况),很可能会导致 F-Queue 队列 中的其他对象永久处于等待状态,甚至导致整个内存回收系统崩溃。 finalize() 方法 是对象逃脱死亡命运的最后一次机会,稍后 GC 将对 F- Queue 中的对象进行第二次小规模的标记,如果对象要在 finalize () 中成功拯救自己一一只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this 关键字)赋值给某个类变量或对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合。如果对象这时候还没有逃脱,那它就真的离死不远了。从下面的代码中可以看到一个对象的 finalize ()被执行,但是它仍然可以存活。

逃脱回收实验

public class FinalizeEscapeGC { 
    public static FinalizeEscapeGC SAVE_HOOK = null; 
    public void isAlive() { 
        System.out.println("yes, i am still alive");
    }
    
    @Override 
    protected void finalize() throws Throwable { 
        super.finalize() ; 
        System.out.println("finalize mehtod executed !"); 
        FinalizeEscapeGC.SAVE_HOOK = this; 
    }
    
    public static void main(String[] args) throws Throwable { 
        SAVE_HOOK = new FinalizeEscapeGC (); 
        //对象第 次成功拯救自己
        SAVE_HOOK = null ; 
        System.gc(); 
        //因为 Finalizer 方法优先级很低,暂停 秒,以等待它
        Thread.sleep(5OO); 
        if (SAVE_HOOK != null) { 
            SAVE_HOOK.isAlive() ; 
        } else {
            System.out.println("no , i am dead"); 
        }
        //下面这段代码与上面的完全相同 ,但是这次自救却失败了
        SAVE_HOOK = null ; 
        System.gc(); 
        //因为 Finalizer 方法优先级很低,暂停 0.5 秒,以等待它
        Thread .sleep(500) ; 
        if (SAVE_HOOK ! = null) { 
            SAVE_HOOK.isAlive(); 
        } else { 
            System.out.println("no, i am dead");
        }
    }
}

输出如下面代码所示:逃脱回收实验运行输出

finalize mehtod executed! 
yes, i am still alive 
no , i am dead

从上面代码的运行结果可以看到, SAVE_HOOK 对象finalize() 方法 确实被 GC 收集器 触发过,并且在被收集前成功逃脱了。另外一个值得注意的地方就是,代码中有两段完全样的代码片段,执行结果却是一次逃脱成功,一次失败,这是因为任何一个对象的 finalize() 方法 都只会被系统自动调用一次,如果对象面临下一次回收,它的 finalize() 方法 不会被再次执行,因此第2段代码的自救行动失败了。

三、标记·清除算法( Mark-Sweep )

当成功区分出内存中存活对象和死亡对象后, GC 接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。目前在 JVM中比较常见的三种垃圾收集算法是 标记-清除算法( Mark-Sweep )复制算法( Copying )标记-压缩算法( Mark-Compact )。在介绍三种算法之前,我们先来通过下图看看它们之间的区别。

标记-清除算法( Mark-Sweep ) 是一种非常基础和常见的垃圾收集算法,该算法被 J.McCarthy 等人在 1960 年提出并成功地发明井应用于 Lisp 语言。以餐巾纸作为示例, 午餐过程中,餐厅的所有人都根据自己的需要取用餐巾纸。 当垃圾收集机器人想收集废旧餐巾纸的时候,它让所有用餐的人先停下来,然后,依次询问餐厅里的每一个人:“你正在用餐巾纸吗?你用的是哪一张餐巾纸?”机器人根据每个人的回答将人们正在使用的餐巾纸画上记号。询问过程结束后,机器人在餐厅里寻找所有散落在餐桌上且没有记号的餐巾纸(这些显然都是用过的废旧餐巾纸),把它们统统扔到垃圾箱里。

回到算法本身。算法涉及几个概念,先来了解 mutatorcollector ,这两个名词经常在垃圾收集算法中出现, collector 指的就是 垃圾收集器 ,而 mutator 是指 除了垃圾收集器之外的部分 ,比如说我们的应用程序本身。 mutator 的职责一般是 NEW (分配内存)、READ (从内存中读取内容)WRITE (将内容写入内存),而 collector 就是回收不再使用的内存来供 mutator 进行 NEW 操作的使用。 mutator 根对象 一般指的是分配在堆内存之外,可以直接被 mutator 直接访问到的对象,一般是指静态/全局变量以及 ThreadLocal 变量。

注解②: Java 中,存储在 Java .lang.ThreadLocal 中的变量和分配在枝上的变量方法内部的临时变量等都属于此类。

标记-清除算法 将垃圾回收分为两个阶段,标记阶段清除阶段 。在标记阶段, collectormutator 根对象 开始进行遍历,对从 mutator 根对象 可以访问到的对象都打上一个标识,一般是在对象的 header 中,将其记录为可达对象。而在清除阶段, collector 对堆内存( heap memory ) 从头到尾进行线性的遍历,如果发现某个对象没有标记为可达对象,通过读取对象的 header 信息,则将其回收。一种可行的实现是,在标记阶段首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。

前面说过,标记-清除算法 的执行过程分为 “标记”“清除” 两大阶段。这种分步执行的思路奠定了现代垃圾收集算法的思想基础。与引用计数算法不同的是,标记-清除算法不需要运行环境监测每一次内存分配和指针操作,而只要在“标记”阶段中跟踪每一个指针变量的指向,用类似思路实现的垃圾收集器也常被后人统称为 跟踪收集器Tracing Collector )。

标记-清除算法 最大的问题是存在大量的空间碎片,因为回收后的空间是不连续的。在对象的堆空间分配过程中,尤其是大对象的内存分配,不连续的内存空间的工作效率要低于连续的空间。

相对于另外两种内存回收算法而 标记-清除算法(Mark-Sweep) 不仅 执行效率低下,更重要的是,由于被执行内存回收的无用对象所占用的内存空间有可能是一些不连续的内存块,不可避免地会产生一些内存碎片,从而导致后续没有足够的可用内存空间分配给较大的对象。

四、复制算法 (Copying)

为了解决 标记-清除算法 在垃圾收集效率方面的缺陷,M.L.Minsky 于1963 年发表了著名的论文, “一种使用双存储区的 Lisp 语言垃圾收集器 (A LISP Garbage Collector Algorithm Using Serial Secondary Storage )” 。M.L.Minsky 在该论文中描述的算法被人们称为 复制算法,它也被 M.L.Minsky 本人成功地引入到了 Lisp 个实现版本中。

标记-清除算法 后来被引入 JVM 中,提升了 GC 在垃圾标记和内存释放这两个阶段的执行效率。还是采用之前的餐厅示例,餐厅被垃圾收集机器人分成南区和北区两个大小完全相同的部分。午餐时,所有人都先在南区用餐(因为空间有限,用餐人数自然也将减少一半) ,用餐时可以随意使用餐巾纸。当垃圾收集机器人认为有必要回收废旧餐巾纸时,它会要求所有用餐者以最快的速度从南区转移到北区,同时随身携带自己正在使用的餐巾纸。等所有人都转移到北区之后,垃圾收集机器人只要简单地把南区中所有散落的餐巾纸扔进垃圾箱就算完成任务了。下一次垃圾收集的工作过程也大致类似,唯一的不同只是人们的转移方向变成了从北区到南区。如此循环往复,每次垃圾收集都只需简单地转移(也就是复制)一次,垃圾收集速度无与伦比一一当然,对于用餐者往返奔波于南北两区之间的辛劳 ,垃圾收集机器人是绝不会流露出丝毫怜悯的。

回到算法本身。复制算法首先将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。

如果系统中的垃圾对象很多,复制算法需要复制的存活对象数量并不会太大。因此在真正需要垃圾回收的时刻,复制算法的效率是很高的。又由于对象在垃圾回收过程中统一被复制到新的内存空间中,因此,可确保回收后的内存空间是没有碎片的。该算法的缺点是将系统内存折半

Java 年轻代串行垃圾回收器中使用了复制算法的思想。年轻代分为 Eden 空间From 空间To 空间 3个部分。其中 From 空间To 空间 可以视为用于复制的两块大小相同、地位相等,且可进行角色互换的空间块。 From To 空间 也称为 Survivor 空间,即 幸存者空间,用于存放未被回收的对象

在垃圾回收时, Eden 空间 中的存活对象会被复制到未使用的 Survivor 空间 中(假设是 To),正在使用的 Survivor 空间(假设是 From )中的年轻对象也会被复制到 To 空间中(大对象,或者老年对象会直接进入老年代,如果 To 空间己满,则对象也会直接进入老年代)。此时, Eden 空间From 空间 中的剩余对象就是垃圾对象,可以直接清空, To 空间则存放此次回收后的存活对象。这种改进的复制算法既保证了空间的连续性,又避免了大量的内存空间浪费。

基于分代的概念, Java 堆区如果还要更进一步细分的话,还可以划分为年轻代( YoungGen)老年代( OldGen ),其中年轻代又可以被划分为 Eden 空间From Survivor 空间To Survivor 空间。在 HotSpot 中, Eden 空间 和另外两个 Survivor 空间 默认所占的比例是8:1, 当然开发员可以通过选项“-XX SurvivorRatio ”调整这个空间比例 。当执行一次 MinorGC (年轻代的垃圾回收)时, Eden 空间中的存活对象会被复制到 To 空间内,井且之前已经经历过一次 MinorGC 并在 From 空间中存活下来的对象如果还年轻的话同样也会被复制到 To 空间内。需要注意的是,在满足两种特殊情况下, Eden From 空间 中的存活对象将不会被复制到 To 空间内。首先是如果存活对象的分代年龄超过选项“ XX: MaxTenuringThreshold ”所指定的阀值时,将会直接晋升到老年代中。其次当 To 空间的容量达到阀值时,存活对象同样也是直接晋升到老年代中当所有的存活对象都被复制到 To 空间或者晋升到老年代后,剩下的均为垃圾对象,这就意味GC 可以对这些已经死亡了的对象执行一次 MinorGC ,释放掉其所占用的内存空间。

当执行完 Minor GC 之后, Eden 空间From 空间 将会被清空,而存活下来的对象则会被全部存储在 To 空间内 ,接下来 From 空间和 To 空间将会互换位置。其实复制算法无非就是使 To Survivor 空间作为一个临时的空间交换角色,务必需要保证两块空间中一块必须是空的,这就是复制算法。尽管复制算法能够高效执行 MinorGC ,但是它却并不适用于老年代中的内存回收,因为老年代中对象的生命周期都比较长,甚至在某些极端的情况下还能够与 JVM 的生命周期保持一致,所以如果老年代也采用复制算法执行内存回收,不仅需要额外的时间和空间,而且还会导致较多的复制操作影响到 GC 的执行效率。

总的来说,由于 JVM 中的绝大多数对象都是瞬时状态的,生命周期非常短暂,所以复制算法被广泛应用于年轻代中。分区、复制的思路不仅大幅提高了垃圾收集的效率,而且也将原本繁纷复杂的内存分配算法变得前所未有的简明和扼要(既然每次内存回收都是对整个半区的回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存就可以了),这简直是个奇迹!不过,任何奇迹的出现都有一定的代价,在垃圾收集技术中,复制算法提高效率的代价是人为地将可用内存缩小了一半。

五、标记-压缩算法 (Mark-Compact)

标记-清除算法 的确可以应用在老年代中,但是该算法不仅执行效率低下,而且在执行完内存回收后还会产生内存碎片,所以 JVM 的设计者在此基础之上进行了改进,标记-压缩算法 由此诞生。标记-压缩算法标记-清除算法复制算法 的有机结合。把标记-清除算法在内存占用上的优点和复制算法在执行效率上的特长综合起来,这是所有人都希望看到的结果。不过,两种垃圾收集算法的整合并不像 1加1 等于2 那样简单,必须引入一些全新的思路。

1970 年前后, G. L. Steele、C. J. Chene 和 D.S. Wise 等研究者陆续找到了正确的方向,标记-压缩算法 的轮廓也逐渐清晰了起来。还是采用之前的餐厅示例,在我们熟悉的餐厅里,这一次,垃圾收集机器人不再把餐厅分成两个南北区域了。需要执行垃圾收集任务时,机器人先执行 标记-清除算法 的第一个步骤,为所有使用中的餐巾纸画好标记,然后,机器人命令所有就餐者带上有标记的餐巾纸向餐厅的南面集中,同时把没有标记的废旧餐巾纸扔向餐厅北面。这样来,机器人只需要站在餐厅北面,怀抱垃圾箱,迎接扑面而来的废旧餐巾纸就行了。

回到算法本身。 成功标记出内存中的垃圾对象后,标记-压缩算法 会将所有的存活对象都移动到一个规整且连续的内存空间中,然后执行 Full GC (老年代的垃圾回收,或者被称为 or GC ) 回收无用对象所占用的内存空间。当成功执行压缩之后,己用和未用的内存都各自一边,彼此之间维系着一个记录下一次分配起始点的标记指针,当为新对象分配内存时,则可以使用 指针碰撞( Bump the Pointer 技术修改指针的偏移量将新对象分配在第一个空闲内存位置上,为新对象分配内存带来便捷。

HotSpot 中,基于分代的概念, GC 所使用的内存回收算法必须结合年轻代和老年代各自的特点。简单来说,就是针对不同的代空间,从而结合使用不同的垃圾收集算法。为年轻代选择的垃圾收集算法通常是以速度优先,因为年轻代中所存储的瞬时对象生命周期非常短暂,可以有针对性地使用 复制算法,因此执行 Minor GC 时, 一定要保持高效和快速。而年轻代中的生存空间通常都比较小,所以回收年轻代时一定会非常频繁。但老年代通常使用更节省内存的回收算法,因为老年代中所存储的对象生命周期都非常长,并且老年代占据了大部分的堆空间,所以老年代的 Full GC 并不会跟年轻代的 Minor GC 一样频繁,不过一旦程序中发生一次 Full GC ,将会耗费更长的时间来完成,那么在老年代中使用标记-清除算法或者标记-压缩算法执行垃圾回收将会是不错的选择。

复制算法 的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在年轻代经常发生,但是在老年代更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活的对象较多,复制的成本也将很高。标记-压缩算法 是一种老年代的回收算法,它在 标记-清除算法 的基础上做了一些优化。也首先需要从根节点开始对所有可达对象做一次标记,但之后,它并不是简单地清理未标记的对象,而是将所有的存活对象压缩到内存的 端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。

标记-压缩算法 的总体执行效率高于 标记-清除算法,又不像 复制算法 那样需要牺牲一半的存储空间,这显然是一种非常理想的结果。在许多现代的垃圾收集器中,人们都使用了 标记-压缩算法 或其改进版本。

六、增量算法 (Incremental Collecting )

在垃圾回收过程中,应用软件将处于一种 Stop the World 的状态。在 Stop the World 状态下,应用程序所有的线程都会挂起,暂停一切正常的工作,等待垃圾回收的完成。如果垃圾回收时间过长,应用程序会被挂起很久,将严重影响用户体验或者系统的稳定性。为了解决这个问题,即对实时垃圾收集算法的研究直接导致了增量收集算法的诞生。

最初,为了进行实时垃圾收集,可以设计一个多进程的运行环境,比如用一个进程执行垃
圾收集工作,另一个进程执行程序代码。这样一来,垃圾收集工作看上去就仿佛是在后台悄悄完成的,不会打断程序代码的运行。在收集餐巾纸的例子中,这一思路可以被理解为垃圾收集机器人在人们用餐的同时寻找废弃的餐巾纸并将它们扔到垃圾箱里。这个看似简单的思路会在设计和实现时碰上进程间冲突的难题。比如说,如果垃圾收集进程包括标记和清除两个工作阶段,那么,垃圾收集器在第一阶段中辛辛苦苦标记出的结果很可能被另一个进程中的内存操作代码修改得面目全非,以至于第二阶段的工作没有办法开展。

M. L. Minsky 和 D. E. Knuth 对实时垃圾收集过程中的技术难点进行了早期的研究, G. L. Steele 1975 年发表了题为 “多进程整理的垃圾收集 Multiproc sing Compactifying Garbage Collection ” 的论文,描述了一种被后人称为 “Minsky-Knuth-Steele 算法” 的实时垃圾收集算法。 E.W.Dijkstra、L.Lamport、R.R.Fenichel 和 J.C.Yochelson 等人也相继在此领域做出了各自的贡献。 1978 年, H.G .Baker 发表了 “串行计算机上的实时表处理技术 (List Processing in Real Time on a Serial Computer)” 一文,系统阐述了多进程环境下用于垃圾收集的 增量收集算法

增量算法 的基本思想是,如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让 垃圾收集线程应用程序线程 交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。

总的来说,增量收集算法的基础仍是传统的标记 清除和复制算法。增量收集算法通过对进
程间冲突的妥善处理,允许垃圾收集进程以分阶段的方式完成标记、清理或复制工作。

七、分代收集算法 (Generational Collecting)

1980 年前后,善于在研究中使用统计分析知识的技术人员发现,大多数内存块的生存周期都比较短,垃圾收集器应当把更多的精力放在检查和清理新分配的内存块上。还是餐巾纸的例子,如果垃圾收集机器人足够聪明,事先摸清了餐厅里每个人在用餐时使用餐巾纸的习惯一一比如有些人喜欢在用餐前后各用掉一张餐巾纸,有的人喜欢自始至终攥着一张餐巾纸不放,有的人则每打一个喷嗖就去用一张餐巾纸一一机器人就可以制定出更完善的餐巾纸回收计划,并总是在人们刚扔掉餐巾纸没多久就把垃圾捡走。这种基于统计学原理的做法当然可以让餐厅的整洁度成倍提高。D. E. Knuth、T. Knight、G. Sussman 和 R. Stallman 等人对内存垃圾的分类处理做了最早的研究。 1983 年, H. Lieberman 和 C.Hewitt 发表了题为 “基于对象寿命的一种实时垃圾收集器( Real-Time Garbage Collector Based on the Lifetimes of ects ” 的论文。这篇著名的论文标志着分代收集算法的正式诞生。此后,在 H. G. Baker、R. L. Hudson、 J.E. B. Moss 等人的共同努力下,分代收集算法逐渐成为了垃圾收集领域里的主流技术。

根据垃圾回收对象的特性,使用合适的算法回收,分代就是基于这种思想。它将内存区间根据对象的特点分成几块,根据每块内存区间的特点,使用不同的回收算法以提高垃圾回收的效率。

Hotspot 虚拟机 为例,它将所有的新建对象都放入称为年轻代的内存区域,年轻代的特点是对象会很快回收,因此,在年轻代就选择效率较高的复制算法。当一个对象经过几次回收后依然存活,对象就会被放入称为老年代的内存空间。在老年代中,几乎所有的对象都是经过几次垃圾回收后依然得以幸存的。因此,可以认为这些对象在一段时期内,甚至在应用程序的整个生命周期中,将是常驻内存的。如果依然使用复制算法回收老年代,将需要复制大量对象。再加上老年代的回收性价比也要低于年轻代,因此这种做法也是不可取的。根据分代的思想,可以对老年代的回收使用与年轻代不同的 标记-压缩算法,以提高垃圾回收效率。

总的来说,分代收集算法是基于对对象生命周期分析后得出的垃圾回收算法。它把对象分为年轻代、老年代、持久代,对不同生命周期的对象使用不同的算法(上述方式中的一个)进行回收。 JVM 垃圾回收器(从 J2SE1.2 开始)都是使用此算法的。


参考资料:《深入理解JVM & G1 GC》
本文文字内容偏多,添加助理VX:发送简信“JVM资料”免费获取

你可能感兴趣的:(深入理解JVM & G1 GC:深度解析七种垃圾收集算法!它们是如何实现的?原理是什么?)