58.垃圾清除阶段-标记-清除算法、复制算法、标记-压缩算法
当成功区分出内存中的存活对象和死亡对象后,GC
接下来的任务就是垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用空间为新对象分配内存。
目前JVM
中有3
种常见的垃圾收集算法:标记-清除算法、复制算法、标记-压缩算法。
1.标记-清除算法
- 当堆中的有效内存空间被耗尽的时候,就会停止整个程序
(Stop the World)
,然后进行两项工作,第一项是标记,第二项是清除。
标记:Collector
从引用根节点开始变量,标记所有被引用的对象,也就是标记出可达对象,并在对象的Header助攻记录为可达对象。
清除:Collector
对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有被标记为可达对象,则将其回收。
绿色方块是存活对象,因为从根节点都可达,黑色方块是垃圾对象,白色方块是空闲空间。Collector
对堆内存从头到尾进行线性的遍历,对垃圾对象进行回收。
优缺点:
- 效率不算高。(标记阶段需要递归遍历找出可达对象,清除阶段需要线性遍历堆中所有对象,清除垃圾对象。)
- 在进行GC的时候,需要停止整个应用程序,导致用户体验差。
- 这种方式清理出来的内存空间是不连续的,产生内存碎片。需要维护一个空闲列表记录空闲空间有哪些。
何为清除?
清除并不是真的置空(也就是对象数据并没有被真正的删除),而是把需要清除的对象地址保存在空闲的地址列表里、下次有新的对象时,判断垃圾对象的位置空间是否够,够就直接存放。
2.复制算法
- 将活着的内存空间划分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
- 如下图所示,将内存区域划分为
A
,B
两个区域,A
区域是正在使用的内存,B
区是没有使用的内存区域。在寻找可达对象的时候,将可达对象直接复制到B
区,然后回收A
区中的所有对象。这时候B
区变成了正在使用的区域,A
区成为了空闲区域,B
区中的对象现在是线性排列的。
优点:
- 没有标记和清除过程,实现简单,运行高效
- 复制之后保证了 空间的连续性,不会出现“碎片”问题
缺点:
- 需要两倍的内存空间,有一块空间是没有使用的
- 复制对象之后,需要修改栈中对象的引用变量地址(因为对象被复制到了另外的空间),复制对象也会消耗不少的时间。
特别的,如果系统中的存活对象很多(极端情况下,所有的对象都是存活对象),复制算法如果进行复制的话,效率会很低。
应用:
新生代的对象基本上都是朝生夕死的,所以可达对象是很少的,非常适合复制算法。新生代里面的S0
区和S1
区,就应用了复制算法。
3.标记-压缩算法
- 老年代使用标记-压缩算法。压缩就是会进行碎片整理,回收之后的内存不存在碎片。
- 执行过程
第一阶段是标记阶段,和标记-清除算法一样,也就是递归遍历对象,找到可达对象,设置Header
里面的信息。
第二阶段是压缩阶段,将所有存活对象压缩到内存的一端,按顺序排放。之后清理剩余的空间。 - 标记-压缩算法可以理解成在标记-清除算法执行之后,再进行一次内存碎片整理。二者的本质差异在于标记-清除算法是一种非移动式的算法,而标记-压缩算法是移动式的。
- 可以看到,被标记的存活的对象会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当需要给新对象分配内存时,JVM只需要维护一个可用内存的起始地址就可以,比维护一个空闲列表少了许多开销。
优缺点: