0%

jvm 垃圾回收算法

概述

jvm中有多种垃圾回收算法,不同的虚拟机采用的回收方法也不同,本文主要介绍其中几种常见的回收算法,包括:标记清除算法、复制算法、标记整理算法、分代收集算法。

标记清除算法

标记清除(Mark-Sweep)算法是最基础的垃圾回收算法,他将回收过程分成两个阶段:标记和清除。第一阶段先扫描内存区域,对不可用对象(可以进行垃圾回收的对象)进行标记,第二阶段则将第一阶段所有标记成可回收的对象进行回收。下图是一个内存区域使用标记清除算法进行回收的前后对比图。

标记清除算法图

此算法有两个不足之处:一个是效率问题,标记和清除这两个过程的效率都不高,另一个是空间问题,标记清除之后会产生大量的内存碎片,可能导致后续在分配大内存对象时因为没有足够连续的空间而不得不提前触发另一次垃圾回收。

复制算法

复制(Copying)算法是将内存按照容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完时,将存活的对象复制到另一块内存区域,然后将这一块的内存空间一次性清理掉。下图是一个内存区域使用复制算法进行回收的前后对比图

复制算法图

此算法解决了标记清除算法中内存碎片的问题,在存活对象较少时,因为复制对象少,能够获得很好的效率。但是此算法由于预留了一半的内存区域而造成内存的浪费。为了进一步改进此算法,可以根据实际情况降低预留内存区域的比例。

现代的商业虚拟机绝大部分采用这种算法回收新生代的内存区域。但是并不是严格按照1:1的比例来预留内存区域的。而是将内存分成一块较大的Eden空间和两块比较小的Survivor空间,每次使用Eden和其中一块Survivor空间。当回收时,将Eden和Survivor中还存活的对象一次性复制到另外一块Survivor空间上,最后清理掉Eden和刚才用的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间是整个新生代的90%,只有10%的预留内存空间会被“浪费”。在决大部分情况下,在进行回收时,Survivor能够存放Eden和一个Survivor中的存活对象,但是也有少数情况会造成溢出,此时需要依赖其他内存(老年代)进行分配担保。

标记整理算法

标记整理(Mark-Compact)算法与标记清理算法类似,同样由两个阶段完成。第一个阶段同样是标记,第二个阶段对存活对象进行移动,将其移动到内存区域的一端,然后将存活对象界线之外的空间对象进行清理回收。下图是一个内存区域使用标记整理算法进行回收的前后对比图

标记整理算法图

此算法既避免了标记清理算法中的内存碎片问题,也弥补了复制算法中的预留内存区域的空间浪费及分配担保的问题。但是如果对象存活率低,经常发生垃圾回收时,此算法则存在效率问题。因此,此算法一般用于对象存活率高的老年代中。

分代收集算法

分代收集(Generational Collection)算法是根据对象存活周期的不同将内存划分成几块,一般情况下是划分成新生代和老年代,然后针对不同的区域使用不同的回收算法。一般情况下,在新生代中,由于大部分对象都是“朝生夕死”,每次回收时都有大量的对象被回收,对象存活率低,因此此区域中基本上使用复制算法进行垃圾回收。而在老年代中,由于对象存活率高,且没有额外的空间进行分配担保,因此一般选择标记整理算法或标记清除算法进行垃圾回收。

当前商业虚拟机的垃圾回收都采用此算法。

参考资料

[1] 周志明,深入理解Java虚拟机:JVM高级特性与最佳实践[M],北京:机械工业出版社,2013