从“菜鸟”到“大神”:带你吃透Java垃圾收集

一、面试被问懵?别慌!

前几天和一个准备跳槽的朋友聊天,他跟我吐槽说去面试,结果被一道关于 Java 垃圾收集器和垃圾收集算法的问题给问懵了,当时场面一度十分尴尬 ,回来之后郁闷得不行。其实这也不能怪他,垃圾收集器和垃圾收集算法一直以来都是 Java 面试中的高频考点,而且这部分知识不仅要背理论,更要理解背后的原理和应用场景,确实不太好掌握。​

相信很多小伙伴在面试的时候都有过类似的经历,本来前面的问题回答得都挺顺利,结果突然被问到垃圾收集器和算法相关的内容,脑子瞬间一片空白,只能硬着头皮瞎扯几句,最后面试结果自然不太理想。别担心,今天这篇文章,就来帮大家把这部分知识好好梳理一下,以后再遇到这类问题,咱不仅能对答如流,还能给面试官留下深刻的印象!

二、垃圾收集算法

在深入了解垃圾收集器之前,我们先来认识一下垃圾收集算法。垃圾收集算法是垃圾收集器的核心,不同的算法有着不同的原理和适用场景 。​

(一)分代收集算法​

分代收集算法是目前应用最广泛的垃圾收集算法之一。它的核心思想是根据对象的存活周期不同,将内存划分为不同的区域,一般分为新生代和老年代。新生代的对象通常朝生夕死,存活时间很短,而老年代的对象存活时间则相对较长 。针对不同区域的特点,采用不同的垃圾收集算法。在新生代,由于对象存活率低,适合采用复制算法,这样可以高效地回收大量死亡对象;而在老年代,对象存活率高,没有额外空间进行分配担保,所以通常采用 “标记 - 清除” 或 “标记 - 整理” 算法 。​

(二)复制算法​

复制算法将内存划分为大小相等的两块,每次只使用其中一块。当这一块内存用完时,将存活的对象复制到另一块内存上,然后把已使用过的内存一次性清理掉 。这种算法的优点是实现简单,且不会产生内存碎片,因为每次都是在一块连续的内存空间中进行分配。它非常适合新生代,因为新生代中大部分对象都是短期存活的,只有少量对象会存活下来被复制到另一块内存。不过,复制算法也有明显的缺点,它需要额外的空间,因为总有一块内存是空闲的,这就导致内存利用率较低。而且,当存活对象较多时,复制的开销会增大,性能会受到影响 。​

(三)标记 - 清除算法​

标记 - 清除算法分为两个阶段:标记阶段和清除阶段。在标记阶段,从根对象开始遍历,标记出所有存活的对象;在清除阶段,遍历整个内存空间,回收所有未被标记的对象,即垃圾对象 。这种算法实现起来比较简单,不需要额外的空间。它的缺点也很明显,首先是效率问题,标记和清除两个过程都需要遍历整个内存空间,时间复杂度较高;其次是会产生内存碎片,因为回收后的内存空间是不连续的,这可能会导致后续分配大对象时找不到足够的连续内存空间,不得不提前触发另一次垃圾回收 。​

(四)标记 - 整理算法​

标记 - 整理算法的标记阶段和标记 - 清除算法相同,都是标记出所有存活的对象。不同的是,在整理阶段,它会将存活对象向一端移动,然后清理掉边界以外的内存,从而实现内存的紧凑 。与标记 - 清除算法相比,标记 - 整理算法解决了内存碎片的问题,使得内存空间更加连续,有利于后续的内存分配。它的缺点是在整理过程中需要移动存活对象,这会带来额外的开销,所以效率相对较低 。​

三、常见垃圾收集器全解析​

了解了垃圾收集算法之后,接下来就是各种垃圾收集器啦,这可是面试中的重点,一定要好好掌握 !​

(一)Serial 收集器​

Serial 收集器是最基本、历史最悠久的垃圾收集器。它是一个单线程收集器,在进行垃圾收集时,必须暂停其他所有的工作线程,直到收集结束,这种现象被称为 “Stop - the - World” 。它的优点是简单高效,在单核 CPU 环境下,由于没有线程交互开销,能获得最高的单线程收集效率 。在新生代,它采用复制算法,将新生代内存划分为 Eden 区和两个 Survivor 区,每次只使用 Eden 区和其中一个 Survivor 区,当 Eden 区满时,将存活对象复制到另一个 Survivor 区 。在老年代,它使用标记 - 整理算法。Serial 收集器是 Client 模式下默认的新生代垃圾收集器 。不过,在现在的多核 CPU 环境下,它的使用场景相对较少,因为停顿时间会让用户明显感觉到卡顿,在 Java Web 应用程序中很少采用 。​

