Javaが分からなすぎるので、JVMについて勉強する(5) -ClassLoader編 その2- 

投稿者: | 2022年8月21日

少し前回から時間があいてしまいました。
前回はjava コマンドの main 関数から順番に読んでいって、オプションの解釈が終わったところまでを読みました。その続きを読んでいきます。早くクラスローダー関係のところをまで辿りつきたいですね。

今回読むところと記録つけかたの変更

Javaの main 関数を実行するところまで、ということにしたいですが全部は難しいと思うので

  • classpathの設定を行なうところ
  • .class ファイルを読みこんでいるところ

の特定や処理の一部が分かれば良いかなと思っています。

そろそろ、やっていることの処理が増えてきたので記録の取り方を少し変えます。意味がありそうな単位で節を区切る、コード片を併記するような形で記録していこうかと思います。

Javaプログラムの起動処理

前回は JLI_LaunchJVMInitialize を呼ぶところで終了したので、今回はそこから読んでいきます。
この関数は名前のとおりJVMの初期化を行なっていそうです。ここから、JavaMain 関数を実行するまでは重要な処理は無い感じなのでざっくり読みます。

JVMInit は、ContinueInNewThread を呼ぶだけの関数になっています。そして、ContinueInNewThreadは、CallJavaMainInNewThreadを呼んでJavaMainの実行に移ります。

CallJavaMainInNewThreadは、OS依存のスレッドの初期化処理を行っています。unix版を読んでいるのでpthreadが呼ばれます。以下のように pthread_create して、pthread_join する処理になっています。

if (pthread_create(&tid, &attr, ThreadJavaMain, args) == 0) {
        void* tmp;
        pthread_join(tid, &tmp);
        rslt = (int)(intptr_t)tmp;
    }

//  ThreadJavaMainの定義は以下のようになっています
static void* ThreadJavaMain(void* args) {
    return (void*)(intptr_t)JavaMain(args);
}

この後に、returnがあって、特に大きな処理もなくプログラムとしても終了してしまいます。つまり、ここで作成しているスレッドこそがJavaで実行されるプログラムとなります。mainから読んできた、lanuncherの機能としてはすべて完了していると言えそうです。

JavaMainの処理

JavaMainの中では色々初期化っぽい処理が実施されていそうです。ざっくり以下の順でJavaのmain関数が実行されているようです。

  • InitializeJVM
  • LoadMainClass
  • GetApplicationClass
  • CreateApplicationArgs
  • PostJVMInit
  • GetStaticMethodID
  • CallStaticVoidMethod

上記の関数のうち、InitializeJVMLoadMainClassCallStaticVoidMethod あたりは気になる関数ですね。他の関数は、GUI用であるとかJavaFX用の処理とコメントが書いてあるので読み飛ばしても良さそうです。

InitializeJVMの処理

InitializeJVMを呼びだすコードは以下のようになっています。

    /* Initialize the virtual machine */
    start = CurrentTimeMicros();
    if (!InitializeJVM(&vm, &env, &ifn)) {
        JLI_ReportErrorMessage(JVM_ERROR1);
        exit(1);
    }

InitializeJVMの中身を見ていくと、冒頭で変数の初期化処理などが実施されています。
ここで登場している、options という変数は起動時に設定したJVMのパラメータなどが格納されている変数です。AddOption 関数などが操作しているのはこの変数となります。

    JavaVMInitArgs args;
    jint r;

    memset(&args, 0, sizeof(args));
    args.version  = JNI_VERSION_1_2;
    args.nOptions = numOptions;
    args.options  = options;
    args.ignoreUnrecognized = JNI_FALSE;

そして、InitializeJVMの中で以下のようにJVMを作成する関数が呼ばれています。

    r = ifn->CreateJavaVM(pvm, (void **)penv, &args);

この変数は関数ポインタとなっていて、JLI_Launch の中で LoadJavaVM で設定されています。実際に呼ばれる関数としては、JNI_CreateJavaVM という関数になります。

