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

投稿者: | 2022年8月11日

これまでの記事のリンク

前回までで大分疑問は解消されてきました、残る疑問は

  • 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_ARGSJAVA_EXTRA_ARGS という定数が設定されていると、const_jargsconst_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_prognameconst_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_MemAllocJLI_MemFreemallocfree のラッパーです
    • SelectVersion は引数の互換性などを確認しているようです。また、-jar オプションが指定されている場合に後続の引数がちゃんと指定されているかもここで確認しています
    • CreateExecutionEnvironment の中でに対してmusl_libcつかってる場合や、AIXを使っている場合には LD_LIBRARY_PATH を設定したりするようです。
    • LoadJavaVM では、libjvmが正しく読めるかを確認して、JNI_CreateJavaVMJNI_GetDefaultJavaVMInitArgsJNI_GetCreatedJavaVMs といった関数のポインタを取得します
    • TranslateApplicationArgs では、-J 始まりのオプションを先頭の方に来るように並び替える
      • –classpathを指定している場合にはワイルドカードの展開などを実行する
        • --class-path--classpath-cp--classpath= の4種類の指定方法がある
    • 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 の中を見ていきたいと思います。

コメントを残す

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