(二)ParNew 收集器​

ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括可用的控制参数、收集算法、“Stop - the - World” 机制、对象分配规则、回收策略等都与 Serial 收集器完全一样,实现上这两种收集器也共用了相当多的代码 。它在新生代同样使用复制算法,并且默认开启的收集线程数与 CPU 的数量相同 。ParNew 收集器是很多 JVM 运行在 Server 模式下新生代的默认垃圾收集器,因为它能充分利用多 CPU、多核心等物理硬件资源优势,在多核环境下可以更快地完成垃圾收集,提升程序的吞吐量 。它还有一个重要的特点,就是目前除了 Serial 收集器外,只有它能与 CMS 收集器配合工作 。在单 CPU 环境中,ParNew 收集器由于存在线程交互开销,可能不会有比 Serial 收集器更好的效果 。​

(三)Parallel Scavenge 收集器​

Parallel Scavenge 收集器也是一个新生代收集器,同样使用复制算法和并行多线程进行垃圾收集 。它的特点是关注点与其他收集器不同,它的目标是达到一个可控制的吞吐量 。吞吐量 = 程序运行时间 /(程序运行时间 + 垃圾收集时间),比如程序运行了 99 分钟,垃圾收集花了 1 分钟,那吞吐量就是 99% 。高吞吐量可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务,例如执行批量处理、订单处理、工资支付、科学计算的应用程序 。它提供了两个参数用以精确控制吞吐量,分别是控制最大 GC 停顿时间的-XX:MaxGCPauseMillis及直接控制吞吐量的参数-XX:GCTimeRatio 。此外,还有一个-XX:+UseAdaptiveSizePolicy参数,打开该开关后,虚拟机将根据当前系统运行情况收集性能监控信息,动态调整 SurvivorRatio、PretenureSizeThreshold 等细节参数 。​

(四)Serial Old 收集器​

Serial Old 收集器是 Serial 收集器的老年代版本,同样是一个单线程收集器,使用标记 - 整理算法 。它主要有两个用途:一是在 JDK 1.5 以及之前的版本中,在 Server 模式下与 Parallel Scavenge 收集器搭配使用;二是作为 CMS 收集器失败后的后备预案,当 CMS 收集器出现 “Concurrent Mode Failure” 失败时,会临时启用 Serial Old 收集器来重新进行老年代的垃圾收集 。它在 Client 模式下是默认的老年代垃圾回收器 。由于是单线程收集,在老年代对象较多、内存较大的情况下,垃圾收集的效率会比较低,停顿时间也会比较长 。​

(五)Parallel Old 收集器​

Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本,使用多线程和标记 - 整理算法 。在注重吞吐量及 CPU 资源敏感的场合,比如大型批处理任务,Parallel Scavenge 收集器和 Parallel Old 收集器的组合能提供很好的内存回收性能 。在 Java 8 中,默认使用的就是 Parallel Scavenge 收集器和 Parallel Old 收集器的组合 。它可以通过 JVM 参数-XX:+UseParallelOldGC来启用 ,还可以通过-XX:MaxGCPauseMillis设置每次垃圾收集停顿时间的最大目标,JVM 会尽量调整来满足这个目标;通过-XX:GCTimeRatio设置垃圾收集时间占总时间的比例 。​

(六)CMS 收集器​

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,从名字上就可以看出它是基于标记 - 清除算法实现的 。它的运作过程相对复杂,分为四个阶段:​

  1. 初始标记:标记 GC Roots 能直接关联到的对象,速度很快,但需要暂停用户线程 。​
  2. 并发标记:从 GC Roots 的关联对象开始遍历整个对象图,耗时比较长,但不需要暂停用户线程 。​
  3. 重新标记:修正在并发标记期间,因用户线程继续运作导致标记产生变动的那一部分对象的标记记录,需要暂停用户线程,但时间相对并发标记阶段较短 。​
  4. 并发清除:清理掉标记阶段已经死亡的对象,由于使用清除算法,不需要移动对象,因此不需要暂停用户线程,可并发清除 。​

总的来说,CMS 收集器的内存回收是和用户线程一起并发执行的,所以它能在很大程度上减少垃圾收集时的停顿时间,提高用户体验 。它也有一些缺点,比如对处理器资源非常敏感,默认回收线程数是(处理器核心数 + 3)/ 4,CPU 核心数越少,对用户线程的影响就越大;无法处理浮动垃圾,在并发清理阶段,用户线程运行会产生新的垃圾,这部分垃圾 CMS 无法在本次收集中处理,只能留待下一次 GC 时清理,这就可能导致 “Concurrent Mode Failure” 失败,进而触发 Full GC;由于基于标记 - 清除算法,会产生内存碎片,可能导致大对象分配时找不到连续空间,不得不提前触发 Full GC 。​

(七)G1 收集器​

