本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力。
前言
在《# JVM 运行时堆内存如何分代?》中介绍了堆内存的分代划分以及垃圾回收的分代收集理论,里面介绍了分代是为了以不同的频率、使用不同的回收算法,进行垃圾回收,以达到效率和内存使用率的平衡。
这篇来介绍一下常见的垃圾回收算法。
「标记-清除」算法
简而言之,分为「标记」和「清除」两个阶段。首先,通过扫描标记出所有需要回收的对象,标记完成后,统一回收掉被标记的对象。也可以反过来,标记出存活的对象,然后回收未被标记的对象。
这个算法有两个缺点:
- 当堆中的对象数量较大,而且大部分都需要被回收时,就需要大量的标记和清除工作,导致这两个过程的执行效率随着对象数量的增长而降低。
- 每次标记和清除之后,会产生大量不连续的内存碎片,导致后续需要分配内存给较大对象时,难以找到足够的连续内存,这个时候就会提前触发新一轮的垃圾回收。
「标记-复制」算法
将可用的内存按容量划分为大小相等的两块,每次只使用其中一块。当使用中的一块内存用完了,就将其中需要继续存活的对象复制到另一块上,然后将以使用的内存空间全部清理掉。
对于大部分对象都需要回收的情况,这个算法只需要复制少数需要继续存活的对象即可,并且同时也解决了「标记-清除」算法存在的内存空间碎片问题。简单、高效。
它的缺点也显而易见:
- 可用的内存空间缩小为原来的一半,空间浪费较多。
- 如果内存中多数对象都是存活的,将产生大量的内存件复制开销。
实际上,「标记-清除」算法是大部分商用 JVM 采用的新生代回收算法。在 JVM 中,新生代的内存空间被分为三个区域:
一个 Eden 区域,两个 Survivors 区域。
当一个新的对象被创建的时候,会在 Eden 区域中划出一块作为存储对象的内存。当 Eden 区域空间耗尽的时候,虚拟机会触发一次 Minor GC,来收集新生代的垃圾对象。存活下来的,会被复制到 Survivor 区域。
Servivor 区域被分成了两块(可以看做「标记-复制」算法划分的两块内存区域),分别用 from 和 to 表示。其中 to 区域是空的。每当发证 Minor GC 的时候,Eden 区域和 from Servivor 区域中存活的对象,会被复制到 to Survivor 区域。然后 from 和 to 互换指针(也就是原来的 from 区域作为新的 to 区域),这样,下次进行 Minor GC 时,to 区域还是空的。
这就是新生代运用「标记-复制」算法完成垃圾回收的过程。在这个过程中,虚拟机会记录 Survivor 区域中对象被来回复制了几次,也就是对象在多少次 Minor GC 之后依然存活(这个次数可以理解为对象的「年龄」),当这个次数达到一定值之后,那么响应的对象会被晋升至老年代。
「标记-整理」算法
主要针对老年代对象的存亡特征。标记阶段与「标记-清除」算法相同,但是在标记完之后,不是将垃圾对象回收,而是让所有存活的对象都像内存空间的一端移动,然后再直接清理边界以外的内存空间。它是一种移动式的回收算法。
对于老年代的垃圾回收,它不像「标记-清除」算法一样会产生内存碎片,但是,对于老年代这种每次回收都有大量对象存活的区域,移动存活对象并更新引用是一个耗时的操作,并且这个过程中需要暂停用户程序。
比较折中的做法是,大多数时候,采用「标记-清除」算法,暂时容忍碎片的存在,当碎片程度影响到大对象的内存分配至,在用「标记-整理」算法收集一次,起到碎片内存整理的作用。