師匠から教えていただいた内容であり、私自身がいきなり気づいたわけではないのですが、OpenJDKのメーリングリストでも他の方からも教えていただいたので、自分の備忘録的にまとめます。
Javaのシンボル名に関するHotSpot VMのコードの話です。ここでのシンボル名というのは、JVMの内部表現で、たいていのものは完全修飾クラス名のピリオド(.)をスラッシュ(/)に変えたものです。つまり、java.lang.Object
クラスならjava/lang/Object
がシンボル名です。このシンボル名のコードを読んでいたときに、私の頭はこんがらがりました。
HotSpotはC++で書かれています。Symbolクラスがあります。
# src/hotspot/share/oops/symbol.hpp class Symbol : public MetaspaceObj { private: // This is an int because it needs atomic operation on the refcount. Mask hash // in high half word. length is the number of UTF8 characters in the symbol volatile uint32_t _hash_and_refcount; u2 _length; u1 _body[2]; void byte_at_put(int index, u1 value) { assert(index >=0 && index < length(), "symbol index overflow"); _body[index] = value; } int length() const { return _length; } ... }
これは現時点で最新のJDK(JDK 15)のソースコードで、JDK 14までは少し違いますが、このエントリの主旨には関わりません。
シンボル名は、_body
に入る文字列です。u1
は符号なしバイトjubyte
のエイリアスです。この_body
、長さが2の配列なんです。沸き起こる疑問は、じゃあシンボル名は2文字、もしくはC++だからNULL 終端文字を考えると1文字しか入らないのか?という疑問です。
# src/hotspot/share/oops/symbol.cpp Symbol::Symbol(const u1* name, int length, int refcount) { _hash_and_refcount = pack_hash_and_refcount((short)os::random(), refcount); _length = length; _body[0] = 0; // in case length == 0 for (int i = 0; i < length; i++) { byte_at_put(i, name[i]); } }
コンストラクタでは、length
分ループして、byte_at_put()
を呼び出します。`byte_at_put()
は_body
配列の要素に文字を代入します。
void byte_at_put(int index, u1 value) { assert(index >=0 && index < length(), "symbol index overflow"); _body[index] = value; }
そうすると、_body
は要素数2なのに、java/lang/Object
のようなシンボル名だと、配列の範囲を超えて書き込んでいるのか、疑問に思ったのです。
jhsdb
でjava.lang.Object
のアドレスを調べ、GDBで見てみました。
(gdb) p ((Symbol*)0x00007ff2580650f0) -> _body $7 = "ja"
やはり最初の2文字しかありません。しかし、_length
の値を使って、この後の領域も見てみます。jhsdb
で見ると、_length = 16でした。なお、JDK14までは_length
がなく、代わりに_length_and_refcount
があります。JDK14でも試しましたが、その時は_length_and_refcount = 1114111
でした。これは16進数で10ffff、下4桁がrefcountなので上2桁からlengthは16とわかります。
さて、char配列で長さ16として見てみます。
(gdb) p (char[])(((Symbol*)0x00007ff2580650f0) -> _body)[0]@16 $9 = "java/lang/Object"
とシンボル名が正しく入っていることがわかります。ということは、安全にシンボル名をすべて書き込めるように対処しているところがあるはずです。
# src/hotspot/share/oops/symbol.cpp void* Symbol::operator new(size_t sz, int len) throw() { int alloc_size = size(len)*wordSize; address res = (address) AllocateHeap(alloc_size, mtSymbol); return res; }
ここで、size(len)*wordSize
した値がシンボル名の長さであり、その長さ分アロケートしていることがわかります。なので、領域は確保できているわけです。