垃圾回收算法可分为2类:
* 引用计数式垃圾收集(直接垃圾收集)
* 追踪式垃圾收集(间接垃圾收集)
java主流的虚拟机都是采用追踪式垃圾收集的方式,我们主要学习这个。
分代收集
- 弱分代假说:绝大部分对象都是朝生夕灭的
- 强分代假说:熬过越多次垃圾收集过程的对象就越难消亡
- 跨代引用假说:相对于同代引用只占极少数
第3条是根据前2条假说逻辑得出的隐含推论:存在相互引用的2个对象,是应该同时生存或者消亡的。举个例子,如果某个新生代对象存在跨代引用,由于老年代难以消亡,该引用会使得新生代对象在回收时同样存活,进而年龄增长,晋升到老年代中,这时候跨代引用就自然消除了。
依据这条假说,我们就不应再为了少量的跨代引用区扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用,只需要在新生代建立一个全局的数据结构(remembered set),这个结构把老年代划分成若干块,标识出老年代的哪一块会存在跨代引用。此后当发生MInor GC的时候,只有包含跨代引用的小跨内存中的对象才会被加入到GC Roots进行扫描。
标记-清除算法
是最基础的垃圾收集算法。首先标记需要回收的对象(也可以反过来标记存活的对象),标记完成后,统一回收所有标记(或者未被标记)的对象。
标记的过程就是对象的生死判定过程
回收前后的状态如图所示:
![图片替换文本](https://i-blog.csdnimg.cn/blog_migrate/4010ba2633ec0e1a45e28901a70de0ed.png#pic_center)
主要缺点:
- 执行效率不稳定。如果堆中包含大量的对象,其中有大部分是需要被回收的,就需要大量的标记和清除动作,导致标记和清除动作的执行效率随着对象的增多而降低;
- 内存空间的碎片化。标记和清除之后会产生大量的不连续的内存碎片,内存碎片太多可能会导致以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发一次垃圾收集动作
标记-复制算法
为了解决标记-清除算法面对大量可回收对象时执行效率底下的问题,提出的一种“半区复制”的垃圾收集算法。
它将可用的内存容量分为大小相等的2块,每次只使用1块。当这块用完了,将还活着的对象复制到另一块内存上,然后把已使用的内存块一次清理掉。
如果内存中多数对象都是存活的,这种算法就会产生大量的对象间复制的开销,但是对于多数对象是可回收的场景,算法需要复制的就是少量存活的对象,而且每次都是针对整个半区内存进行回收,分配内存时也不用考虑内存碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。
回收前后状态如图:
![图片替换文本](https://i-blog.csdnimg.cn/blog_migrate/e38d8d8978fc761c5a22c8565a11f870.png#pic_center)
优点:实现简单,运行高效
缺点:可用的内存缩小为原来的一半,空间浪费过多
现代商用的虚拟机大多优先采用这种算法去回收新生代,IBM曾经有一项专门的量化研究:新生代的有98%的对象都熬不过第一轮的收集,因此并不用按照1:1的比例划分新生代的内存空间。
HotSpot虚拟机的Serial、ParNew等新生代收集器均采用了一种更优化的半区分代复制策略——“Appel式回收”。具体做法:把新生代划分较大的一块Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和一块Survivor。发生垃圾收集时,将Eden和Survivor上仍然存活的对象一次性的复制到另外一块Survivor空间上,然后直接清理到Eden和已用过的那块Survivor空间。
HotSpot默认Eden和Survivor的大小比例是8:1,即每次新生代的可用空间占用整个新生代空间的90%
标记-整理算法
标记-复制算法在对象存活率较高的时候,有大量的对象复制操作,效率将会降低。更为极端的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中存在100%对象存活的极端情况,所以老年代不能直接选用标记-复制算法。
针对老年代对象的特点,提出了一种有针对性的“标记-整理”算法。
其中标记的过程都一样,但是后续动作是让所有存活的对象向内存空间的一端移动,然后直接清理掉边界以外的内存。如图所示:
![图片替换文本](https://i-blog.csdnimg.cn/blog_migrate/835991331897baed189d5018643cd3a2.png#pic_center)
标记-清除算法和标记-整理算法的本质差异在于前者是一种非移动式的回收算法,后者是移动式的。是否移动存活对象是一种优缺点共存的风险决策:
- 如果移动存活对象,尤其是在老年代这种每次都有大量对象存活的区域,移动存活并更新所有对象的引用是一种极为负重的操作。这种操作需要暂停整个用户应用程序才能进行,被称为“Stop The World”
- 但是如果和标记-清除一样不考虑移动和整理存活对象的话,碎片化的空间只能依赖于更复杂的内存分配器和内存访问器来解决。然而内存访问是用户程序最频繁的操作,如果在这一环节增加负担,会影响整个应用程序的吞吐量。
基于以上2点来看,是否移动对象都存在弊端,移动则内存回收时更复杂,不移动则内存分配时更复杂。HotSpot虚拟机里面关注吞吐量的Parallel Scavenge收集器是基于标记-整理算法的,而关注延迟的CMS收集器则是基于标记-清除算法的,这也从侧面印证这点。
“和稀泥”方案
最后提一种“和稀泥”的解决方案:不在内存分配和访问上增加太大的负担,具体做法是虚拟机平时大多数时候采用标记-清除算法,暂时容忍内存碎片的存在,直到内存空间的碎片化程度已经影响到对象分配时,再采用标记-整理算法收集一次,获得规整的内存空间。
基于标记-清除算法的CMS收集器面临空间碎片过多时采用的就是这种处理办法。