JVM学习笔记(五)———垃圾收集算法

垃圾收集算法

从判定对象存亡的角度看,垃圾收集算法分为“引用收集式垃圾收集”(Reference Counting GC)和“追踪式垃圾收集”(Tracing GC),分别对应引用计数算法和可达性分析算法,本文主要讨论追踪式垃圾收集相关算法的思想。

分代收集理论

要讨论垃圾收集算法,就一定避不开分代收集理论,当下商业虚拟机的垃圾收集器大都遵循了分代收集(Generational Collection)理论,分代收集理论建立在两个分代假说之上,即

  • 弱分代假说:绝大多数对象都是朝生夕灭的。
  • 强分代假说:熬过越多次垃圾收集的对象就越难以消亡。

以上两个分代假说共同奠定了多款常用垃圾收集器的一致设计原则:垃圾收集器应该将Java堆划分出不同的区域,将不同年龄的对象分配在合适的内存空间。

这样做的原因是,如果一块内存区域内都是朝生夕灭的对象,那么将这些对象集中在一起,每次进行垃圾收集的时候只关注如何保留可以存活下来的一小部分对象,而不用去标记大量即将要被回收的对象;同样的如果一块内存区域都是难以消亡的对象,那将他们集中在一起,每次进行垃圾收集的时候只关注那些可以被回收的对象即可。除此之外,还可以使用较高的频率回收朝生夕灭的对象集,以较低的频率回收难以消亡的对象集,这样就能以较低的代价回收到大量的内存空间。

在Java堆被虚拟机划分为不同观点区域后,垃圾收集器可以每次只针对某一个区域进行回收,因此便有了"Minor GC"“Major GC”"Full GC"这样不同回收类型的划分,各类型具体含义如下

——部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集。
————新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
————老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。
————混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。
——整堆收集(Full GC):指目标是收集整个Java堆和方法区的垃圾收集

有了对Java堆内存的划分,便可以在不同的内存区域使用适合的有针对性的垃圾收集算法,主流的有"标记-复制算法"“标记-清除算法”“标记-整理算法”。

在现在的商用虚拟机李,设计者一般至少会将Java堆内存划分为新生代老年代两个区域。在新生代中,每次垃圾收集时都会有大批对象死去,而在多次回收后存活的少量对象,将逐步晋升到老年代中存放。

这样的划分会引出一个问题,引发问题的根本原因是:对象不是孤立的、对象之间会存在跨代引用。假设现在要对新生代进行一次垃圾收集,但新生代中的对象可能被老年代的对象引用,为了找出新生代中存活的对象,不得不在GC Roots Set中加入全部老年代进行可达性分析,以此来保证分析结果的正确性。遍历全部老年代的所有对象理论上可行,但无疑会为内存回收带来很大的性能负担。为了解决这个问题,需要对分代收集理论添加第三条经验法则:

  • 跨代引用假说:跨代引用相对于同代引用来说仅占极少数。

跨代引用假说其实可以根据强分代与弱分代假说逻辑推理得出,存在相互引用关系的对象应该倾向于同时生存或同时消亡的,如果某新生代的对象A被老年代的对象B所引用,得益于老年代的对象B难以消亡,新生代的对象A在垃圾回收时也同样难以消亡,存活过多次垃圾收集后,对象A自然也就晋升到老年代当中了,至此对象A,B的跨代引用关系也就消除了。

根据跨代引用假说,可以得出不应为了少量的跨代引用而扫描整个老年代,同样不必浪费空间专门记录每一个对象存在哪些跨代引用,只需要在新生代上建立一个全局数据结构(该结构被统称为记忆集),这个结构将老年代划分为若干小块,表示出老年代的哪一块内存会存在跨代引用。这样当在Minor GC发生时,只需要将存在跨代引用区域的老年代对象加入GC Roots进行扫描即可。这样做会增加部分时间开销,也存在维护记录数据正确性的问题,但相比扫描整个老年代来说仍然是划算的。

标记-清除算法

标记-清除(Mark-Sweep)算法是最早出现且最基础的垃圾收集算法,顾名思义,其思路为先将所有需要回收的对象标记,再统一回收掉被标记的对象,也可以反过来标记存活的对象,再统一回收掉未被标记的对象。

此算法的主要缺点有二

  • 执行效率不稳定:如果Java堆中包含大量对象,其中大部分都是需要进行回收的,这时必须进行大量的标记和清除动作,执行效率会随处理对象数量的增长而降低。
  • 空间碎片化:标记、清除后会产生大量不连续的空间碎片,空间碎片太多可能会导致在以后程序执行的过程中,为大对象分配内存的压力较大,可能会出现可用空间还有许多,但没有足够大的连续空间为大对象分配内存,而不得不提前触发下一次垃圾收集动作。

