Fight the Future

Java言語とJVM、そしてJavaエコシステム全般にまつわること

HotSpot VMのシンボル名に関わるコード

師匠から教えていただいた内容であり、私自身がいきなり気づいたわけではないのですが、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のようなシンボル名だと、配列の範囲を超えて書き込んでいるのか、疑問に思ったのです。

jhsdbjava.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した値がシンボル名の長さであり、その長さ分アロケートしていることがわかります。なので、領域は確保できているわけです。