# JVM实战:常见的垃圾回收算法有哪些?

# 垃圾回收算法
# 标记-清除算法
这是最基础的垃圾回收算法,后续的很多算法都是基于它的缺点进行改进而来的
工作原理
从 GC Roots 开始遍历,先标记出所有需要回收的对象,在标记完成后统一回收被标记的对象。也可以反过来,标记存活的对象,统一回收未被标记的对象
优点
实现非常简单,不需要移动对象
缺点
- 效率问题,标记和清除的两个过程的效率都不高
- 内存碎片问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后再程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
# 标记-复制算法
为了解决“标记 - 清除”算法面对大量垃圾时的碎片化和效率问题,标记-复制算法应运而生
工作原理
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。
优点
- 没有内存碎片:复制过去的对象在新的内存块中是连续排列的
- 分配效率高:回收后只需移动堆顶指针,按顺序分配内存即可
缺点
- 内存利用率低:将可用内存缩小为了原来的一半,代价太高
- 不适合存活率高的区域:如果对象存活率很高,复制的操作就会非常频繁,导致效率变低
改善
JVM 并没有直接采用 1:1 的划分,而是对该算法进行了改良。将新生代分为一块较大的Eden空间和两块较小的Survivor空间(S0和S1),比例通常是8:1:1。
每次分配内存时只使用Eden和其中一块Survivor。发生垃圾收集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor上,接着清理掉Eden和已经用过的Survivor空间,循环往复。内存浪费就降低到了 10%
当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(大多就是老年代)进行分配担保
循环往复的过程如下
Eden+S0->S1(将Eden和S0存活的对象移动到S1)
Eden+S1->S0
Eden+S0->S1
# 标记-整理算法
标记-复制算法在对象存活率较高时(比如老年代)效率会变低,而且会浪费一半的空间。为此,针对老年代的特点提出了“标记 - 整理”算法
工作原理
标记过程仍然与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存
优点
- 无内存碎片:存活对象被紧凑地排列在了一起
- 内存利用率 100%:不需要像复制算法那样预留一半的空间,非常适合对象存活率高的老年代
缺点
移动对象(指针重定向)是一个比较重的操作,需要暂停用户线程(Stop The World,简称 STW),在对象存活率极高的情况下,移动对象的成本较高
# 分代收集
当代的 JVM 几乎都采用了分代收集。JVM 根据对象存活周期的不同,将堆内存划分为新生代和老年代
| 内存区域 | 对象特点 | 回收频率 | 适用算法 | 原因 |
|---|---|---|---|---|
| 新生代 | 朝生夕灭(只有少量存活) | 非常频繁(Minor GC) | 标记-复制算法 | 存活对象少,复制成本低,且无碎片 |
| 老年代 | 存活率高,没有额外担保空间 | 较低(Full GC / Major GC) | 标记-清除,标记-整理算法 。关注吞吐量用标记-整理算法,关注延迟用标记-清除算法 | 对象存活率高,没有额外空间为其做分配担保 |