随笔 - 171  文章 - 0  评论 - 0  阅读 - 62466

Card Marking

如果 young gc 线程只遍历年轻代内的对象引用,那么老年代到年轻代的跨代引用就会被忽略,被老年代存活对象跨代引用的年轻代对象会被回收,这样就破坏了应用程序的运行。但是如果每次ygc都进行全堆扫描,且ygc次数较频繁,会很慢。在 young gc 时,为了找到跨代引用,通常有这几个方法:
  • 当对象引用路径指向老年代时继续遍历老年代对象找到跨代引用。
  • 线性地扫描老年代对象,标记跨代引用。
  • 从程序开始运行,就使用一个集合记录所有跨代引用的创建,在 young gc 时扫描这个集合里指向年轻代的跨代引用。
前两种方式都需要在 young gc 时去遍历老年代对象,因为老年代存活对象多,工作量太大,jvm 使用的是第三种方式。
首先分析跨代引用如何产生的:对于老年代到年轻代的跨代引用(a->b),产生条件有两种:
  1. gc 线程把对象 a 从年轻代移动到了老年代,
  2. 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实现是字节数组。
 
posted on   zhengbiyu  阅读(37)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

点击右上角即可分享
微信分享提示