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 2023-03-19 19:02  zhengbiyu  阅读(58)  评论(0)    收藏  举报