标记,到底在标记什么?
虚拟机标记算法的核心答案只有一句话:它通过遍历对象图,找出所有仍被引用的“活对象”,并为其打上特定记号,为后续的垃圾回收清扫划定精确边界,没有标记,回收器就只能盲目清场,整个内存管理将彻底失控。针对Java虚拟机(JVM)的自动内存管理,理解标记算法是看懂垃圾回收所有上层机制的基石,本篇不会堆砌术语,而是从最朴素的“认人”逻辑出发,拆解它的原理与应用现场。
虚拟机标记算法原理:如何从一堆对象中“认人”
要理解原理,就得从判断“谁还活着”这个最根本的问题说起,标记的第一步,不是去扫垃圾,而是去清点存活者。
可达性分析:唯一权威的“验尸官”
主流虚拟机(如HotSpot)依靠可达性分析(GC Roots Tracing)判定对象生死,它的逻辑是:从一组被称之为“GC Roots”的根节点集合出发,自上而下沿着引用链进行遍历搜索,凡是能被遍历到的对象,就被标记为存活对象;反之,则是可回收的“死人”。
这与引用计数算法形成鲜明对比的是,可达性分析能优雅地解决循环引用问题,即便两个对象互相指向对方,只要它们与GC Roots断绝了联系,在遍历时依然无法被触及,就会被判定为垃圾,行业共识认为,这是当前商用虚拟机选型的关键原因稳定性压倒一切。
GCRoots:名单上的“特殊公民”
哪些元素有资格作为GC Roots?常见场合包括:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象、常量引用的对象。
- 本地方法栈中JNI(Native方法)引用的对象。
- 虚拟机内部的引用(如基本数据类型对应的Class对象、常驻的异常对象NullPointerException等)。
- 所有被同步锁(synchronized关键字)持有的对象。
- 反应Java虚拟机内部情况的JMXBean、JVMTI回调等。
只有当这些根节点都确认“无路可走”时,对象才会被正式“宣判死刑”。
从标记到“处决”:对象真的会“死而复生”吗
即便被标记为不可达,对象也并非立刻被物理删除,在标记阶段完成后,虚拟机会将不可达对象放入一个待回收队列(F-Queue),这是finalize()方法的最后机会这也是许多开发者容易误解的地方,如果对象在finalize()中重新与GC Roots建立关联,它就能成功“越狱”,逃过本次回收,业内专家指出,任何依赖finalize()来释放资源的代码都是反模式,因为它执行时机不确定,且极大拖累回收效率,强烈建议用try-with-resources替代。
标记的变体:标记-清除、标记-复制与标记-整理的区别
单纯的“打标记”只是第一步,如何基于标记结果进行高效回收,才是核心战场,根据后续处理策略,衍生出三种经典算法,它们各有各的脾气。

标记-清除算法:最朴素的“原教旨主义”
这是最基础的算法,理解它很简单:先标记所有需要回收的对象,然后统一回收所有被标记的对象。
- 优势:逻辑简单,实现方便。
- 致命伤:执行效率不稳定,内存碎片严重,回收后产生大量不连续的内存碎片,导致后续分配大对象时不得不提前触发新的垃圾回收。
标记-复制算法:为了“幸存者”的体面
为了解决碎片化与低效问题,复制算法(常被称为“标记-复制”)登场,它将内存划分为大小相等的两块(通常表现为Eden区和Survivor区),每次只用其中一块,当这一块内存用完,就将存活对象平移到另一块,然后一次性清空原区域,HotSpot虚拟机默认的Eden:Survivor比例是8:1,这保证了内存利用率高达90%(只有10%的浪费用于“备用区”)。
- 核心优势:内存规整,高效快捷,分配内存只需指针碰撞即可。
- 现实痛点:对象存活率较高时(如老年代场景),复制一次涉及大量内存拷贝,效率会急转直下,它决定了标记-复制算法天生适合“朝生夕灭”的新生代,而非长命百岁的老年代。
标记-整理算法:老年代的“搬家工”
老年代对象的存活率较高,如果强行采用复制,代价巨大,这时就轮到标记-整理算法出场,它的标记阶段与标记-清除一致,但后续处理不同:它让所有存活对象向内存空间一段移动(整理),然后清理掉边界以外的内存。
| 对比维度 | 标记-清除 | 标记-复制 | 标记-整理 |
|---|---|---|---|
| 空间碎片 | 严重 | 无 | 无 |
| 分配速度 | 慢(需遍历空闲列表) | 快(指针碰撞) | 中等(需移动指针) |
| 适用场景 | 老年代(CMS的初版思路) | 新生代 | 老年代(Parallel Old、G1) |
| 交互延迟 | 高(全量遍历+清理) |
高(存活高时拷贝开销大) | 中(需要STW暂停) |
从理论到生产:CMS、G1与ZGC的实际选择
理解了原理,自然要看向真实战场,现代Java虚拟机中,没有哪款收集器是仅靠一种算法走到黑的,它们都是择优混用。
高风险场景:低延迟优先(CMS的教训)
CMS(并发标记清除)收集器曾是老年代的主流,它以“获取最短回收停顿时间为目标”,但CMS的失败在于“清除”而非“标记”,由于它采用标记-清除算法,长时间运行后必然产生大量碎片,如果老年代剩余空间无法容纳一个新生代晋升的大对象,必然会触发“Concurrent Mode Failure”,继而退化为Serial Old进行Full GC,导致长时间卡顿,这也是为什么在高并发、秒杀场景(例如大型电商的订单结账模块)中,工程师必须仔细调整CMS的触发阈值,或者干脆迁移到G1。
大多数服务器场景的默认选择:G1的“分治”策略
G1(Garbage First)收集器现在已是服务端JVM的默认选项,它的创新在于将整个Java堆划分为多个大小相等的Region,它不仅做标记,还利用“回收集”的概念,优先回收价值最大的Region。
- 实现路径:G1同时使用标记-复制和标记-整理算法,但作用在不同区域之间,它能回收的同时,保持内存区域规整,避免了整堆的碎片化。
- 参数实操:调整
-XX:MaxGCPauseMillis=100只能作为软性目标,G1内部通过-XX:G1NewSizePercent和-XX:G1MaxNewSizePercent控制新生代比例,如果发现Humongous(大对象,>Region大小50%的对象)分配频繁导致GC频繁,可以考虑调大-XX:G1HeapRegionSize参数(默认动态生成,范围1MB~32MB)。
前沿探索:ZGC的指针染色技术
不同于传统的对象头标记,ZGC(Z Garbage Collector)采用了“染色指针(Colored Pointers)”技术,它把标记的信息直接编码在指针的未使用位上,这让它在标记、转移阶段几乎不需要停顿,虽然ZGC并非传统意义上的“标记算法”完全套用,但它的底层仍基于可达性分析做标记,只是从“在对象上做笔记”变成了“在指针上做笔记”,在需要超大堆(TB级)、且要求亚毫秒级停顿的金融交易风控系统中,ZGC正逐渐成为一个备选答案。
操作指南:在真实环境中观察标记过程
了解这些理论,不妨打开命令行看一眼你的JVM到底在做什么,在JDK 8及以后版本,开启以下参数最常见的观察方式是添加GC日志打印:
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps
-Xloggc:/path/to/gc.log