JNI_CreateJavaVM の中を見ていきます。この関数の実際の処理は、JNI_CreateJavaVM_inner が行なうようです。同期用の変数などを操作しながら以下の関数を呼んで、ここからVM作成の実際の処理が始まりそうです。

  result = Threads::create_vm((JavaVMInitArgs*) args, &can_try_again);

この Thraeds::create_vm の中を全て把握しようとするのは大変そうです。なので適当にアタリをつけて(関数定義をさっと読んでみて関係がありそうか判断してみました)読んでいきます。

  • vm_init_globals
  • init_globals

あたりは関係がありそうな名前と処理内容な気がします。
実際に軽く読んでみたところ、vm_init_globals はあまり関係が無さそうだったので、init_globals を読んでいこうかと思います。

init_globals のなかを見てみると……

  // 中略
  // すごく気になる関数!
  classLoader_init1(); 
  // ...
  universe2_init();  // dependent on codeCache_init and stubRoutines_init1
  javaClasses_init();// must happen after vtable initialization, before referenceProcessor_init
  interpreter_init_code();  // after javaClasses_init and before any method gets linked
  // ...

すごく気になる関数がありました。中を見ていくと……

void classLoader_init1() {
  EXCEPTION_MARK;
  ClassLoader::initialize(THREAD);
  if (HAS_PENDING_EXCEPTION) {
    vm_exit_during_initialization("ClassLoader::initialize() failed unexpectedly");
  }
}

ついに、ClassLoaderが表われました!!

ClassLoader::initialize の処理

ClassLoader::initialize の冒頭にコメントがあります。

// Initialize the class loader's access to methods in libzip.  Parse and
// process the boot classpath into a list ClassPathEntry objects.  Once
// this list has been created, it must not change order (see class PackageInfo)
// it can be appended to and is by jvmti.

libzipの初期化やboot classpathを処理して、ClassPathEntryのリストを作る処理のようです。
関数の内部を見てみると実際の処理は、以下の2つの行となっています。

  // lookup java library entry points
  load_java_library();
  // jimage library entry points are loaded below, in lookup_vm_options
  setup_bootstrap_search_path(THREAD);

load_java_librarylibjava.so の読み込みを行なう関数でした。JDK_Canonicalize関数を利用するために読み込むようです。

setup_bootstrap_search_path は、以下のような定義になっています。

void ClassLoader::setup_bootstrap_search_path(JavaThread* current) {
  const char* bootcp = Arguments::get_boot_class_path();
  assert(bootcp != NULL, "Boot class path must not be NULL");
  if (PrintSharedArchiveAndExit) {
    // Don't print bootcp - this is the bootcp of this current VM process, not necessarily
    // the same as the boot classpath of the shared archive.
  } else {
    trace_class_path("bootstrap loader class path=", bootcp);
  }
  setup_bootstrap_search_path_impl(current, bootcp);
}

Arguments::get_boot_class_path() はboot_classpathを読み取る処理になっています。

この変数は、Threads::create_vm の中で呼ばれている、Arguments::init_system_properties() の中で初期化されています。Arguments::init_system_properties() -> os::init_system_properties_values() -> os::set_boot_path と実行され、最終的にArguments::set_boot_class_path の中で設定されます。JAVA_HOME/lib/modules というファイルか、JAVA_HOME/modules/java.base のどちらかになるようです。

次に、setup_bootstrap_search_path_impl の処理を見ていきます。setup_bootstrap_search_path_impl はboot_classpathとして渡された class_path という文字列に対して、jimageの読み込み処理を実行します。

_jrt_entry = new ClassPathImageEntry(JImage_file, canonical_path);

Javaは、最近はmoduleという仕組みが導入されておりJava Image形式というフォーマットにも対応しています。その形式に対応するClassPathを追加します。exploded build という展開した形式を使った方式も残っているようです。この場合に boot classpathが JAVA_HOME/modules/java.base になるようです。

boot classpathに独自で追加のパスを書いたりしている場合は追加でclasspathが追加されたりする処理があったりするようです。update_class_path_entry_list でそれは処理されるようです。

