复制算法明明要浪费一半内存,为什么新生代还偏偏用它

「Java 进阶之路」系列 Day33

写在前面

垃圾回收算法这部分很多人背得出"标记清除、标记整理、复制算法"这三个名字,但问一句"复制算法要白白浪费一半内存,这么不划算的算法为什么现在的JVM还在用",就说不清楚了。这篇把三种基础算法的原理和真正的适用场景讲清楚,为下一篇讲分代收集打好基础。


一、是什么:三种基础回收算法长什么样

标记清除(Mark-Sweep)

flowchart LR A[从GC Roots出发标记所有存活对象] --> B[清除所有没有被标记的对象] B --> C[清除后留下大量不连续的内存碎片]

分两步走:先从GC Roots出发,把所有能被引用到的存活对象标记出来;再把内存里没有被标记的对象直接清除掉。这是最直观的思路,但清除之后,存活对象和被清空的位置犬牙交错地分布在内存里,会产生大量不连续的内存碎片

标记整理(Mark-Compact)

flowchart LR A[从GC Roots出发标记所有存活对象] --> B[把存活对象整体向一端移动] B --> C[清理掉边界之外的空间 内存变连续]

前半步和标记清除一样,先标记存活对象;不同的是后半步------把所有存活对象整体向内存的一端移动挤压,让它们紧凑地排列在一起,再直接清理掉边界之外的剩余空间。这样处理完之后,剩下的空闲内存是连续的一整块,解决了标记清除的碎片问题,代价是"移动对象"这个操作本身有开销(不仅要搬运数据,还要同步更新所有指向这些对象的引用地址)。

复制算法(Copying)

flowchart LR A[内存分成两块 只使用其中一块] --> B[把当前使用块里的存活对象复制到另一块] B --> C[清空原来使用的那一块 交换角色]

把可用内存划分成两块相同大小的区域,每次只使用其中一块。垃圾回收发生时,把当前正在使用这一块里的存活对象,整体复制到另一块空闲区域,然后把原来那一整块内存直接清空------因为是整块清空,不需要一个个甄别哪里该清哪里不该清,效率很高,而且天然没有碎片问题(复制过去的对象是紧凑排列的)。代价很直接:能实际使用的内存永远只有总容量的一半,另一半时刻空闲着等待被交换进来。


二、为什么三种算法会共存:适用场景完全不同

三者不存在绝对的"谁比谁更好",各自的取舍决定了它们分别适合什么场景:

算法 是否有碎片 内存利用率 存活对象多时的开销
标记清除 较低(不用移动对象,只标记和清除)
标记整理 较高(存活对象越多,要移动、更新引用的开销越大)
复制算法 低(永远浪费一半) 极高(存活对象越多,要复制的数据越多)

这张表最关键的信息藏在最后一列 :标记整理和复制算法的开销都随着"存活对象的数量"增加而增加,只是标记整理是"移动"、复制算法是"复制";反过来说,如果一个内存区域里的对象大部分都很快死掉、只有极少数存活下来,复制算法的复制成本就低到可以忽略------这恰好是JVM新生代的真实特点:新创建的对象绝大多数都是"朝生夕死"的临时对象,一次GC下来能存活的可能只有个位数百分比。既然要复制的对象本来就没几个,"浪费一半空间"这个代价换来的是"效率极高、没有碎片"这两个明显的好处,非常划算。

反过来,老年代的对象大多是经历过多次GC依然存活下来的"老兵",存活率很高,如果在老年代用复制算法,每次GC要复制的对象数量会非常庞大,效率反而很差;而且老年代空间本身通常比新生代大得多,"浪费一半"这个代价在老年代场景下完全无法接受。所以老年代更适合用标记整理(或者标记清除)这类不需要为"永久闲置一半空间"买单的算法。

标记清除作为最基础、最直接的思路,虽然有碎片问题,但胜在实现简单、不需要移动对象,在内存空间比较充裕、暂时不那么在意碎片的场景下(比如某些以标记清除为主要策略的老年代收集器)依然有它的价值,是理解后面两种改进算法的基础。


三、怎么用:一句话记住三者的选型逻辑

  • 对象大多"朝生夕死"、存活率低 → 用复制算法,牺牲一半空间换来极高的回收效率和零碎片
  • 对象大多长期存活、存活率高 → 用标记整理,虽然移动对象有开销,但换来了没有碎片、内存利用率高
  • 想要最简单直接、能接受碎片 → 用标记清除,实现简单、不用移动对象

