blog

なぜプログラムを実行するとデフォルトでmainメソッドが呼び出されるのか?

では、誰がいつメインスレッドを開始したのでしょうか?それを知るためのソースコード。 上のコードはメイン・プロセスを保持したもので、他のコードは省略しました。 次に、JVMをロードし、実際にダイナミック...

Feb 21, 2020 · 11 min. read
シェア

Javaプログラムのmainメソッドを実行する場合、現在のスレッドがメイン・スレッドであることが知られています。

Thread.currentThread().getName()

では、このメインスレッドは誰がいつ始めたのでしょうか?ソースコードで調べてみましょう。

jvmのスタートアップエントリーはmain.cで、macでjvmをデバッグできるようになったので、以下のパラメーターで起動します。

java -Xss512K -XX:+UseConcMarkSweepGC -Xms512M Main arg1=think123 arg2=666
public class Main {   public static void main(String[] args) {       for(String arg: args) {           System.out.println("input arg : " + arg);       }       System.out.println("main thread name : " + Thread.currentThread().getName());   } }

ランチャーからjvmを起動するために、まずmain.cが作成されます。

 return JLI_Launch(margc, margv,
   jargc, (const char**) jargv,
   0, NULL,
   VERSION_STRING,
   DOT_VERSION,
   (const_progname != NULL) ? const_progname : *margv,
   (const_launcher != NULL) ? const_launcher : *margv,
   jargc > 0,
   const_cpwildcard, const_javaw, 0);

java.cファイルにJLI_Launchの実装があり、そのメイン・プロセスは次のとおりです。

  1. 実行環境の作成、主にjrepath/jvmpathの決定

  2. jvmの読み込み

  3. パージングパラメータ

  4. jvmの初期化、メインメソッドの実行

JNIEXPORT int JNICALL
JLI_Launch(int argc, char ** argv,              /* main argc, argv */
  int jargc, const char** jargv,          /* java args */
  int appclassc, const char** appclassv,  /* app classpath */
  const char* fullversion,                /* full version defined */
  const char* dotversion,                 /* UNUSED dot version defined */
  const char* pname,                      /* program name */
  const char* lname,                      /* launcher name */
  jboolean javaargs,                      /* JAVA_ARGS */
  jboolean cpwildcard,                    /* classpath wildcard*/
  jboolean javaw,                         /* windows-only javaw */
  jint ergo                               /* unused */
)
{
  char jvmpath[MAXPATHLEN];
  char jrepath[MAXPATHLEN];
  char jvmcfg[MAXPATHLEN];
  
  // 実行環境を作成する
  実行環境を作成する&argc, &argv,
                             jrepath, sizeof(jrepath),
                             jvmpath, sizeof(jvmpath),
                             jvmcfg,  sizeof(jvmcfg));
  if (!IsJavaArgs()) {
      SetJvmEnvironment(argc,argv);
  }
  ifn.CreateJavaVM = 0;
  ifn.GetDefaultJavaVMInitArgs = 0;
  // JVMをロードする
  もし(!LoadJavaVM(jvmpath, &ifn)) {
      return(6);
  }
  // パラメータを解析する
  もし(!ParseArguments(&argc, &argv, &mode, &what, &ret, jrepath)) {
      return(ret);
  }
  // JVMを初期化する
  return JVMInit(&ifn, threadStackSize, argc, argv, mode, what, ret);
}

上記のコードは、メイン・プロセスを維持するもので、他のコードは省略します。

実行環境の構築

void CreateExecutionEnvironment(int *pargc, char ***pargv,
  char jrepath[], jint so_jrepath,
  char jvmpath[], jint so_jvmpath,
  char jvmcfg[],  jint so_jvmcfg) {
    jboolean jvmpathExists;
    // 実行ファイルのパスを設定する。ここでは、パスはjava実行プログラムの絶対パスである。
    // /Users/xxx/jvm/jdk12-06222165c35f/build/macosx-x86_64-server-slowdebug/jdk/bin/java
    // JREPathとJDKPathは、このパスに基づいて計算される。
    SetExecname(*pargv);
    char * jvmtype    = NULL;
    int  argc         = *pargc;
    char **argv       = *pargv;
    // jreのパスを見つける
    もし(!GetJREPath(jrepath, so_jrepath, JNI_FALSE) ) {
        JLI_ReportErrorMessage(JRE_ERROR1);
        exit(2);
    }
   // コードの一部を省略する
    // jvmのパスを見つける
    もし(!GetJVMPath(jrepath, jvmtype, jvmpath, so_jvmpath)) {
        JLI_ReportErrorMessage(CFG_ERROR8, jvmtype, jvmpath);
        exit(4);
    }
    // mac os専用操作
    MacOSXスタートアップ;
   
    return;
}

jvmpath/jrepathの長さは1024バイトを超えることはできませんので、Javaをインストールするときにフォルダレベルが深すぎないように注意を払う必要があることに留意する必要があります!

上記のコードを実行すると、jvmpath/jrepathの値は以下のようになります。

ここでのjvmpathの値はlibjvm.dylibであり、使用するJVMダイナミック・リンク・ライブラリーであることに注意してください。

JVMのロード

次のステップはJVMのロードで、実際にはlibjvm.dylibダイナミック・リンク・ライブラリーをロードします。

