Stringクラスのカノニカライゼーション

Stringクラスのカノニカライゼーションがどのように実装されているか気になったので調べていたのですが、ソースをみたらString#intern()はnativeで実装されている。そういえば新人の頃に調べて同じところで行き詰った記憶があります。ソースがだめならってことでJava言語規定を読んでいたんですが、あまり細かく書いてない。
解説なぞないものかと見てみても、意外に無い。みんな気にしないところなのか?俺だけなのか?
だってさ、「new String」したら無駄にメモリが使われるところまでは納得いくけどその後その変数にsubstringとかしたらその変数って駄目なほうのMapに入るのか既存のMapに入るのかとか超きになる。ほかにもソースコードに書いた文字列ってどうやってStringクラスになるのかとか、でっかいシステムだとStringのMapって数10万のエントリを保持することになるからかなりコンフリクトするんじゃないかとか気になるわけですよ。身近なところなのでしっかり理解しておきたいじゃないですか。

しかし今やOPENJVMのおかげでJVMのソースが読める。素晴らしい。
http://openjdk.java.net/

さっそくJVMのソースを読んでみる。

String.java

    public native String intern();

javaクラスはこんな感じでCにぽいと投げられています。

String.c

#include "jvm.h"
#include "java_lang_String.h"

JNIEXPORT jobject JNICALL
Java_java_lang_String_intern(JNIEnv *env, jobject this)
{
    return JVM_InternString(env, this);
}

投げられたC側では「JVM_InternString」に委譲
ちなみに「JVM_InternString」はjvm.hの中でこんな感じで定義されている

/*
 * java.lang.String
 */
JNIEXPORT jstring JNICALL
JVM_InternString(JNIEnv *env, jstring str);

関数ポインタが登録されているので、JVM_InternStringの実体が定義されているソースファイルを探す。
JVMがつく関数は、基本HotSpotのディレクトリの中を探すといいよと天使がささやいていたので探します。
Grepをかけてみるとhotspot/src/share/vm/prims/jvm.cppにありました。

hotspot/src/share/vm/prims/jvm.cpp

// String support ///////////////////////////////////////////////////////////////////////////

JVM_ENTRY(jstring, JVM_InternString(JNIEnv *env, jstring str))
  JVMWrapper("JVM_InternString");
  JvmtiVMObjectAllocEventCollector oam;
  if (str == NULL) return NULL;
  oop string = JNIHandles::resolve_non_null(str);
  oop result = StringTable::intern(string, CHECK_NULL);
  return (jstring) JNIHandles::make_local(env, result);
JVM_END

ふむふむ。さらにStringTable::internを追う。

hotspot/src/share/vm/classfile/symbolTable.hpp

  // Interning
  static oop intern(symbolOop symbol, TRAPS);
  static oop intern(oop string, TRAPS);
  static oop intern(const char *utf8_string, TRAPS);

hppだけど実装はあんまなくて本体はcppのほうにあるらしい。

hotspot/src/share/vm/classfile/symbolTable.cpp

oop StringTable::intern(oop string, TRAPS)
{
  if (string == NULL) return NULL;
  ResourceMark rm(THREAD);
  int length;
  Handle h_string (THREAD, string);
  jchar* chars = java_lang_String::as_unicode_string(string, length);
  oop result = intern(h_string, chars, length, CHECK_NULL);
  return result;
}

ユニコードにしたものを更にintern(h_string, chars, length, CHECK_NULL);にぶち込む。

oop StringTable::intern(Handle string_or_null, jchar* name,
                        int len, TRAPS) {
  unsigned int hashValue = hash_string(name, len);
  int index = the_table()->hash_to_index(hashValue);
  oop string = the_table()->lookup(index, name, len, hashValue);

  // Found
  if (string != NULL) return string;
  
  // Otherwise, add to symbol to table
  return the_table()->basic_add(index, string_or_null, name, len,
                                hashValue, CHECK_NULL);  
}

ハッシュ化してテーブルの中にあればそれを使い、無ければ追加して返却するってことか。

結局

結局まとめると、Stringをユニコードに変換してMapに入れて管理してますよと。StringTableの中身は普通に文字列を保持するMapだった。なにか特殊なことをしてるのかと思っていたけれど全くそんなことは無いようだ。

なんだ〜JNIコールしてるからもっとすんごいことしてるのかと思ってたよ。そりゃ解説もあんまないわな。ちなみにMapの初期保持数はsymbolTable.hppに定義されていて1009だった。増えた時の対処は特に無し。OPENJVMだからなか〜?


しかしとなるとなんでJNIで実装されてるんだろう。pureJavaで書いてもできそうなのに。。
やっぱHandleとスレッドのあたりの問題なのかな、、、、

正直symbolTable.cppのこの辺りの意味が良く理解できない。。Cの力の無さが露呈しますね。

  ResourceMark rm(THREAD);
  int length;
  Handle h_string (THREAD, string);

うーむ。。。色々かんがえてたらわかんないことだらけだ、、ちゃんとCやらないとダメだな。。
JNI呼出時にメモリがどこに確保されるのかとかも良く分かってないし、結局知りたかった「new String」で生成した場合どの瞬間にメモリが無駄になってその後無駄に出来た文字列型に対するメソッドがどのように振舞うのかも理解できんかった。


この辺りを理解するにはベースが足りないな。。修行しなおさないとだ。