这三条选型逻辑不是孤立存在的------它们正是现代JVM"分代收集"思想的地基:新生代对象存活率低,天然适合复制算法;老年代对象存活率高,天然适合标记整理(或标记清除)。下一篇会具体讲这套分代收集机制,以及CMS、G1、ZGC这些实际的垃圾回收器分别是怎么组合运用这些基础算法的。


四、面试追问

Q1:标记清除算法最大的缺点是什么?

会产生大量不连续的内存碎片。因为清除操作只是把没有被标记的对象所在的内存位置标记为可用,并不会移动、整理剩下的存活对象,导致空闲内存和存活对象犬牙交错地分布,如果后续需要分配一个较大的连续内存空间,即使空闲内存总量足够,也可能因为找不到足够大的连续空间而提前触发一次垃圾回收。

Q2:标记整理算法是怎么解决标记清除的碎片问题的?

标记出存活对象之后,不是直接原地清除死亡对象,而是把所有存活对象整体向内存的一端移动挤压,让它们紧凑排列,再统一清理边界之外的剩余空间。这样处理完之后剩下的空闲内存是连续的一整块,不存在碎片问题,代价是移动对象本身有开销,需要同步更新所有指向这些被移动对象的引用地址。

Q3:复制算法为什么要浪费一半的内存空间,这样设计的好处是什么?

因为复制算法每次只使用一半的内存区域,垃圾回收时把存活对象整体复制到另一半空闲区域,再把原来使用的那一半直接整体清空。好处是清空操作不需要逐一甄别该清理哪些位置,效率很高,而且复制过去的存活对象天然紧凑排列,完全没有碎片问题。代价是能实际使用的内存永远只有总容量的一半。

Q4:为什么新生代适合用复制算法,老年代不适合?

新生代里的对象绝大多数都是"朝生夕死"的临时对象,一次垃圾回收下来能存活的对象比例很低,需要被复制的对象数量本来就很少,复制的开销可以忽略不计,"浪费一半空间"换来的高效率和零碎片非常划算。老年代的对象大多是经历过多次垃圾回收依然存活的对象,存活率很高,如果用复制算法,每次都要复制大量对象,效率反而很差,而且老年代空间通常更大,浪费一半空间的代价也更难接受。

Q5:三种垃圾回收算法在效率、内存利用率、碎片问题上分别有什么取舍?

标记清除实现简单、不需要移动对象,效率相对较高,但会产生内存碎片;标记整理没有碎片问题、内存利用率高,但移动对象和更新引用有额外开销,存活对象越多开销越大;复制算法效率高、没有碎片,但要牺牲一半的内存空间不能使用,且存活对象越多需要复制的数据量也越大。三者没有绝对的优劣,要结合具体场景(比如对象的存活率高低)来选择。


下一篇预告

Day34 讲分代收集思想以及CMS、G1、ZGC这几种常见垃圾回收器该怎么选,把这篇讲的三种基础算法真正落地到实际的JVM调优场景里。

相关推荐
妙码生花1 小时前
全网 8k star 的 BuildAdmin 正式发布 Golang 版本,这次我们在CRUD赛道杀死了比赛。
前端·后端·ai编程
worilb1 小时前
Java/JVM 常见诊断文件对比
java·开发语言
写代码像蔡徐抻1 小时前
三年了,AI为何还没有抢走程序员饭碗?
前端·后端·面试
魔兽大山哥1 小时前
【NL2SQL 实战 08】主链路:从提问到出数,图里跑了哪些节点
后端
Zane19941 小时前
只重写了 __eq__,为什么类突然变得不可哈希了?常用魔法方法大盘点
后端·python
程序员cxuan1 小时前
Claude Fable 5.1 的提示词又被扒了
人工智能·后端·程序员
toolsmith1 小时前
AI能替你读代码,但替代不了你积累洞察力:一次YouTrack逆向实战全记录
java·源码阅读
步行cgn1 小时前
Spring Boot 绑定简单 Bean 详解
java·spring boot·后端
鹿角片ljp1 小时前
我如何用 Frontend Design Skill 重构平台
java