boolean LoadJavaVM(const char *jvmpath, InvocationFunctions *ifn)
{
    Dl_info dlinfo;
    void *libjvm;
#ifndef STATIC_BUILD
  // dlopen (libjvm) 経由でダイナミックライブラリファイルを読み込む.dylib),を返し、ハンドルを返す。
  libjvm = dlopen(jvmpath, RTLD_NOW + RTLD_GLOBAL);
#else
  libjvm = dlopen(NULL, RTLD_FIRST);
#endif
  if (libjvm == NULL) {
      JLI_ReportErrorMessage(DLL_ERROR1, __LINE__);
      JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());
      return JNI_FALSE;
  }
  // dlsym関数を使ったlibjvmのJNI_CreateJavaVM関数のアドレスは、ifnのCreateJavaVMプロパティにバインドされている。
  ifn->CreateJavaVM = (CreateJavaVM_t)
      dlsym(libjvm, "JNI_CreateJavaVM");
  if (ifn->CreateJavaVM == NULL) {
      JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());
      return JNI_FALSE;
  }
  
  // dlsym関数を使ったlibjvmのJNI_GetDefaultJavaVMInitArgs関数のアドレスは、ifnのGetDefaultJavaVMInitArgsプロパティにバインドされる。
  ifn->GetDefaultJavaVMInitArgs = (GetDefaultJavaVMInitArgs_t)
      dlsym(libjvm, "JNI_GetDefaultJavaVMInitArgs");
  if (ifn->GetDefaultJavaVMInitArgs == NULL) {
      JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());
      return JNI_FALSE;
  }
  // 上記のように、libjvmのGetCreatedJavaVMs関数のアドレスをifnのGetCreatedJavaVMsプロパティにバインドする。
  ifn->GetCreatedJavaVMs = (GetCreatedJavaVMs_t)
  dlsym(libjvm, "JNI_GetCreatedJavaVMs");
  if (ifn->GetCreatedJavaVMs == NULL) {
      JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());
      return JNI_FALSE;
  }
  return JNI_TRUE;
}

LoadJVMは主に以下の2つのことを行います。

  1. dlopen による libjvm.dylib ダイナミック・リンク・ライブラリのロード
  2. InvocationFunctions 構造体のプロパティに DLL の関数をバインドします。

dlopenおよびシステム提供関数と組み合わせて使用されます。

コマンドライン引数の解析

ParseArguments関数では、主に-classpath、-version、-helpなどのコマンドラインパラメータで解析されますが、ここで最も重要なことは、-Xss、-Xmx、-Xmsこれらの3つのパラメータを解析することです、これらの3つのパラメータの形式と他の異なるため。これは、パラメータ名の後に特定のサイズ

単位はT(t)、G(g)、M(m)、K(k)の8つのうち1つだけ。

その他のパラメーターの解析と決定は、JVMの初期化時に行われます。

VMの初期化

ContinueInNewThread0(int (JNICALL *continuation)(void *), jlong stack_size, void * args) {
  int rslt;
  pthread_t tid;
  pthread_attr_t attr;
  pthread_attr_init(&attr);
  pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
  // スタックをセットアップする_size(-Xssパラメータが解析される)
  if (スタック_size > 0) {
    pthread_attr_setstacksize(&attr, stack_size);
  }
  pthread_attr_setguardsize(&attr, 0); // no pthread guard page on java threads
  // 最初のパラメータはスレッドプロンプトポインタ、2番目のパラメータはスレッド属性である。
  // 第3パラメータはスレッドのランタイム関数の開始アドレスで、第4パラメータはランタイム関数のパラメータである。
  if (pthread_create(&tid, &attr, (void *(*)(void*))continuation, (void*)args) == 0) {
    void * tmp;
    pthread_join(tid, &tmp);
    rslt = (int)(intptr_t)tmp;
  } else {
   
    rslt = continuation(args);
  }
  pthread_attr_destroy(&attr);
  return rslt;
}

pthread_create関数はスレッドを作成するためのもので、pthread_create関数で渡される第3パラメータ継続はJavaMainであり、これはJavaのスレッド実行メソッドに相当します。java.cにありますが、関数が長すぎるため、より重要な部分のみを残しました。

IEEE標準1003.1cは、標準のスレッドで定義され、それはPthreadと呼ばれるスレッドパッケージを定義し、ほとんどのUNIXシステムは、この規格をサポートしています。

int JNICALL JavaMain(void * _args)
{
   
 ... コードを省略する
  
  // JVMを初期化するCreateJavaVMメソッドは、ここのロジックはより複雑である。
  // jvmの初期化は、以下のような他のパラメータを解析し、チェックする。-XX:+UseConcMarkSweepGC
  if (!InitializeJVM(&vm, &env, &ifn)) {
    JLI_ReportErrorMessage(JVM_ERROR1);
    exit(1);
  }
  ret = 1;
  // 実行するクラスをロードする
  mainClass = LoadMainClass(env, mode, what);
  CHECK_EXCEPTION_NULL_LEAVE(mainClass);
 
  // メイン・メソッドのIDを取得する
  mainID = (*env)->GetStaticMethodID(env, mainClass, "main",
                                     "([Ljava/lang/String;)V");
  CHECK_EXCEPTION_NULL_LEAVE(mainID);
  // メイン・メソッドを呼び出す
  (*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);
  // すべての非デーモンが終了したら、VMを破棄する。
  LEAVE();
}

メイン・メソッドの呼び出しは、jni.cppのjni_invoke_staticメソッドを通して行われ、最終的にはJavaCalls::call(javaCalls.cpp)を通して行われます。

javaCalls::callメソッドは、Javaスレッドからのみ呼び出すことができます。

この時点でメインスレッドがスタート。

Read next

Netty ノート - RPC アプリケーションを手書きする

前のプロジェクトに基づいて、新しいサブプロジェクト03-netty-rpcを作成し、プロジェクトの依存関係とMavenの設定は、GitHubのプロジェクトリポジトリを参照してください。 RPCリクエストのデータ形式を定義する新しいクラスを作成します。リモートプロシージャコールでは、Nettyネットワークを介して送信する必要があるデータがあります。

Feb 21, 2020 · 10 min read