垃圾回收算法可分为2类:
* 引用计数式垃圾收集(直接垃圾收集)
* 追踪式垃圾收集(间接垃圾收集)
java主流的虚拟机都是采用追踪式垃圾收集的方式,我们主要学习这个。
第3条是根据前2条假说逻辑得出的隐含推论:存在相互引用的2个对象,是应该同时生存或者消亡的。举个例子,如果某个新生代对象存在跨代引用,由于老年代难以消亡,该引用会使得新生代对象在回收时同样存活,进而年龄增长,晋升到老年代中,这时候跨代引用就自然消除了。
依据这条假说,我们就不应再为了少量的跨代引用区扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用,只需要在新生代建立一个全局的数据结构(remembered set),这个结构把老年代划分成若干块,标识出老年代的哪一块会存在跨代引用。此后当发生MInor GC的时候,只有包含跨代引用的小跨内存中的对象才会被加入到GC Roots进行扫描。
是最基础的垃圾收集算法。首先标记需要回收的对象(也可以反过来标记存活的对象),标记完成后,统一回收所有标记(或者未被标记)的对象。
标记的过程就是对象的生死判定过程
回收前后的状态如图所示:
主要缺点:
为了解决标记-清除算法面对大量可回收对象时执行效率底下的问题,提出的一种“半区复制”的垃圾收集算法。
它将可用的内存容量分为大小相等的2块,每次只使用1块。当这块用完了,将还活着的对象复制到另一块内存上,然后把已使用的内存块一次清理掉。
如果内存中多数对象都是存活的,这种算法就会产生大量的对象间复制的开销,但是对于多数对象是可回收的场景,算法需要复制的就是少量存活的对象,而且每次都是针对整个半区内存进行回收,分配内存时也不用考虑内存碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。
回收前后状态如图:
优点:实现简单,运行高效
缺点:可用的内存缩小为原来的一半,空间浪费过多
现代商用的虚拟机大多优先采用这种算法去回收新生代,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%对象存活的极端情况,所以老年代不能直接选用标记-复制算法。
针对老年代对象的特点,提出了一种有针对性的“标记-整理”算法。
其中标记的过程都一样,但是后续动作是让所有存活的对象向内存空间的一端移动,然后直接清理掉边界以外的内存。如图所示:
标记-清除算法和标记-整理算法的本质差异在于前者是一种非移动式的回收算法,后者是移动式的。是否移动存活对象是一种优缺点共存的风险决策:
基于以上2点来看,是否移动对象都存在弊端,移动则内存回收时更复杂,不移动则内存分配时更复杂。HotSpot虚拟机里面关注吞吐量的Parallel Scavenge收集器是基于标记-整理算法的,而关注延迟的CMS收集器则是基于标记-清除算法的,这也从侧面印证这点。
最后提一种“和稀泥”的解决方案:不在内存分配和访问上增加太大的负担,具体做法是虚拟机平时大多数时候采用标记-清除算法,暂时容忍内存碎片的存在,直到内存空间的碎片化程度已经影响到对象分配时,再采用标记-整理算法收集一次,获得规整的内存空间。
基于标记-清除算法的CMS收集器面临空间碎片过多时采用的就是这种处理办法。