Javaのmainメソッドの戻り値はvoidだが、リターンコードはどうなっているのか

はじめに

こんにちは。divxエンジニアの石川です。
先日Java Silver合格目指して勉強していたのですが、Javaではmainメソッドの戻り値をvoidにしないといけないことが気になりました。
試しに戻り値をint型にしてみると、コンパイルは通るものの、実行時に「mainメソッドはpublic static void main(String[] args)って感じで宣言せい」とエラーになります。
returnIntTest.java

public class returnIntTest{
    public static int main(String[] args){
        return 0;
    }
}

C言語のmain関数では戻り値をint型にできますが、Javaのmainメソッドでは戻り値をvoid以外にできません。
では、Javaで作成したプログラムは呼び出し元にリターンコードを送らないのか?それはマズいのでは?そもそもなぜ戻り値をvoidにしているのか?
そのような疑問から調査しました。

先に結論

  • Javaのプログラムは本当にリターンコードを送らないのか?
    →Javaはリターンコードを送っている
    • System.exit()やRuntime.getRuntime().halt()でリターンコードをセットできる
    • System.exit()やRuntime.getRuntime().halt()をしてなくても、JVMがリターンコードを自動でセットしている
  • そもそもなぜJavaの戻り値はvoidなのか?
    →(仮説)以下の理由から、Javaの戻り値をvoidに設計し、コーダーがreturnを意識する必要がないようにしているのではないか
    • リターンコードには特別な意味付けがされている値があり、いたずらに値を入れるべきではない
    • リターンコードはOSによって利用可能な範囲が異なるため、実行環境が変わった場合に、想定した値が戻らない可能性がある

mainメソッドの戻り値がvoidだと、リターンコードが返せないのでは?

C言語におけるmain関数の戻り値はトクベツ

そもそもなぜ「mainメソッドの戻り値」についてフォーカスしているのかを説明します。
C言語のプログラムでは、main関数の戻り値は、リターンコードとして呼び出し元へ返されます。
returnCodeTest.c

#include <stdlib.h>
int main(int argc, char* args[]){
    return atoi(args[1]);
}

main以外のメソッドの戻り値は、呼び出し元のメソッドに値を受け渡すという、プログラム内で完結するやりとりです。
mainメソッドの戻り値は、リターンコードとして、そのプログラムを実行したもの(シェルや他のプログラム)に対して返されます。
C言語におけるmain関数の戻り値は、プログラム外にも影響があるやりとりなので、トクベツなのです。

リターンコードが本当にないと何がマズいのか

C言語はmain関数の戻り値がリターンコードになるとして、Javaはmainメソッドの戻り値がありません。
では、リターンコードもないのでしょうか。
そもそも、リターンコードがないとしたら、何が困るんでしょうか?

  • リターンコードは実行結果を返す
    リターンコードは、そのコマンドやプログラムの実行元に対し、実行結果を返します。

    • 0
      成功を表します。
    • 1 ~ 255
      失敗を表します。
      1はエラー全般、2はビルドインシェルコマンドの誤用による失敗、127は存在しないコマンドを実行した・・・などなど、意味付けされているリターンコードもあります。
      特別な意味を持つリターンコード一覧:https://tldp.org/LDP/abs/html/exitcodes.html
  • リターンコードは色々な場面で利用されている

    • 例1・bashの&&
      &&を使うと、「コマンドが成功した場合(リターンコードが0だった場合)のみ、次のコマンドを実行する」という書き方ができます。
      returnCodeCommboTest.c

        #include <stdio.h>
        #include <stdlib.h>
        #include <string.h>
        int main(int argc, char* args[]){
          printf(strcat(args[1], "を返す\n"));
            return atoi(args[1]);
        }
      

    • 例2・Automator
      MacのAutomatorで、ワークフローに「シェルスクリプトを実行」を組み込んでみましょう。
      リターンコードに0を設定しているパターンでは、ワークフローが無事に完了します。
      リターンコードに1を設定しているパターンでは、実行失敗判定になります。
      returnCodeTest.c

        #include <stdlib.h>
        int main(int argc, char* args[]){
            return atoi(args[1]);
        }
      

リターンコードは実行結果を表し、色々な場面で利用されていることがわかりました。
ではでは、戻り値がvoidになっているJavaのmainメソッドからは、リターンコードは戻せないのでしょうか?

Javaもリターンコードを返している

もちろんJavaのプログラムもリターンコードを返しています。

  • Javaのリターンコードは、JVMが自動で返している
    下記のコードのcase 0, 1, 2にあたります。
    正常終了で0、異常終了で1を返します。
  • Javaのリターンコードは、System.exit()やRuntime.getRuntime().halt()で任意の値を返せる
    下記のコードのcase3, 4にあたります。
    任意の値を返します。

ReturnCodeSample.java

public class ReturnCodeSample {
    public static void main(String[] args) throws Exception{
        int num = Integer.parseInt(args[0]);
        
        switch(num){
        case 0:
            System.out.println("do nothing");
            break;
        case 1:
            System.out.println("return");
            return;
        case 2:
            System.out.println("throw new Exception;");
            throw new Exception("");
        case 3:
            System.out.println("System.exit(100)");
            System.exit(100);
        case 4:
            System.out.println("halt(200)");
            Runtime.getRuntime().halt(200);
        }
    }
}

リターンコードはJVMのどこでセットされるのか?

