「这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战」。
引言:前文已经简单的介绍了垃圾回收方面的知识和概念,本文则主要介绍垃圾收集算法以及一些与垃圾收集有关的理论。
垃圾收集算法
从对象消亡的角度可以将垃圾收集算法分为引用计数式垃圾收集与追踪式垃圾收集,两则也被称为直接垃圾收集和间接垃圾收集,本文主要介绍的算法均属于追踪式垃圾收集的范畴。
分代收集理论
分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,其建立在以下两个基础上:
-
弱分代假说:绝大多数对象都是朝生夕灭的。
-
强分代假说:熬过越多次垃圾回收的对象就越难消亡。
-
跨带引用假说:跨带引用相对于同代引用仅占少数。
该假说奠定了多款垃圾收集器的一致原则:收集器应该将Java堆分成多个区域,然后根据对象熬过的垃圾回收次数将其分到不同的区域存储。
比如将一些朝生夕灭的对象放在一个区域,每次回收只需要关注保留少量存活而不是去标记那些要回收的对象,然后熬过多次垃圾回收的对象放在一个区域,降低该区域垃圾回收的频率。这样就同时兼顾了垃圾回收的时间开销和内存空间的有效利用。
针对不同划分标准将Java堆分成不同区域出现了几种不同的针对性的垃圾回收算法,比如标记-复制、标记-清除、标记-整理算法等,这几种垃圾回收算法都是始于分代回收理论。
标记-清除算法
标记-清除算法有两个阶段,分别是标记、清除,首先是标记出要清理的对象,然后统一回收这些对象。该算法实现简单,不过其执行效率不稳定而且当Java堆包含太多的对象,且大部分需要回收的时候,就需要大量的标记和清除的动作,而且会产生很多的空间碎片,不方便后续创建需要许多连续空间的对象(创建时候如果没有足够的空间可能会提前触发垃圾回收)。关注延迟的CMS收集器就是基于此算法进行实现的。
标记-复制算法
标记-复制算法简单来说就是将一块内存空间分成两块,每次只用一块,当一块内存空间满了后,把存活对象复制到另外一块上,然后再清除使用过的那一块内存空间,该算法实现简单,效率高,不过可用空间只有原来的一半了,空间浪费严重。
该算法被大多商用Java虚拟机用来回收新生代的对象。此外还有一种Appel式回收,是将新生代分为Eden和两块Survivor空间,其中两者占比为8:1,每次回收只利用Eden空间和一块Survivor空间,将存活的对象复制到另外一块Surivivor空间中,如果超出的话就通过分配担保机制直接进入老年代。
标记-复制算法在对象存活数较多的时候要进行较多的复制操作,效率比较低。而且如果不想浪费50%的空间就需要额外的空间进行分配担保,所以老年代一般不使用这种算法。
标记-整理算法
针对老年代对象的存亡特征,出现了标记-整理算法,该算法过程和标记-清除算法类似,不过该算法是让所有的存活对象向内存空间一端进行移动,然后直接清理掉边界以外的内存。
在移动对象的时候需要暂停用户应用程序才能实现,但是如果和标记-清除算法一样则会产生许多碎片空间,而该问题只能依赖于更复杂的内存分配器和内存访问器来解决,这就会导致整个程序的吞吐量下降。基于上述问题需要不同应用的收集器自行进行选择合适的算法,比如关注吞吐量的Parallel Scavenge收集器就是基于此算法实现的,而关注于延迟的CMS收集器就是基于标记-清除算法实现的。不过CMS收集器在面临空间碎片过多时候会采用进行一次标记-整理算法来获取规整的内存空间。
结语
本文大致介绍了几种垃圾回收的算法,上述算法都是始于分代回收理论。关于一些经典的垃圾收集器的学习我会在后面的文章中进行介绍。本文就介绍到此。