标记-清除算法执行过程示意图如下:(很丑,将就看)
在这里插入图片描述

标记-复制算法

标记-复制算法简称复制算法,为了解决标记-清除算法面对大量可回收对象时执行效率低的问题,首先被提出的是"半区复制"垃圾收集算法,这种算法将可用内存空间划分为两块大小相等的空间,每次只使用其中的一块,当这一块的内存空间用完了,就将还存活的对象复制到另一块内存上,再把已经使用过的那块空间全部回收。

当面对内存中大多数对象都是存活的情况,这种算法将会产生大量内存间复制的开销,但当面对内存中大多数对象都说可回收的情况,那么只需要复制少数存活对象即可,不难看出,复制算法更适合应用于新生代。标记-复制算法不存在空间碎片化问题,因为每次针对一个半区完全回收,在内存分配时只需要移动堆顶指针按顺序分配即可。

复制算法的实现简单,运行高效,但”半区复制“算法的缺点也显而易见,就是将可用内存缩小为了原来的一半,空间浪费过多。

标记-复制算法执行过程示意图如下:
在这里插入图片描述
根据弱分代假说,有研究表明新生代中的对象约有98%熬不过第一轮垃圾收集,因此对于复制算法,根本不需要按照1:1的比例来划分新生代的内存空间。

针对新生代对象朝生夕灭的特点和"半区复制"算法的缺陷,一种更加优化的半区复制分代策略被题出,现在称为"Appel式回收"。

Appel式回收的具体做法是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden空间和其中一块Survivor空间。当发生垃圾收集时,将Eden和被使用的Survivor中存活的对象一次性复制到另一块未被使用的Survivor空间上,然后直接回收Eden和Survivor的空间。HotSpot虚拟机默认的Eden和Survivor的大小比例是8:1,即每次新生代中可用的空间为实际可用空间的90%。

上文中提及的98%的新生代对象熬不过第一轮垃圾收集,但这只是在某一普遍条件下的测试结果,不能保证任何条件下的每次垃圾收集都只有不多于10%(Survivor默认比例)的对象存活,因此Appel式回收还有一种充当逃生门的安全设计,当一次回收后的Survivor空间不足以容纳存活的对象时,需要其他内存区域(大多数情况下是老年代)进行分配担保,即将这些对象通过分配担保机制直接晋升至老年代。

标记-整理算法

使用标记-清除算法会产生空间碎片化的问题,而不会引起空间碎片化问题的标记-复制算法又不适用于老年代,一款针对老年代的标记-整理算法应运而生。其算法的标记过程与标记-清除算法相同,在清除过程,先将所有存活对象都向内存空间一侧移动,然后再回收掉边界意外的内存,标记-整理算法执行过程示意图如下
在这里插入图片描述
标记-清除算法与标记-整理算法的本质差异在于前者是非移动式算法,后者是移动式算法。是否移动回收后的存活对象是一项优缺点并存的风险决策。优点显而易见是不会产生空间碎片化的问题。

在移动式算法中,如果要移动对象的话,尤其是在老年代一类每次回收都有大量对象要移动的区域,移动存活对象并更新所有引用是一种极其负重的操作,这种对象移动操作必须全程暂停用户应用程序才能进行,这种停顿被最初的虚拟机设计者描述为"Stop The World",为了解决线程停顿问题,不同的垃圾收集器提供了不同的解决方案。

如果像标记-清除算法完全不考虑移动和整理存活对象的话,空间碎片化的问题只能依赖更为复杂的内存分配器和内存访问器来解决。内存访问是用户程序最频繁的操作,在这个环节上增加额外的负担,势必会影响应用程序的吞吐量

吞吐量 = 运行用户代码实际/运行用户代码时间+运行垃圾收集时间

基于上述描述可见,是否移动对象都存在弊端,移动对象会导致在内存回收时更为复杂,反之则内存分配时更加复杂。从线程停顿时间的角度看,不移动对象的停顿时间更短(甚至不停顿),但移动对象会收获更高的应用程序吞吐量。除单独使用清除和整理某一算法外,还有一种混合使用的方法,即虚拟机多数时间都采用标记-清除算法,直到空间碎片化程度已经影响到了对象分配时,才采用一个标记-整理算法收集,解决空间碎片化问题。


参考书籍 《深入理解Java虚拟机》第三版 ——周志明
本篇内容主要用于作者自身学习总结记录,才疏学浅,如文中出现纰漏,还望指正

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

7rulyL1ar

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值