JVM(十二)垃圾清除阶段算法
-
垃圾清除阶段是指,当成功区分出内存区域中的存活对象和死亡对象之后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。
-
目前在JVM中比较常见的三种垃圾收集算法是
标记-清除算法(Mark-Sweep)
、复制算法(Copying)
、标记压缩算法(Mark-Compact)
1 标记-清除算法(Mark-Sweep)
-
标记-清除算法(Mark-Sweep)
是一种非常基础和常见的垃圾收集算法 -
当堆中的有效内存空间被耗尽的时候,就会停止整个程序(Stop The World),然后进行两项工作,第一项是标记,第二项是清除:
-
标记
:Collector从引用根节点开始遍历,标记所有被引用的对象,一般是在对象的Header
中记录为可达对象(递归遍历部分对象) -
清除
:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有被标记为可达对象,则将其回收(线性遍历全部对象)这里的清除并不是真的置空清除,而是把需要清除的对象地址保存在空闲的地址列表里面,当下次有新的对象加载的时候,就判断垃圾的位置空间是否足够,如果够就存放,本质是覆盖原有垃圾对象
-
-
标记-清除算法(Mark-Sweep)
的缺点:- 效率不算高
- 在进行GC的时候,需要停止整个应用程序,导致用户的体验差
- 这种方式清理出来的内存空间会产生内存碎片,因此是不连续的,需要额外维护一个空闲列表
2 复制算法(Copying)
-
复制算法的核心思想是:将活着的内存空间分为两块,在垃圾回收的时候将正在使用的内存空间中活着的对象复制到未被使用的内存块中,之后清除正在使用的内存块中的对象,交换两个内存块的角色完成垃圾回收
-
新生代的内存空间中的
幸存者0
和幸存者1区
的垃圾清除就是使用的这种复制算法 -
优点:
- 没有标记和清除的过程,实现简单、运行高效
- 复制过去之后能够保证空间的连续性,因此不会出现碎片问题
没有标记过程指的是不需要在存活对象的Header中添加存活记录,仅仅只需要通过根可达算法将可达对象复制到另一个内存区域即可
-
缺点:
- 需要两倍的内存空间
- 对于G1这种垃圾收集器,会将内存拆分成为大量的
分区
(region),复制意味着GC需要维护分区之间的对象引用关系,导致产生内存占用和时间开销 - 复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的,否则如果像老年代一样大多数对象都是存活对象的话,依然使用复制算法就会导致复制的成本特别高
3 标记-压缩算法(Mark-Compact)
-
标记-压缩算法
是为了解决标记-清除算法
和复制算法
的缺点而设计的:复制算法
适用于新生代而不适用于老年代大多数情况对象都是存活对象的情况标记-清除算法
可以应用在老年代中,但是该算法的效率低下,而且在执行完内存回收后还会产生内存碎片,不利于老年代大对象的存储,所以标记-压缩算法
在此基础上进行了改进
-
执行过程:
- 第一阶段从根节点开始标记所有被引用的对象,和
标记-清除算法
一样 - 第二阶段将所有的存活对象都压缩到内存的一端,并按照顺序排放
- 最后,清理边界以外的所有空间
- 第一阶段从根节点开始标记所有被引用的对象,和
-
标记-压缩算法
的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此也可以称之为标记-清除-压缩算法
二者的本质差异在于标记-清除算法是一种非移动式的回收算法,标记-压缩是移动式的。是否移动回收后的存回对象是一种优缺点并存的风险决策
-
标记的存活对象按照内存地址整理排列之后,再给新对象分配内存的时候,只需要存储一个内存的起始地址即可,而不需要再维护一个空闲列表
-
优点:
- 消除了
标记-清除算法
内存区域分散的缺点,需要给新对象分配内存的时候,只需要存储一个内存的起始地址即可,而不需要再维护一个空闲列表 - 避免了
复制算法
内存减半的代价
- 消除了
-
缺点:
标记-压缩算法
的效率低于复制算法
- 移动过程中,如果对象被其他对象引用,则需要调整引用的地址
- 移动过程需要STW,全程暂停用户应用程序
4 三种算法的比较
标记-清除算法 | 复制算法 | 标记-压缩算法 | |
---|---|---|---|
速度 | 中等 | 最快 | 最慢 |
空间开销 | 少(但会堆积内存碎片) | 通常需要存活对象的两倍大小(不会堆积内存碎片) | 少(不会堆积内存碎片) |
移动对象 | 否 | 否 | 是 |