これまでの記事のリンク
- 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
では、順番に読んでいこうかと思います。
main
JLI_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を指定している場合にはワイルドカードの展開などを実行する
AddApplicationOptions
CLASSPATH
環境変数(-Denv.class.path
)やホームディレクトリの設定(-Dapplication.home
)を行う。-Djava.class.path
も追加するコードが残っているが、引数に渡されるパターンは存在していないように見える。JLI_Launchの引数としては渡せるようだが、java
コマンドなどでは利用されていない
ParseArguments
関数で引数の中でJVM自体に解釈させる必要があるものは、ここで一端処理する
ParseArguments
java
コマンドは-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
の中を見ていきたいと思います。