G1(Garbage First)收集器是 JDK 1.7 提供的一个新收集器,在 JDK 9 之后,它替代了 Parallel 回收器,成为了默认的垃圾收集器 。它的设计目标是在有限的时间内获取尽可能高的收集效率,并且可以建立可预测的停顿时间模型 。G1 收集器将 Java 堆分为多个大小相等的独立区域(Region),每个 Region 都可以根据需要扮演新生代的 Eden 空间、Survivor 空间以及老年代空间 。它还引入了一个特殊的 Humongous 区域,用于存储大对象,当大对象的大小超过单个 Region 时,会被放在多个连续的 Humongous Region 中,G1 会把这个区域当作老年代来处理 。​

G1 收集器的垃圾回收过程大致分为以下四个步骤:​

  1. 初始标记:标记 GC Roots 能直接关联到的对象,并且修改 TAMS 指针的值,让下一阶段的用户线程并发工作时能正确在可用 Region 中分配新对象,此阶段需要停止用户线程,但时间短,且在 Minor GC 中同步完成 。​
  2. 并发标记:从 GC Roots 开始对堆中的对象进行可达性分析,扫描整个堆,耗时较长,但和用户进程并发进行 。​
  3. 最终标记:对用户线程做另一个短暂的暂停,处理并发阶段结束后遗留的少量记录 。​
  4. 筛选回收:对各个 Region 的回收价值和成本进行排序,根据用户期望的停顿时间来制定停顿计划,把回收的那部分 Region 中存活的对象复制到空的 Region 中,再清理掉旧 Region 中的全部空间,此过程涉及对象的复制移动,需要暂停用户线程 。​

G1 收集器在整体上是基于标记 - 整理算法实现的,从两个 Region 来看,是基于标记 - 复制算法,这两种方法确保 G1 在垃圾回收时不会产生内存碎片 。它优先回收价值大的 Region,也就是那些包含大量可回收对象的区域,这样可以在有限的时间内获得更好的回收效果 。

四、垃圾收集器选择​

这么多垃圾收集器和算法,在实际应用中该如何选择呢?这主要取决于应用场景的特点和需求 。​

(一)对响应时间敏感的 Web 应用​

对于像电商网站、在线游戏平台这种对响应时间要求极高的 Web 应用,用户希望操作能够得到即时响应,任何明显的卡顿都可能导致用户流失 。在这种场景下,应该优先选择能减少停顿时间的垃圾收集器,比如 CMS 收集器或 G1 收集器 。CMS 收集器通过并发标记和并发清除阶段,能在很大程度上减少垃圾收集时的停顿时间,提高用户体验;G1 收集器不仅能建立可预测的停顿时间模型,还能优先回收价值大的 Region,在保证响应时间的同时,也有较好的垃圾回收效果 。​

(二)注重吞吐量的后台运算任务​

像数据分析、科学计算这类后台运算任务,它们通常不需要与用户进行频繁交互,更关注的是如何在有限的时间内完成尽可能多的计算任务 。对于这类应用,Parallel Scavenge 收集器和 Parallel Old 收集器的组合是比较好的选择 。Parallel Scavenge 收集器以吞吐量为目标,通过多线程进行垃圾收集,能高效率地利用 CPU 时间;Parallel Old 收集器作为它的老年代版本,同样支持多线程并发回收,两者结合可以在注重吞吐量及 CPU 资源敏感的场合,提供很好的内存回收性能 。​

(三)内存较小的应用场景​

如果应用程序运行在内存资源有限的环境中,比如一些嵌入式设备或者小型服务器,Serial 收集器可能是一个不错的选择 。虽然它是单线程收集器,会导致 “Stop - the - World”,但它实现简单,内存开销小,在内存较小的情况下,其简单高效的特点可以发挥优势 。当然,如果是在多核环境下,也可以考虑使用 ParNew 收集器,利用多线程来提高垃圾收集效率 。

五、总结​

Java 垃圾收集器家族就像一个拥有众多能工巧匠的团队,每个成员都有独特的技能,为 Java 程序的高效运行保驾护航。从简单高效的 Serial 收集器,到追求极致低延迟的 ZGC 收集器,它们各自在不同的领域发光发热 。​

在选择垃圾收集器时,就像为你的应用程序挑选合适的伙伴,需要充分了解应用的需求和运行环境,权衡吞吐量、响应时间和 CPU 资源等因素,做出明智的决策。同时,不要忘记通过实际测试和监控来验证和优化你的选择,让垃圾收集器与应用程序完美协作,创造出高效、稳定的 Java 应用世界。希望这篇文章能帮助大家深入了解 Java 垃圾收集器,在开发的道路上一帆风顺🚀 如果你有任何问题或想法,欢迎在评论区留言分享~​

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值