随笔
推迟了一周才发布这篇文章实属罪过,最近延迟更新是因为最近在研究下一个系列“微服务、云原生”文章的整体排版。不得不吐糟一句,java的内容真的是多到没边,尤其是当微服务成为主流,导致程序员之间的层级化也越来越严重,架构师的要求达到有史以来的恐怖高度,业务开发人员的可替换性更高(难受~~~)。
引言
这篇博文将介绍HotSpot虚拟机中常见的几种垃圾回收算法,如:标记清除、拷贝算法、标记压缩等。
参考书籍:“深入理解Java虚拟机”
个人java知识分享项目——gitee地址
个人java知识分享项目——github地址
标记清除(Mark Sweep)
最基础的收集算法是“标记-清除”(Mark-Sweep)算法,如同它的名字一样,算法分 为“标记”和“清除”两个阶段:
- 标记:首先标记出所有需要回收的对象,标记的过程其实就在上一篇文章中有了介绍
- 清除:在标记完成后统一回收所有被标记的对象
jvm首先会标记出可以回收的垃圾对象(通过根可达算法),然后将这些对象进行清除其实也就是将对象相应的内存块重置,这一整个的过程都是通过虚拟机的守护进程(deamon)完成。
标记清除算法是最基础的收集算法,之所以说它 是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其不足进行改进而得到的。因此此算法的缺点也是比较明显的:
- 效率不算高,需要停止整个应用程序,导致用户体验差
- 这种方式清理出来的空闲内存是不连续的,产生内存碎片。需要维护一个空闲列表,而且空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
复制算法(Copying)
为了解决效率问题,一种称为“复制”(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空间没有足够空间存放上一次新生代收集下来的存活对象时, 这些对象将直接通过分配担保机制进入老年代。
复制算法的优缺点:
- 优点:没有标记和清除过程,实现简单,运行高效。复制过去以后保证空间的连续性,不会出现“碎片”问题
- 缺点:此算法的缺点也很明显,就是需要两倍的内存空间。对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小。
标记压缩(Mark Compact)
此算法是为了解决复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低所提出来的一种算法。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中 所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
此算法的优缺点:
- 优点: 消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。消除了复制算法当中,内存减半的高额代价
- 缺点:从效率上来说,标记-整理算法要低于复制算法。移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。移动过程中,需要全程暂停用户应用程序。即:STW
分代收集算法
前面所有的这些算法中,并没有一种算法可以完全替代其他算法,它们都具有自己独特的优势和特点。
分代收集算法应运而生。分代收集算法,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分为新生带和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。
在HotSpot中,基于分代的概念,GC所使用的内存回收算法必须结合年轻代和老年代各自的特点。
- 年轻代(Young Gen) - 年轻代特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。- 这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,因此年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。
- 老年代(Tenured Gen) - 老年代特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。- 这种情况存在大量存活率较高的对象,复制算法明显变得不合适。一般是由标记-清除-整理额混合实现。 - Mark阶段的开销与存活对象的数量成正比- Sweep阶段的开销与所管理区域的大小成正相关- Compact阶段的开销与存活对象的数据成正比
增量收集算法
如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么久可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。
- 优点:增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作
- 缺点:使用这种方式,由于在垃圾回收过程中,间断性地还执行了引用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转化的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。
最后再总结一下三种常见的算法的优劣:
Mark SweepMark ConpactCopying速度中等最慢最快空间开销少(但会堆积碎片)少(不堆积碎片)通常需要活对象的2倍大小(不堆积碎片)移动对象否是是
版权归原作者 a_ittle_pan 所有, 如有侵权,请联系我们删除。