【JVM系列】JVM打扫房间的不同攻略——常见GC算法
在谈垃圾回收算法之前,我们不妨先想想平时打扫屋子的经验。屋子里面既有像纸团、香蕉皮之类的垃圾对象,也有像杯子、花瓶和散落在地上的玩具这些不是垃圾的对象。那么,我们打扫屋子的时候可以采取什么样子的策略呢?
标记-清除策略:锁定纸团、香蕉皮这种垃圾对象,把垃圾扫到垃圾桶就完事儿了,其它像散落在地上的玩具、杯子等不适垃圾的对象也不去整理收纳。这样做的好处是扫起来非常快,但是下次买个新东西回家的时候还是没地方放,一般你这样打扫房间妈妈是会骂你的。
标记-整理策略:锁定垃圾,清扫垃圾,然后把玩具、杯子或者其它有用的东西收纳整理好。这样以来,虽然耗费的时间更多,但是获得的空间是更大的,新买东西回家之后也有更多的空间来放新东西,妈妈喜欢这种打扫方式。
复制策略:把房间分为几个区域,在打扫的时候顺便把有用的对象就顺手丢到固定的区域里面去,然后再把剩下的区域全部清理掉。这样和标记-整理一样不会产生垃圾散落在地上的情况,但是打扫的时候要先确保屋子里面有空间是干净的,这样对空间的利用率不高。
分代策略:不仅仅要清扫垃圾,还要标记长时间不使用的对象,例如:锤子、电钻这些不是经常使用的工具,咱们把它们移动到仓库里面去长期收纳起来。这时候,房间就好比年轻代,收纳经常使用的对象,仓库就好比老年代,收纳不那么常常使用但是又很重要的对象。
其实,把垃圾收集和平时打扫房间相类比起来会发现还是有很多相似之处的,这样也更方便我们去理解常见的gc算法。
标记-清除(Mark-Sweep)算法
标记-清除(Mark-Sweep)算法是早期垃圾收集领域的经典算法之一,其操作过程分为两个主要步骤,即标记阶段和清除阶段:
标记阶段: 在这一阶段,算法会遍历所有存活对象,并标记它们。通过从根对象出发,沿着对象引用链逐步遍历的方式来完成的,所有可达的对象都会被标记,而未被标记的对象则被认定为垃圾。
清除阶段: 在标记完成后,清除阶段将会执行。这个阶段的任务是清理所有被标记为垃圾的对象。这些对象将被释放,以便为新的对象提供空间。清除后,内存中将只保留标记为存活的对象。关于标记-清除算法的优点和缺点如下表:
优点 | 描述 |
---|---|
简单易懂 | 标记-清除算法的设计相对简单,易于理解和实现。 |
适用于垃圾较少场景 | 在垃圾相对较少的情况下,该算法表现出较高的清理效率。 |
缺点 | 描述 |
---|---|
内存碎片问题 | 标记-清除算法容易导致内存碎片问题,难以满足连续内存分配的需求。 |
性能不稳定 | 当内存中包含大量待清理的垃圾对象时,标记和清除的效率可能会下降,导致性能不稳定。 |
复制(Copying)算法
为了克服标记-清除算法在面对大量可回收对象时执行效率较低的问题,复制(Copying)算法应运而生。复制算法的核心思想是将整个内存空间划分为两个相等的区域,当一块区域的内存使用完毕后,将其中存活的对象复制到另一块尚未使用的区域,然后清理已使用过的内存空间。复制算法的优点和缺点如下表:
优点 | 描述 |
---|---|
实现简单,运行高效 | 复制算法的设计相对简单,执行效率较高,适用于垃圾对象较少的场景。 |
无内存碎片 | 由于整块区域都被使用,不会产生内存碎片问题,有效避免了由碎片化导致的内存管理难题。 |
缺点 | 描述 |
---|---|
内存空间浪费 | 复制算法需要预留一半的内存空间用于存放未使用的区域,导致内存空间利用率较低。 |
大量复制开销 | 如果内存中的大多数对象都是存活的,复制算法将面临大量对象复制的开销,可能导致性能下降。 |
标记-整理(Mark-Compact)算法
标记-整理算法在垃圾回收的过程中首先标记出所有需要回收的对象。不同于标记-清除算法,标记整理算法的后续步骤并非直接清理可回收对象,而是通过将所有存活的对象移动到一端,然后直接清理掉端边界以外的内存。标记-整理算法的优点和缺点如下表:
优点 | 描述 |
---|---|
无内存空间碎片 | 标记-整理算法的重要特点是不会产生内存空间碎片,有效解决了标记-清除算法可能面临的内存碎片问题。 |
避免多余空间浪费 | 相较于复制算法,标记-整理算法不需要额外预留一半内存空间,避免了空间浪费。 |
缺点 | 描述 |
---|---|
效率较低 | 由于标记-整理算法需要移动对象,其效率相对较低。对象移动不仅增加系统负担,而且需要全程暂停用户线程,即“Stop The World”,这可能对应用程序的响应时间产生不利影响。 |
引起引用对象更新 | 由于对象移动,所有引用该对象的地方都需要更新,这可能引起额外的开销。 |
标记整理算法的优点在于解决了内存空间碎片问题,避免了多余空间的浪费。然而,由于需要移动对象,它可能引起一些性能上的开销和引用对象更新的问题。这些是在选择垃圾回收算法时需要考虑的权衡。
Appel 式回收
在1989年,Andrew Appel为了更优化处理具有“朝生夕灭”特点的对象,提出了一种半区复制分代策略,如今被称为“Appel式回收”。该策略的具体实施是将内存分为一块较大的Eden空间和两块较小的Survivor空间,通常可称为From区和To区,或者Survivor 0区和Survivor 1区。
研究显示,新生代中的对象有98%具有“朝生夕死”的特性,因此并不需要按照1:1的比例划分内存空间。相反,采用一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor空间。在进行回收时,将Eden和Survivor中仍存活的对象一次性地复制到另一块Survivor空间上,最后清理掉Eden和已使用的Survivor空间。换句话说,仅有一个Survivor空间会被浪费。
HotSpot虚拟机新生代中,Eden和Survivor空间的比例为8:1:1。在使用过程中,仅使用Eden区和一个Survivor区,即新生代有90%的空间处于活跃状态,只有10%的空间是浪费的。虽然在“普通场景”下,新生代中的98%对象可被回收,但由于无法百分百保证每次回收都仅有不多于10%的对象存活,因此在Survivor空间不足时,需要依赖其他内存进行分配担保,通常是老年代。简而言之,如果Survivor空间不足,对象将以分配担保的形式进入老年代。
主流GC收集器采用的算法
垃圾回收器 | 采用的GC算法 | 代次 |
---|---|---|
Serial | 复制 | 新生代 |
Parallel | 复制 | 新生代 |
ParNew | 复制 | 新生代 |
CMS (Concurrent Mark-Sweep) | 标记-清除 | 老年代 |
G1 (Garbage-First) | 标记-整理 | 老年代 |
ZGC (Z Garbage Collector) | 标记-整理 | 老年代 |
Shenandoah | 标记-复制(独立的全局复制阶段) | 老年代 |
上述表格展示了一些主流垃圾回收器所采用的GC算法。值得注意的是,一些回收器可能会组合使用不同的算法,或者在特定阶段采用特定的算法,以优化性能和内存利用。这种选择通常取决于应用程序的特性和性能需求。
总结
GC算法涵盖了标记-清除、复制、标记-整理以及Appel 式回收等多种方法。标记-清除算法设计简单,适用于垃圾较少的情况,但可能导致内存碎片和性能不稳定。复制算法通过将内存划分为两块,避免了碎片问题,但需要预留一半内存,造成空间浪费。标记-整理算法解决了内存碎片,但效率较低,需要移动对象。Appel 式回收则以半区复制分代策略,减少了空间浪费,适用于“朝生夕灭”特点的对象。选择适当的算法需根据具体场景和系统需求,权衡各算法的优劣。
原文始发于微信公众号(ByteRaccoon):【JVM系列】JVM打扫房间的不同攻略——常见GC算法
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/186429.html