JVM垃圾收集算法
文章目录
前言
从如何判断对象是否消亡的角度,可以将垃圾收集算法分为“引用计数式垃圾收集”和“追踪式垃圾收集(Tracing GC)”两大类,以下内容主要讨论追踪式垃圾收集。
一、分代收集理论
当前的商业虚拟机的垃圾收集器,大多遵循了“分代收集(Generational Collection)”的理论进行设计。而分代收集理论又建立在以下两个分代假说之上:
- 强分代假说(Strong Cenerational Hypothesis):熬过越多次垃圾收集过程的对象就越难消亡。
- 弱分代假说(Weak Generational Hypothesis ):大部分的对象都是朝生夕灭的。
根据这两个分代假说,收集器将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域中存储。
现在的商用Java虚拟机依据分代收集理论,一般至少会把Java堆划分为新生代(Young Generation)和老年代(Old Generation)两个区域。每次垃圾收集时都会有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。
- 跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。
对象不少孤立的,对象间会存在跨代引用。跨代引用假说就是为了解决类似的问题而提出的。根据前两条假说,可以得出这样一个隐式推论:存在相互引用关系的两个对象应该倾向于同生共死。
依据这条假说,我们不必再为少量的跨代引用就去扫描整个老年代,也不必浪费空间专门记录一个对象是否存在以及存在哪些跨代引用,只需要在新生代上建立一个全局的数据结构(记忆集),这个结构把老年代划分为若干小块,标识出老年代的哪一小块内存存在跨代引用,此后当发生Minor GC(新生代回收)时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。虽然这种方法需要在对象改变引用关系时维护记录数据的准确性,会增加一些运行开销,但相比于扫描整个老年代来说,还是相当高效的。
名词补充:
- 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
- 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。
- 混合收集(Mixor GC):指目标是收集整个新生代和部分老年代的垃圾收集。目前只有G1收集器有这种收集行为。
- 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。
二、垃圾收集算法
1.标记-清除算法
“标志-清除”是最早出现也是最基础的垃圾收集算法,在1960年由Lisp之父John McCarthy提出。
该算法分为 “标志” 和 “清除” 两阶段:首先标记所有需要回收的对象,在标记完成后,统一回收所有被标记的对象,也可以标记存活的对象,统一回收所有未被标记的对象。
标记-清除算法的两大缺点:
- 执行效率不稳定: 如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时就必须进行大量的标记和清除动作,导致标记和清除两个动作的执行效率随着对象数量的增加而下降。
- 内存空间的碎片化问题: 标记、清除之后会产生大量不连续的内存碎片,空间碎片太多会导致当以后在程序运行过程中需要分配较大内存对象时无法找到足够的连续的内存而被迫提前触发另一次垃圾收集动作。
2.标记-复制算法
标记-复制算法简称为复制算法,解决了标记-清除算法面对大量可回收对象时执行效率低的问题。1969年Fenichel提出了一种称为“半区复制”(Semispace Copying)的垃圾收集算法,它将可用内存划分为大小相等的两块,每次只使用其中一块。当这块空间用完,就将还存活的对象复制到另一块内存,然后再把使用过的内存空间一次性清理回收。
优点:
- 对于内存中多数对象可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也不需要考虑空间碎片的问题,只需要移动堆顶指针,按顺序分配即可。
缺点:
- 如果内存中多数对象都存活,这种算法将会产生大量存活对象复制的开销。
- 复制回收算法的代价是将可用内存减半,空间利用率太低。
IBM公司曾有一项专门研究表明:新生代中的对象有98%熬不过第一轮收集。因此,没必要按照1:1的比例划分新生代的内存空间。“Appellate式回收” 就对新生代的空间比例做出了调整。
Apple式回收把新生划分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。当发生垃圾收集时将Eden和Survivor存活的对象一次性复制到另外一块Survivor空间,然后一次性清除掉Eden和已经使用过的那块Survivor空间。HotSpot虚拟机默认Eden与Survivor的空间大小比例为8:1,这样分配新生代中可用空间就是90%,在一般的场景下完全够用,但是在极端场景下,Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(实际上大多数情况下是老年代的内存)进行分配担保(Handld Promotion)。
3.标记-整理算法
标记-整理算法 是针对老年代的存亡特征提出的,其标记过程与“标记-清除算法”一样,但后续的步骤是燃所有存活的对象都向内存空间的另一端移动,然后直接清除掉边界以外的内存。
缺点: 在老年代这种每次回收都有大量对象存活的区域,移动存活对象并更新所有引用这些对象的地方将会是一种负担极重的操作,而且移动对象操作必须全程暂停用户应用程序才能进行,像这样的停顿被形象描述为“Stop The World”。
优点: 如果使用标记-清除算法,产生的碎片化空间问题只能依赖更为复杂的内存分配器和内存访问器来解决,内存访问是用户程序最频繁的操作,如果在这一环节上增加额外的负担,必然会直接影响程序的吞吐量。从垃圾收集的停顿时间来看,不移动对象停顿时间会更短,甚至不需要停顿,但是从整个程序的吞吐量来看,移动对象会更划算。
总结
收集算法是内存回收的方法论!垃圾收集器就是内存回收的实践者。
本文为笔者学习JVM内存分配和垃圾收集器内容的笔记,希望提炼出来的笔记可以帮助大家快速回顾相关的知识点,共同进步!
本文参考:《深入链接Java虚拟机》