推迟了一周才发布这篇文章实属罪过,最近延迟更新是因为最近在研究下一个系列“微服务、云原生”文章的整体排版。不得不吐糟一句,java的内容真的是多到没边,尤其是当微服务成为主流,导致程序员之间的层级化也越来越严重,架构师的要求达到有史以来的恐怖高度,业务开发人员的可替换性更高(难受~~~)。
这篇博文将介绍HotSpot虚拟机中常见的几种垃圾回收算法,如:标记清除、拷贝算法、标记压缩等。
参考书籍:“深入理解Java虚拟机”
最基础的收集算法是“标记-清除”(Mark-Sweep)算法,如同它的名字一样,算法分 为“标记”和“清除”两个阶段:
jvm首先会标记出可以回收的垃圾对象(通过根可达算法),然后将这些对象进行清除其实也就是将对象相应的内存块重置,这一整个的过程都是通过虚拟机的守护进程(deamon)完成。
标记清除算法是最基础的收集算法,之所以说它 是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其不足进行改进而得到的。因此此算法的缺点也是比较明显的:
为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块(也就是堆内存中的两个survivor区,在以前的堆系列文章中有过介绍)。此算法的原理是通过将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
现在的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor[1]。 当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最 后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是 8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10% 的内存会被“浪费”。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每 次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里 指老年代)进行分配担保(Handle Promotion)。
内存的分配担保就好比我们去银行借款,如果我们信誉很好,在98%的情况下都能按时偿还,于是银行可能会默认我们下一次也能按时按量地偿还贷款,只需要有一个担保人能保证如果我不能还款时,可以从他的账户扣钱,那银行就认为没有风险了。内存的分配担保也一样,如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时, 这些对象将直接通过分配担保机制进入老年代。
复制算法的优缺点:
此算法是为了解决复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低所提出来的一种算法。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中 所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
此算法的优缺点:
前面所有的这些算法中,并没有一种算法可以完全替代其他算法,它们都具有自己独特的优势和特点。
分代收集算法应运而生。分代收集算法,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分为新生带和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。
在HotSpot中,基于分代的概念,GC所使用的内存回收算法必须结合年轻代和老年代各自的特点。
如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么久可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。
最后再总结一下三种常见的算法的优劣:
Mark Sweep | Mark Conpact | Copying | |
---|---|---|---|
速度 | 中等 | 最慢 | 最快 |
空间开销 | 少(但会堆积碎片) | 少(不堆积碎片) | 通常需要活对象的2倍大小(不堆积碎片) |
移动对象 | 否 | 是 | 是 |