- https://light-of-moe.ddo.jp/~sakura/diary/?p=680
- https://light-of-moe.ddo.jp/~sakura/diary/?p=743
- https://light-of-moe.ddo.jp/~sakura/diary/?p=773
- https://light-of-moe.ddo.jp/~sakura/diary/?p=931
少し前回から時間があいてしまいました。
前回はjava
コマンドの main
関数から順番に読んでいって、オプションの解釈が終わったところまでを読みました。その続きを読んでいきます。早くクラスローダー関係のところをまで辿りつきたいですね。
今回読むところと記録つけかたの変更
Javaの main
関数を実行するところまで、ということにしたいですが全部は難しいと思うので
- classpathの設定を行なうところ
.class
ファイルを読みこんでいるところ
の特定や処理の一部が分かれば良いかなと思っています。
そろそろ、やっていることの処理が増えてきたので記録の取り方を少し変えます。意味がありそうな単位で節を区切る、コード片を併記するような形で記録していこうかと思います。
Javaプログラムの起動処理
前回は JLI_Launch
が JVMInitialize
を呼ぶところで終了したので、今回はそこから読んでいきます。
この関数は名前のとおり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
上記の関数のうち、InitializeJVM
、LoadMainClass
、CallStaticVoidMethod
あたりは気になる関数ですね。他の関数は、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_library
は libjava.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_globals
の universe2_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の設定を行なっている箇所やクラスローダが実際に呼ばれて処理しているところの特定は出来ました。次回以降はこのクラスローダが何をしているのかを少し詳しく見て行きたいと思います。