JJUG CCC 2018 Fallのセッション、数村さんのGCを発生させないJVMとコーディングスタイルにて、TLABとBump the Pointerについての説明がありました。
(https://www.slideshare.net/kenjikazumura/gcjvm より抜粋)
TLAB、理屈は知っているけれど、よく考えると何も見たことがないなと思いました。そして、Red Hat社のAlekseyさんの個人サイトに、TLABの話があったなあ、と思い出しました。
JVM Anatomy Park #4: TLAB allocation
なぞりながら、実行してみると同時に、GCを発生させないJVMとコーディングスタイルのセッション内容の理解を深めることにします。
前提
TLABが何をするものかは、知っている前提です。
newする部分をPrintAssemblyする
TLABへのオブジェクトのアロケーションを見ます。単にObjectをnewするコードを書きました。
public class Tlab { public static void main(String[] args) throws Exception { Tlab t = new Tlab(); System.out.println(t.test()); System.out.println(t.test()); } public Object test() throws Exception { Object o = new Object(); return o; } }
Alekseyさんの説明にあるように、JITコンパイルさせて、PrintAssemblyを出力したいので、newするコードはメソッドにし、コードを削除されないようにインスタンスはreturnしてprintlnします。
PrintAssemblyするには、HSDISが必要です。ビルドして配置します。Ubuntuならsudo apt-get install libhsdis0-fcml
でも入りました。
実行します。-Xcomp
をつけて、testメソッドの最初の呼び出しで、強制的にJITコンパイルさせます。
javac Tlab.java java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=quiet -XX:CompileOnly=Tlab::test -Xcomp Tlab
アセンブリが出力されます。HSDISは、デフォルトではAT&Tのシンタックスとなります。
TLABのバッファサイズと、使用領域のtopを指すポインタの位置を比較している部分を探します。Alekseyさんの記事のものと異なり、prefetchnta
ではなくprefetchw
でした。
0x00007f38d4ad016c: mov 0x138(%r15),%rax 0x00007f38d4ad0173: mov %rax,%r10 0x00007f38d4ad0176: add $0x10,%r10 0x00007f38d4ad017a: cmp 0x148(%r15),%r10 0x00007f38d4ad0181: jae 0x00007f38d4ad01cb ;; B2: # B3 <- B1 Freq: 0.9999 0x00007f38d4ad0183: mov %r10,0x138(%r15) 0x00007f38d4ad018a: prefetchw 0x100(%r10) 0x00007f38d4ad0192: mov $0x20000208,%r11d ; {metadata('java/lang/Object')} 0x00007f38d4ad0198: movabs $0x0,%r10 0x00007f38d4ad01a2: lea (%r10,%r11,8),%r10 0x00007f38d4ad01a6: mov 0xb8(%r10),%r10 0x00007f38d4ad01ad: mov %r10,(%rax) 0x00007f38d4ad01b0: movl $0x20000208,0x8(%rax) ; {metadata('java/lang/Object')} 0x00007f38d4ad01b7: mov %r12d,0xc(%rax) ;*synchronization entry ; - Tlab::test@-1 (line 9)
数村さんのセッションで、HotSpotでTLABが使うレジスタはr15と決まっている、という説明がありました。この出力で、自分でも確認ができました。もしバッファサイズを超えた場合に戻れるように、raxからr10にコピーして、以降r10を使って、オブジェクトがアロケートできるかテストしています。
add $0x10,%r10
で、$0x10=16、つまりnew Object()したことで、オブジェクトのサイズである16を、現在のtopに加算しています。
cmp 0x148(%r15),%r10
で、進めたtopの位置と、TLABのendの位置を比較しています。もし、topがTLABのendを超えれば、jaeでジャンプします。
OKであれば、mov %r10,0x138(%r15)
で、topの位置を、0x138(%r15)のメモリ上に保存します。最初の行でそこから値を取り出していますので、上書きした、ということです。
mov %r10,(%rax)
で、r10でのテストがOKだったので、r10からをraxに書き込んでいます。
movl $0x20000208,0x8(%rax)
では、{metadata('java/lang/Object')}とあるように、いわゆるklassへの参照を書き込んでいます。
これもまた、わかったつもり…
アセンブリを読んで、TLABとBump the Pointerについて、理解が深まった気でいました。ここで、さらなるアドバイスをもらうことができました。
Linuxならコアとって、GDBでR15の指すところを見て、それがどこかのスレッドのTLABかを見れば「思う」が明確になると思いますよ。少なくともEdenかどうかはすぐにわかりますね。
— Yasumasa Suenaga (@YaSuenag) 2018年12月27日
たしかに、cmp 0x148(%r15),%r10
で、0x148(%r15)
がTLABのendであるというのは、説明を読んでそう考えているだけで、実際にendかどうかは確認できていないのです。
ブレークポイントとGDBを使って検証する
r15など、レジスタの値を見るのは、GDBを使うのがよさそうと考えました。CLIでのGDBも試しましたが、CLionでのデバッグ環境を整えたので、CLionを使います。
src/hotspot/share/gc/shared/threadLocalAllocBuffer.inline.hpp
にブレークポイントを打ちました。ThreadLocalAllocBuffer::allocate(size_t size)
関数です。この関数が、TLABにオブジェクトをアロケートしています。
inline HeapWord* ThreadLocalAllocBuffer::allocate(size_t size) { invariants(); HeapWord* obj = top(); if (pointer_delta(end(), obj) >= size) { // successful thread-local allocation #ifdef ASSERT // Skip mangling the space corresponding to the object header to // ensure that the returned space is not considered parsable by // any concurrent GC thread. size_t hdr_size = oopDesc::header_size(); Copy::fill_to_words(obj + hdr_size, size - hdr_size, badHeapWordVal); #endif // ASSERT // This addition is safe because we know that top is // at least size below end, so the add can't wrap. set_top(obj + size); invariants(); return obj; } return NULL; }
CLionで、同様に-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=quiet -XX:CompileOnly=Tlab::test -Xcomp Tlab
を実行します。SIGSEGVでブレークしますが、何度がresumeすると、allocate関数で止まります。
ThreadLocalAllocBufferでのtopが0x83600000
、endが0x836ac058
とわかりました。0x148(%r15)
の値が0x836ac058
であれば、cmp 0x148(%r15),%r10
がendとポインタの位置の比較であったと確信が持てます。
CLionは、ブレークポイントで止まると、自動的にGDBもアタッチしてくれます。便利です。ただ、GUIですし、マシンリソースはそれなりに使います。
GDBでメモリの値を見ます。
(gdb) info register r15 r15 0x7ffba401b000 140718765092864
r15の値は0x7ffba401b000
でした。0x148(%r15)
ですので、この値に0x148
を加算すると、0x7ffba401b148
です。このメモリアドレスの値を見ます。
(gdb) x 0x7FFBA401B148 0x7ffba401b148: 0xffffffff836ac058
0xffffffff836ac058
!つまり、0x836ac058
です!endは0x836ac058
でしたので、0x148(%r15)
にはendの位置が格納されていると、はっきりしました!
まとめ
TLABへのアロケーションを調べました。実際に、TLABへアロケートしている関数、アセンブリだけでなく、実行時の値を見て動作を確認できました。
JVM部分はとくに動かして学ぶということがしづらく、手を抜いて文献を読んだだけで、理解したつもりになってしまっていることがあります。広く浅くと、狭く深くを使い分けつつ、T型、逆クラゲ型にJVMの開発能力を作っていきたいです。