公式ドキュメントによると、JVMは、次の①か②どちらかの条件が発生するまでmainスレッドを実行し続けます。
Thread (Java Platform SE 8)

  • ① exitした場合
    Runtimeクラスのexit()が呼び出され、セキュリティ・マネージャーがexit動作を許可した場合
  • ② 非デーモン・スレッドがすべて終了した場合
    run()の呼び出しから復帰することによって、またはrun()以外から送られる例外をスローすることによって、デーモン・スレッドではないすべてのスレッドが終了した場合

この①と②でリターンコードをセットしている場所を見てみましょう。

① exitした場合

  • System.exit()で終了
  • Runtime.getRuntime().halt()で終了

exitした場合は、下記のメソッドを呼び出し、JVMを停止します。
こちらより引用 http://hsmemo.github.io/articles/no2935jtd.html

java.lang.System.exit()
-> java.lang.Runtime.exit()
   -> java.lang.Shutdown.exit()
      -> java.lang.Shutdown.sequence()
      -> java.lang.Shutdown.halt()           (← ここまでが Java の世界. 以下は HotSpot 内部の世界)
         -> Java_java_lang_Shutdown_halt0()
            -> JVM_Halt()
               -> before_exit()
               -> vm_exit()
                  -> VM_Exit::doit()         (← ここは飛ばしていきなり vm_direct_exit() に行くパスもある)
                     -> vm_direct_exit()
                        -> exit()

Runtime.getRuntime().halt()は、Shutdown.halt()を呼び出します。
System.exit()やRuntime.getRuntime().halt()の引数にセットした値は、最終的にHotSpot(JVM)の世界で、java.cppファイルにあるvm_direct_exit()で::exit()に渡されます。
このC++の::exit(N)で、呼び出し元にリターンコードをセットしています。
openjdk-jdk8u-master/hotspot/src/share/vm/runtime/java.cppのvm_direct_exit()

void vm_direct_exit(int code) {
  notify_vm_shutdown();
  os::wait_for_keypress_at_exit();
  ::exit(code);
}

② 非デーモン・スレッドがすべて終了した場合

  • 何もせず終了
  • returnで終了
  • Exceptionで終了

Javaのmainメソッドは、HotSpotのJavaMain()というメソッドから実行されます。
以下は、HotSpot内部のメソッドであるJavaMain()で、Javaのmainメソッドを実行している部分です。
変数retに、mainがExceptionを投げていた場合は1を、そうでなければ0をセットしています。
jdk1.8.0_202.jdk/Contents/Home/src/launcher/java.cのJavaMain()

int JNICALL
JavaMain(void * _args)
{
    ...省略...

    /* Invoke main method. */
    (*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);

    /*
     * The launcher's exit code (in the absence of calls to
     * System.exit) will be non-zero if main threw an exception.
     */
    ret = (*env)->ExceptionOccurred(env) == NULL ? 0 : 1;
    LEAVE();
}

この変数retは、最終的にHotSpotのmain関数のreturnに渡されます。
このC言語のreturn Nで、呼び出し元にリターンコードをセットしています。

そもそも、なぜ非デーモン・スレッドがすべて終了した場合はJVMがリターンコードをセットしているのか?(自分の仮説です)

以下の理由から、コーダーがリターンコードを意識する必要がないように設計されているのではないかと考えました。

  • リターンコードには特別な意味付けがされている値があり、いたずらに値を入れるべきではない

    リターンコード2はビルドインシェルコマンドの誤用による失敗、127は存在しないコマンドを実行した・・・という意味付けを知らない方も多いでしょう。
    うっかり特別な意味付けがされているリターンコードを使ってしまわないよう、JVM側で適切な値を返しているのではないでしょうか?

  • リターンコードはOSによって利用可能な範囲が異なるため、実行環境が変わった場合に、想定した値が戻らない可能性がある
    Windowsだと符号付き整数32bit(−2,147,483,648〜2,147,483,647)
    Unix系だと符号無し整数8bit(0〜255)

以上です。

余談ですが、この記事書くのに「CもC++もJavaも試したいけど、環境構築面倒だわね・・・paiza.ioだとリターンコード見られないしなー」って思ってたらAWS Cloud9が大活躍してくれました。最高。

divxでは一緒に働ける仲間を募集しています。
興味があるかたはぜひ採用ページを御覧ください。

divx.co.jp

検証環境

  • AmazonLinux2
    • AMI名:Cloud9AmazonLinux2-2022-06-14T11-29
    • AMI ID:ami-011cfcf7a08034ef3
    • gcc:7.3.1 20180712 (Red Hat 7.3.1-15)
    • javac:1.8.0_312
    • java:1.8.0_312
  • Mac(Automator.appの検証のみ)
    • macOS Monterey:12.4
    • Automator:2.10 (512)
    • gcc:Apple clang version 13.1.6 (clang-1316.0.21.2.5)

参考資料

OpenJDKの設計と実装に関する備忘録
http://hsmemo.github.io/index.html

Appendix E. Exit Codes With Special Meanings
https://tldp.org/LDP/abs/html/exitcodes.html

Java SE 8ソースコード
https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html

HotSpotソースコード
https://github.com/AdoptOpenJDK/openjdk-jdk8u

クラスThread
https://docs.oracle.com/javase/jp/8/docs/api/java/lang/Thread.html

終了ステータス
https://shellscript.sunone.me/exit_status.html