これまでの記事のリンク
- 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
前回までで大分疑問は解消されてきました、残る疑問は
- classpathの取り扱い
- JVM自体がバイトコードをどのように解釈して実行しているのか
classpathの理解
今回は、classpathの周りを理解したいと考えています。ですが、どこから読んでいけばいいのか分からないのでもう少し整理しようかと思います。
自分が今までのclasspathというものの理解としては、
- Javaが起動時にライブラリなんかを見に行くパス
- Jarとかを置いとくと読みこんでくれる
くらいの理解です。そんなに誤解してたりはしないと思いますので、最終的な理解としてはコード読んでも変わらないとは思っています。
ただ、いくつかの項目は分かっていません
- java -jar などとするときの挙動の違い
- Javaが起動時にライブラリを読むタイミング
- module、jar、classファイルと色々読むファイルがあるが、それらの扱いの違い
この辺りが少しでも理解できれば良いなと思っています
関連しそうな仕様など
仕様などを読めば分かることも多いと思います。
パッケージとモジュールについてはOracleの言語仕様が参考になりそうです。https://docs.oracle.com/javase/specs/jls/se18/html/jls-7.html
JarファイルについてのOracleの仕様としてはこれが参考になりそうです。
https://docs.oracle.com/javase/7/docs/technotes/guides/jar/jar.html
読んでいく箇所
これは悩んだのですが、良い場所を思いつかないので、java コマンドの main から順番に読んでいこうかと思います。独自にクラスローダを呼べたり、JNIの中でもClassLoaderを呼んだりしていて、いつでもクラスローダは動きそうなので確実に呼ぶ箇所というと main から順番に読むのが良さそうだなと考えました、起動時処理についても理解が深まって一石二鳥でもあります。
main関数
この手の大きなソフトウェアは main 見つからないというのが良くある話なのですが、java コマンドの main はあっさり見つかりました。
/src/java.base/share/native/launcher/main.c
では、順番に読んでいこうかと思います。
mainJLI_Listにコマンドライン引数(argc, argv)を格納していきますJAVA_ARGSやJAVA_EXTRA_ARGSという定数が設定されていると、const_jargs、const_extra_jargsといった変数に代入されていますgrepとか gdbで色々見てみると、javacなどの他のプログラムもこのmain関数を使って実装されているようです。javacの 場合は、-DJAVA_ARGS='{ "-J--add-modules", "-JALL-DEFAULT", "-J-ms8m", "-m", "jdk.compiler/com.sun.tools.javac.Main", }が指定されてコンパイルされるようですjavaコマンドの場合は、JAVA_ARGSは指定されていないようです
- 同様に、
const_progname、const_launcherもコンパイル時に決定されるようですjavacの場合は、-DJAVA_ARGS='{ "-J--add-modules", "-JALL-DEFAULT", "-J-ms8m", "-m", "jdk.compiler/com.sun.tools.javac.Main", }がコンパイル時に渡されています
JLI_Launch- /src/java.base/share/native/libjli/java.c#L227
- 途中で出てくる
JLI_MemAlloc、JLI_MemFreeはmalloc、freeのラッパーです SelectVersionは引数の互換性などを確認しているようです。また、-jarオプションが指定されている場合に後続の引数がちゃんと指定されているかもここで確認していますCreateExecutionEnvironmentの中でに対してmusl_libcつかってる場合や、AIXを使っている場合にはLD_LIBRARY_PATHを設定したりするようです。LoadJavaVMでは、libjvmが正しく読めるかを確認して、JNI_CreateJavaVM、JNI_GetDefaultJavaVMInitArgs、JNI_GetCreatedJavaVMsといった関数のポインタを取得しますTranslateApplicationArgsでは、-J始まりのオプションを先頭の方に来るように並び替える- –classpathを指定している場合にはワイルドカードの展開などを実行する
--class-path、--classpath、-cp、--classpath=の4種類の指定方法がある
- –classpathを指定している場合にはワイルドカードの展開などを実行する
AddApplicationOptionsCLASSPATH環境変数(-Denv.class.path)やホームディレクトリの設定(-Dapplication.home)を行う。-Djava.class.pathも追加するコードが残っているが、引数に渡されるパターンは存在していないように見える。JLI_Launchの引数としては渡せるようだが、javaコマンドなどでは利用されていない
ParseArguments関数で引数の中でJVM自体に解釈させる必要があるものは、ここで一端処理する
ParseArgumentsjavaコマンドは-mなどのメインの場所を指定するオプション、-jarのオプション以降のオプションは実行するプログラム側のオプションと解釈する
--class-path、--classpath、-cp、--classpath=- この辺りの引数を処理するために
SetClassPathが呼ばれる
- この辺りの引数を処理するために
SetClassPath- 指定されたパスをクラスパスとして追加する(
-Djava.class.pathで追加) - ワイルドカードは展開して、
.jarファイルだけが抽出される
- 指定されたパスをクラスパスとして追加する(
JVMInitを呼び出す
ParseArguments の中で、-m などの処理は、以下のように break してそのまま引数処理が終わってしまう感じになっていました。
if (JLI_StrCmp(arg, "-jar") == 0) {
ARG_CHECK(argc, ARG_ERROR2, arg);
mode = checkMode(mode, LM_JAR, arg);
} else if (JLI_StrCmp(arg, "--module") == 0 ||
JLI_StrCCmp(arg, "--module=") == 0 ||
JLI_StrCmp(arg, "-m") == 0) {
REPORT_ERROR (has_arg, ARG_ERROR5, arg);
SetMainModule(value);
mode = checkMode(mode, LM_MODULE, arg);
if (has_arg) {
*pwhat = value;
break;
}あと、この辺で面白かったのは
} else if (JLI_StrCmp(arg, "-version") == 0) {
printVersion = JNI_TRUE;
return JNI_TRUE;
} else if (JLI_StrCmp(arg, "--version") == 0) {
printVersion = JNI_TRUE;
printTo = USE_STDOUT;
return JNI_TRUE;
} -version と --version で出力先が変更されていたり、-fullversion や --full-version というオプションがあったりと色々知らないことがありました。精読はしていないので、時間があるときにまた読んでみたいです。まだまだ知らないオプションとかがあったりしそうです。
今回はここまで
ここまででJVMを起動する準備ができたようです。main から JLI_Launch を通してやっていたのは引数の処理が中心でした。
- クラスパスに関係がありそうな引数は、
--class-path、-cp、--class-path、--classpath=の4種類 CLASSPATH環境変数からも読む- ワイルドカードが利用できて、
jarファイルが抽出される - セパレータは
;が利用される - 上の引数は、解釈した結果として
-Djava.class.pathとしてJVMのオプションとして追加される
という感じになっているようです
- javaコマンドの引数は、
-jarや-m、--module、--module=の後は実行するアプリケーションの引数と解釈される。しかし、-Jの引数は並び替えが実行されてJVM側で解釈するようになっている。
ということも分かりました。
java何も分からないので大変勉強になりました。
今回はちょっと長くなりそうなので、いったんここで休憩です。引数の処理が終わったので、次はJVMInit の中を見ていきたいと思います。