Card Marking
如果 young gc 线程只遍历年轻代内的对象引用,那么老年代到年轻代的跨代引用就会被忽略,被老年代存活对象跨代引用的年轻代对象会被回收,这样就破坏了应用程序的运行。但是如果每次ygc都进行全堆扫描,且ygc次数较频繁,会很慢。在 young gc 时,为了找到跨代引用,通常有这几个方法:
- 当对象引用路径指向老年代时继续遍历老年代对象找到跨代引用。
- 线性地扫描老年代对象,标记跨代引用。
- 从程序开始运行,就使用一个集合记录所有跨代引用的创建,在 young gc 时扫描这个集合里指向年轻代的跨代引用。
前两种方式都需要在 young gc 时去遍历老年代对象,因为老年代存活对象多,工作量太大,jvm 使用的是第三种方式。
首先分析跨代引用如何产生的:对于老年代到年轻代的跨代引用(a->b),产生条件有两种:
- gc 线程把对象 a 从年轻代移动到了老年代,
- a 本身是老年代对象,应用线程修改了 a 的引用指向了年轻代的 b。
对于 第一种情况gc 线程本身创建的跨代引用,可以直接由 gc 线程在创建时记录,所以问题就变成了:如何记录应用线程修改对象引用时创建的跨代引用?
在 jvm 中使用分治法,将老年代划分成多个 card(和 linux 内存 page 类似),统称为card table,只要 card 内对象引用被应用线程修改,就把 card 标记为 dirty。然后 young gc 时会扫描老年代中 dirty card对应的内存区域作为 GC roots,记录其中的跨代引用,这种方式被称为Card Marking。
jvm 通过写屏障(write barrier)来实现监控程序线程对引用的修改,并且标记对应 card,写屏障工作方式和代理模式类似,具体来说是通过在引用赋值指令执行时,添加对了 card table 的修改指令。
以最简单的setFoo(Object bar)方法为例:
setFoo(Object bar) { this.foo = bar; }
jvm 编译的汇编指令如下,第一行是赋值指令,后面几行标记被修改引用所在的 card 为dirty card,即CARD_TABLE[this address >> 9] = 0:
; rsi is 'this' address ; rdx is setter param, reference to bar ; JDK6: mov QWORD PTR [rsi+0x20],rdx ; this.foo = bar mov r10,rsi ; r10 = rsi = this shr r10,0x9 ; r10 = r10 >> 9; mov r11,0x7ebdfcff7f00 ; r11 is base of card table, imagine byte[] CARD_TABLE mov BYTE PTR [r11+r10*1],0x0 ; Mark 'this' card as dirty, CARD_TABLE[this address >> 9] = 0
小结
jvm 使用 card marking 的方式,避免了 young gc 时扫描整个老年代存活对象,付出的代价是在每次修改引用时添加额外的汇编指令实现写屏障,和额外的内存来保存 card table,在Hotspot实现是字节数组。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构