JVM利用记忆集、卡表和写屏障解决跨代引用收集难题。基于跨代引用极少的假设,通过卡表记录老年代可能存在跨代引用的卡页,写屏障实时维护卡表脏状态。MinorGC仅扫描脏卡页,避免扫描整个老年代,以空间换时间提升效率。
JVM 在处理跨代引用收集时,面临一个经典难题:Minor GC 明明只回收年轻代,但如果老年代中有对象引用着某个年轻代对象,这个年轻代对象就不能被回收。最简单的做法是每次都扫描整个老年代——但这显然不现实,尤其当老年代已经膨胀到几十 GB 时。于是 JVM 祭出了一套精巧的组合拳:记忆集(Remembered Set)、卡表(Card Table) 和 写屏障(Write Barrier)。说穿了,核心思路就一句话:用空间换时间,假设跨代引用极少,然后只记录可能出问题的地方。

长期稳定更新的攒劲资源: >>>点此立即查看<<<
具体来说,整个流程依赖一条关键假说——“跨代引用极少”。年轻代对象频繁创建又快速消亡,老年代对象往往活得长、很少突然指向年轻代。基于这个观察,JVM 的设计者决定:不记录每一条跨代引用,而是只标记“这个老年代内存块里可能存在跨代引用”。这样一来,Minor GC 时只需要扫描这些被标记的内存块,就能找出所有指向年轻代的老年代引用,进而保护那些不应该被回收的年轻代对象。
记忆集是一个抽象概念,它的作用说白了就是一张“清单”,登记了哪些非收集区域(通常是老年代)里可能含有指向当前收集区域(年轻代)的引用。Minor GC 时,GC Roots 除了线程栈、静态变量这些常规项,还会把记忆集中登记的这些对象也当作根——这样一来,老年代的其他区域就完全不用碰了。
卡表是记忆集在 HotSpot 里的物理实现。简单说,它就是一个字节数组,每个字节对应老年代中的一块固定大小内存——这个固定大小的内存块叫做卡页(Card Page)。
addr,卡表下标就是 addr >> 9,非常快。0 表示这个卡页里目前没有跨代引用;1(Dirty)表示这个卡页里至少有一个对象引用了年轻代对象,需要 GC 时重点排查。卡表本身不会自己变脏,必须有人实时更新。谁来干这活?写屏障。每当老年代对象的某个字段被赋值——尤其是新赋的引用指向了一个年轻代对象——JVM 会在赋值指令之前或之后插入一小段代码。这段代码就是写屏障,它的唯一任务就是:检测这次赋值,然后把对应卡页标记为 Dirty。
oldObj.field = youngObj 时,写屏障会立刻计算 oldObj 所在的卡页位置,并将对应的卡表项设为 1。一次年轻代 GC 到底怎么利用这些机制?流程并不复杂,但设计得非常巧妙。
整个过程的关键点在于:不需要碰那些干净的卡页,因为它们内部要么没有老年代对象,要么老年代对象根本没有指向年轻代的引用。这样一来,GC 扫描的范围就从“整个老年代”缩小到了“少量 Dirty 卡页”,效率提升非常显著。而这套机制之所以能正常工作,根本前提还是那个假设——跨代引用确实很少。如果这个假设不成立,卡表里 Dirty 卡页暴增,写屏障的维护成本也会急剧攀升,那这套方案可能就扛不住了。好在实际情况中,这个假设基本成立,所以这套设计才成了 JVM 的标配。
侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述