前情提要,我们知道内存空间是有限的,对象存放在内存中,随着对象不断的增加,内存空间总会有占满的一天。这时候,就需要对内存空间进行管理,将已经没用的对象进行回收,清理出足够的空间存放新的对象。
那么,JVM 对于垃圾对象是怎么回收呢?本文将简述几种垃圾回收算法,废话少说,开始发车!
标记-清除算法
这是最基础的,也是最容易实现垃圾回收算法,它分为两个步骤:
标记 和 清除。
标记:标记出需要被回收的对象
清除:将被标记的对象进行回收。
下面用一张图演示整个垃圾回收过程:
图1
那么,标记-清除算法有什么优缺点呢?
优点:简单!
缺点:
-
标记和清除需要经历了两轮遍历,效率比较低。
-
清除垃圾之后,将会产生大量的 不连续 的碎片空间。
这里有一个疑问,大量的不连续的碎片空间有什么坏处呢?
以图1 清除后 的内存空间为例,分以下几种情况分析:
(1)需要分配3格的空间
首先,我们要知道,这种算法模式下,JVM 会维护一个空闲列表,记录空闲空间的位置,分配空间的时候依据这个列表来查找空闲空间。
当需要分配3格空间的时候,从头遍历空闲列表,只有当遍历到最后,才能找到足够的空间。
像这种需要遍历到最后才能找到合适空间的情况,这也是一种开销。
(2)需要分配5格空间
需要分配5格空间时,从头到尾遍历空闲列表,并没有找到足够的空间。
图1所示,清除后的空闲空间的总量超过5格,然而不存在一段连续的5格空间,因为遍历后找不到足够的空间存放。
那么,这时候不得不触发一次垃圾回收,以获取足够的空间。
这种情况就像房间不进行整理,东西随处乱放,即使剩余很多实际空间,当需要放进大物件的时候,找不到一块合适的地方放置,只能对房间进行清理或整理,腾出足够的空间。
标记-压缩算法
标记-清除算法 最明显的一个缺点就是产生大量的 不连续 的碎片空间,针对这个缺点进行优化改进,在标记-清除的中间,添加 压缩 操作,对存活对象进行 整理,目的是为了将空间空间进行整合,这就是标记-压缩算法。
下面还是用一张图演示整个垃圾回收过程:
图2
压缩 主要是将所有存活的对象都移动到一端,然而,对象之间基本上存在引用,例如 4 引用了 8,这意味着 4 需要存储 8 的地址,每次 8 进行移动,4 都需要重新存储 8 的地址。
因此,压缩并非简单的往前移动,压缩之前,需要先计算每个存活对象的新地址,这就需要多次遍历存活对象,避免出现压缩过程中出现覆盖存活对象的情况。
例如,上图整理后的结果,其实中间省略了复杂的计算,并非 1、4、8 按顺序简单的往前移动。
那么,标记-压缩算法有什么优缺点呢?
优点:
- 解决了 标记-清除算法 会产生碎片空间的问题。
缺点:
- 压缩 需要移动存活对象到一端,而且保证移动的位置不会覆盖存活对象,就必须通过计算找到合适的位置,这就产生了额外的开销。
标记-复制算法
同样是针对 标记-清除算法 产生大量的 不连续 的碎片空间的缺点,不同于 标记-压缩算法,标记-复制算法 将内存空间任性的分为两块区域,两块区域交替使用。因此,实际投入使用的只有其中一块区域,即总空间的一半。
下面还是用一张图演示整个垃圾回收过程:
图3
如图3所示,内存空间被分为 A区 和 B区 两块区域,每次只使用其中一块区域(例如A区),当空间不足需要垃圾回收时,将 A区 的存活对象复制到 B区 的一端后,直接清空 A区 的空间。
当空间再次不足的时候,重复以上的操作,两个区域替换使用。
那么,标记-复制算法有什么优缺点呢?
优点:
- 相对标记-压缩算法,不需要对存活对象进行多次遍历,计算新地址,直接复制到另一个区域就可以了。
缺点:
-
只能使用一半的内存空间,空间利用率低。
-
当存活对象较多的时候,需要进行大量的复制操作,效率下降。
标记-压缩 和 标记-复制 之间的区别,可以想象成房间需要添置新的家具,可是没有一块完整的空间来放置,需要我们对房间的布局进行整理。
标记-压缩:就是我只有一间房,整个房间进行重新布局,首先做好详细的规划,目标是把所有家具都放得更加紧凑,尽量将腾出的空间集中在一块,然后把新家具放置进去。
标记-复制:就是我有两间房,我不想动脑做计划,每次就直接将东西搬到另一间房,所有家具按顺序排放,剩下的空间就可以放置新家具。
再说一句
标记-清除 作为最简单的垃圾回收算法,最致命的缺点就是产生大量的碎片空间,针对这个缺点的优化,演变出 标记-压缩 和 标记-复制 这两种算法。
然而,标记-压缩 和 标记-复制 各自的优缺点都相对比较极端,从时间和空间的角度,标记-压缩牺牲了时间,标记-复制牺牲了空间。
那么,是否有一种算法能够平衡这两者的优缺点,达到更高的效率呢?没错,就是分代算法。它根据对象生命周期的特征,将内存空间合理的划分出不同的区域,建立相关的规则,更加高效的管理内存空间。
下一篇文章,将会带各位了解 分代算法 的奥妙之处,敬请期待。