当你查看gc.log时,看到类似 [GC (Allocation Failure) [PSYoungGen: 102400K->10239K(204800K)],这里的102400K->10239K本质上就是标记算法刚完成后的数据变化从总占用102400K,经过标记和复制,存活下来的只有10239K,通过观察垃圾回收日志,可以直观估算出各类对象的存活率,进而帮助决定是调大新生代大小还是转移到大页内存。
标记的终点是“物尽其用”
回到开篇的问题,虚拟机标记算法虽不直接释放内存,却是整个回收体系的“侦察兵”,无论是“标记-清除”的简单粗暴、“标记-复制”的以空间换时间,还是“标记-整理”的以时间换空间,它们都指向同一个终极目标:让有限的内存承载更多的业务数据,选择哪种算法,本质上是在吞吐量、低延迟与内存占用之间做匹配自身业务的权衡,当你下一次面对线上GC压力时,一切优化,始于一纸精准的“活人名单”。
Q&A:关于虚拟机标记算法,你可能还想问
G1和CMS在“标记”上最大不同是什么?
最大的区别在于标记的范围和记录方式,CMS是整堆扫描,并发标记阶段会遍历老年代和新生代,但不移动对象,只做标记,而G1通过SATB(Snapshot-At-The-Beginning)算法捕捉并发标记开始时的堆快照,同时严格记忆跨Region引用,从而能精准地构建出每个Region的回收价值和存活对象数,G1的标记更细粒度,为后续精准的“回收集合并”打下基础。
为什么说“标记”阶段通常会引发STW(Stop The World)?
因为修改对象引用关系会导致标记结果失真,如果在标记过程中,一个原本存活的对象突然被赋值给了某个已经扫描过的变量,回收器就会漏标,导致存活对象被错误回收,无论是CMS的初始标记还是G1的最终标记,都需要短暂暂停业务线程,为了缓解这个问题,回收器引入了记忆集(Remembered Set)和写屏障(Write Barrier),将引用变更的信息记录下来,确保最终标记能覆盖到所有被修改过的引用。
为什么复制算法在新生代高效,在老年代却会被弃用?
关键在于对象的存活率与生命周期的双向指标,新生代的对象“朝生夕灭”,绝大多数对象活过第一轮Minor GC就会被回收,因此需要复制的存活对象极少,复制成本低且空间利用率高,但老年代中的对象通常存活率极高(例如缓存对象、数据库连接池中的长连接),假如在老年代使用复制算法,就需要将几乎全部对象搬运一遍,这会产生巨大的内存移动开销和停顿时间,这好比搬家时,搬一个只有两件行李的旅店客人很容易,但搬一个装满整套家具的三居室则费力不讨好。
图片来源于AI模型,如侵权请联系管理员。作者:酷小编,如若转载,请注明出处:https://www.kufanyun.com/ask/921207.html


评论列表(2条)
读了这篇文章,我深有感触。作者对标记的理解非常深刻,论述也很有逻辑性。内容既有理论深度,又有实践指导意义,确实是一篇值得细细品味的好文章。希望作者能继续创作更多优秀的作品!
这篇文章写得非常好,内容丰富,观点清晰,让我受益匪浅。特别是关于标记的部分,分析得很到位,给了我很多新的启发和思考。感谢作者的精心创作和分享,期待看到更多这样高质量的内容!