ここまでで、boot classpathの設定が完了して、ClassLoaderの初期化が完了のようです。

ClassLoader::load_class について

classloaderクラスの読み込みが完了したので、ついでにClassLoaderに定義されている ClassLoader::load_class について調べてみます。名前のとおりクラスの読み込み処理が実装されていそうです。

javaをgdbで追いかけてみると、この関数が最初に呼ばれるときのスタックトレースは以下のようになっていました。

#0  ClassLoader::load_class (name=0x7fffc6b4e0f8, search_append_only=false, __the_thread__=0x7ffff00285c0)
#1  0x00007ffff6ce699c in SystemDictionary::load_instance_class_impl (class_name=0x7fffc6b4e0f8,
#2  0x00007ffff6ce6d35 in SystemDictionary::load_instance_class (name_hash=3867026853, name=0x7fffc6b4e0f8,
#3  0x00007ffff6ce4def in SystemDictionary::resolve_instance_class_or_null (name=0x7fffc6b4e0f8,
#4  0x00007ffff6ce39be in SystemDictionary::resolve_or_null (class_name=0x7fffc6b4e0f8, class_loader=...,
#5  0x00007ffff6ce37f5 in SystemDictionary::resolve_or_fail (class_name=0x7fffc6b4e0f8, class_loader=...,
#6  0x00007ffff5f2638b in SystemDictionary::resolve_or_fail (class_name=0x7fffc6b4e0f8, throw_error=true,
#7  0x00007ffff6dc5872 in vmClasses::resolve (id=vmClassID::Object_klass_knum, __the_thread__=0x7ffff00285c0)
#8  0x00007ffff6dc596c in vmClasses::resolve_until (limit_id=vmClassID::String_klass_knum,
#9  0x00007ffff6dc64d7 in vmClasses::resolve_through (last_id=vmClassID::Object_klass_knum,
#10 0x00007ffff6dc5a24 in vmClasses::resolve_all (__the_thread__=0x7ffff00285c0)
#11 0x00007ffff6ce7b9e in SystemDictionary::initialize (__the_thread__=0x7ffff00285c0)
#12 0x00007ffff6d63e48 in Universe::genesis (__the_thread__=0x7ffff00285c0)
#13 0x00007ffff6d663cf in universe2_init () at /home/sakura/sandbox/jdk/src/hotspot/share/memory/universe.cpp:969
#14 0x00007ffff6523bbc in init_globals () at /home/sakura/sandbox/jdk/src/hotspot/share/runtime/init.cpp:138
#15 0x00007ffff6d456c8 in Threads::create_vm (args=0x7ffff55c0dd0, canTryAgain=0x7ffff55c0cdb)

init_globalsuniverse2_init から始まる流れで利用されていそうです。Javaのクラスの読み込みなどを取りまとめている SystemDictionary の初期化の中で、vmClasses::resolve_all() で基本的なクラスの読み込み(resolve)を行なっているようです。

void SystemDictionary::initialize(TRAPS) {
  // Allocate arrays
  _placeholders        = new PlaceholderTable(_placeholder_table_size);
  _loader_constraints  = new LoaderConstraintTable(_loader_constraint_size);
  _invoke_method_table = new SymbolPropertyTable(_invoke_method_size);
  _pd_cache_table = new ProtectionDomainCacheTable(defaultProtectionDomainCacheSize);

#if INCLUDE_CDS
  SystemDictionaryShared::initialize();
#endif

  // Resolve basic classes
  vmClasses::resolve_all(CHECK);
  // Resolve classes used by archived heap objects
  if (UseSharedSpaces) {
    HeapShared::resolve_classes(THREAD);
  }
}

疲れたので次回に……

大分疲れてしまったので、ここから先は次にしようかと思います。

今回で、classpathの設定を行なっている箇所やクラスローダが実際に呼ばれて処理しているところの特定は出来ました。次回以降はこのクラスローダが何をしているのかを少し詳しく見て行きたいと思います。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です