JDK 12 Shenandoah简介

JDK 12 Shenandoah GC简介

本文根据Shenandoah的原始Paper整理而成。

Shenandoah作为一个实验性GC在JDK8时代就有了,在JDK12正式成为一个可选的垃圾回收器。

在JDK11有一个类似功能的ZGC被引入,相关介绍见JDK 11 ZGC简介

背景

带有Compact功能的GC有着更小的占用空间以及更好的缓存局部化,,但是stop the worst的compact对于200GB的大队来说,无法达到10~500毫秒的暂停时间。,所以就需要一个并发的gc,应用程序在运行的同时也能进行compact堆的工作,于是就有了Shenandoah gc,它是一个可以并发contact堆的GC算法,在Shenandoah以前的g1和cms,如果要compact堆的话是stop the world型的,G1通过分区来减少stop the world时间,cms默认不compact,如果打开了compact会stop the world compact。

Shenandoah 简介

基本想法

在对象头上加一个forward pointer,在运行时通过cas来更新这个forward pointer到拷贝的对象的新地址,在下一轮mark的时候,再去更新引用到这个对象的指针。在这之前,通过这个forward pointer,把老对象的访问转移到新对象上。

对象布局

如下图所示,Shenandoah在每一个对象头上加了一个forward pointer,如下图中的Indirection pointer。
shenandoah_object_layout

堆布局

类似g1,堆也被分成了相同大小的region。在gc时,任何region都可以成为回收对象,回收时回收那些垃圾较多的region(虽然分了region,但是没有分代)。如图2所示。
shanandoah_heap_layout

GC阶段

  1. 初始化标记:Stop the world扫描根集合。比如Java threads class static。
  2. 并发标记:遍历对象图,标记活着的对象。通过上一次GC所留下的forward pointer,把指向老对象的引用更新到上一次GC所Copy的新对象的引用(类似ZGC在mark阶段同时remap的工作)。
  3. 最终标记:Stop the world,重新扫描根集合,copy根集合到新的region,更新根集合让他们指向新的对象。初始化并发compact,free掉上一次清理过的region。
  4. 并发compact:把活着的对象拷贝到新的region。
    shanandoah_gc_phases

并发标记

使用一个SATB(SNAPSHOT AT BEGINNING)算法————任何在标记的开始阶段活着的,或者被新引用到的对象都被认为是活着的对象。
维护SATB需要一个write barrier,如果应用程序引用了一个新对象,这个新对象必须仍然能够被追踪到。具体做法是write barrier把新引用放到一个会被mark线程扫描的队列里。
每个mark线程会持有一个Threadlocal的标识region有多少live data的数据。在mark的结束阶段汇总这些数据来决定对哪些region做compact。

回收集的选择

选择或者对象较少的region(也就是垃圾较多的region)

并发Compaction

当回收集选好了之后,就要开始Compact阶段。GC线程使用一个猜测性的协议来完成compact。

  • 先猜测性的吧对象拷贝到Threadlocal allocation buffer中。
  • 然后CAS老对象的forward pointer到新对象的地址上。
  • CAS如果失败,说明有其他线程也做了同样的事情,这时候GC线程回滚掉之前COPY的对象,使用老对象上的其他线程留下的forward pointer。

Java线程有写操作时也是用上述方法来COPY对象,这也是CAS失败的原因。

更新引用

更新引用需要遍历整个Heap,所以放到下一次GC中的mark阶段去做。

Barriers

Shenandoah通过read barrier把对老对象的读取forward到新对象上。ReadBarrier的实现很简单,读对象的时候,通过forward pointer转读到新对象的地址即可。

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

As shown in gure 4 our compiled read barriers are a
single assembly language instruction.

void
ShenandoahBarrierSet::compile_resolve_oop(){
__ movptr(dst, Address(dst, -8));
}

Figure 4: Shenandoah simple read barrier for elds
of non-null objects

Here's an assembly code snippet for
reading a field:
When we start register %rsi contains the address of the
object, and the field is at offset 0x10.
mov 0x10(%rsi),%rsi
; *getfield value
Here's what the snippet looks like
with Shenandoah:
mov -0x8(%rsi),%rsi
; read of forwarding pointer at address object - 0x8
mov 0x10(%rsi),%rsi
; *getfield value
Figure 5: Shenandoah read barrier snippets

有两个Write Barrier。一个是用来保证SATB的正确性,具体做法见并发标记那一节(这个WriteBarrier和CMS还有G1中使用的一样)。

另一个仅用在concurrent compact阶段,用来确保在对象写操作时,该对象已经被copy到新的region。(于是写操作写到新对象上而不是老对象)

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
Here's an assembly code snippet for
writing a field with Shenandoah:
0x00007fa8351e3f66: cmpb $0x0,0x640(%r15)
; %r15 is the local thread
; 0x640 is the offset of the
; evacuation_in_progress flag
; so we compare the evacuation_in_progress
; flag to zero
0x00007fa8351e3f6e: mov -0x8(%r8),%r13
; Read the indirection pointer.
0x00007fa8351e3f72: je 0x00007fa8351e3f7f
; if !evacuation_in_progress jump to store
0x00007fa8351e3f74: xchg %rax,%r13
; swap our object %r13 with %rax
; %rax is the expected input arg
0x00007fa8351e3f77: callq Stub::shenandoah_wb
0x00007fa8351e3f7c: xchg %rax,%r13
; swap the return value %rax
; which is possibly new address of
; our object back into %r13
0x00007fa8351e3f7f: mov %sil,0x18(%r13,%rdx,1)
;*bastore {reexecute=0 rethrow=0 return_oop=0}
; - jdk.internal.org.objectweb.asm.ByteVector::
; putUTF8@61 (line 255)

Figure 6: Shenandoah speci c write barrier snippet

结论

对于并发Compact型的GC算法,不管是ZGC还是Shenandoah套路基本都是使用一个Forward pointer(在ZGC中是每个Page一个Forwarding table),当读写老对象时,通过forward pointer来转到新对象上的读写。

我希望我能简单介绍一下Shenandoah GC。如果您对相关实现的代码感兴趣,可以看文章开头原始Paper里的介绍,或者直接看openJDK里对应的实现.