本文将详细介绍HotSpot的启动过程,启动过程涉及到的逻辑比较复杂,细节也比较多,为了让大家更快的了解这部分知识,我录制了对应的视频放到了B站上,大家可以参考。 

第4节-HotSpot的启动过程 

下面我们开始以文章的形式简单介绍一下启动过程。

HotSpot通常会通过java.exe或javaw.exe来调用/jdk/src/share/bin/main.c文件中的main()函数来启动虚拟机,使用Eclipse进行调试时,也会调用到这个入口。main.c的main()函数负责创建运行环境,以及启动一个全新的线程去执行JVM的初始化和调用Java程序的main()方法。main()函数最终会阻塞当前线程,同时用另外一个线程去调用JavaMain()函数。main()函数的调用栈如下: 

  1. main() main.c
  2. JLI_Launch() java.c
  3. JVMInit() java_md_solinux.c
  4. ContinueInNewThread() java.c
  5. ContinueInNewThread0() java_md_solinux.c
  6. pthread_join() pthread_join.c

调用链的顺序从上到下,下面简单介绍一下涉及到的相关方法。

首先就是main()方法,方法的实现如下: 

  1. 源代码位置:/openjdk/jdk/src/share/bin/main.c
  2.  
  3. #ifdef JAVAW
  4.  
  5. char **__initenv;
  6.  
  7. int WINAPI WinMain(HINSTANCE inst, HINSTANCE previnst, LPSTR cmdline, int cmdshow){
  8. int margc;
  9. char** margv;
  10. const jboolean const_javaw = JNI_TRUE;
  11.  
  12. __initenv = _environ;
  13.  
  14. #else /* JAVAW */
  15. int main(int argc, char **argv){
  16. int margc;
  17. char** margv;
  18. const jboolean const_javaw = JNI_FALSE;
  19. #endif /* JAVAW */
  20. #ifdef _WIN32
  21. {
  22. int i = 0;
  23. if (getenv(JLDEBUG_ENV_ENTRY) != NULL) {
  24. printf("Windows original main args:\n");
  25. for (i = 0 ; i < __argc ; i++) {
  26. printf("wwwd_args[%d] = %s\n", i, __argv[i]);
  27. }
  28. }
  29. }
  30. JLI_CmdToArgs(GetCommandLine());
  31. margc = JLI_GetStdArgc();
  32. // add one more to mark the end
  33. margv = (char **)JLI_MemAlloc((margc + 1) * (sizeof(char *)));
  34. {
  35. int i = 0;
  36. StdArg *stdargs = JLI_GetStdArgs();
  37. for (i = 0 ; i < margc ; i++) {
  38. margv[i] = stdargs[i].arg;
  39. }
  40. margv[i] = NULL;
  41. }
  42. #else /* *NIXES */
  43. margc = argc;
  44. margv = argv;
  45. #endif /* WIN32 */
  46. return JLI_Launch(margc, margv,
  47. sizeof(const_jargs) / sizeof(char *), const_jargs,
  48. sizeof(const_appclasspath) / sizeof(char *), const_appclasspath,
  49. FULL_VERSION,
  50. DOT_VERSION,
  51. (const_progname != NULL) ? const_progname : *margv,
  52. (const_launcher != NULL) ? const_launcher : *margv,
  53. (const_jargs != NULL) ? JNI_TRUE : JNI_FALSE,
  54. const_cpwildcard, const_javaw, const_ergo_class);
  55. }

这个方法是Windows、UNIX、Linux以及Mac OS操作系统中C/C++的入口函数,而Windows的入口函数和其它的不太一样,所以为了尽可能重用代码,这里使用#ifdef条件编译,所以对于基于Linux内核的Ubuntu来说,最终编译的代码其实是如下的样子:

  1. int main(int argc, char **argv){
  2. int margc;
  3. char** margv;
  4. const jboolean const_javaw = JNI_FALSE;
  5. margc = argc;
  6. margv = argv;
  7. return JLI_Launch(margc, margv,
  8. sizeof(const_jargs) / sizeof(char *), const_jargs,
  9. sizeof(const_appclasspath) / sizeof(char *), const_appclasspath,
  10. FULL_VERSION,
  11. DOT_VERSION,
  12. (const_progname != NULL) ? const_progname : *margv,
  13. (const_launcher != NULL) ? const_launcher : *margv,
  14. (const_jargs != NULL) ? JNI_TRUE : JNI_FALSE,
  15. const_cpwildcard, const_javaw, const_ergo_class);
  16. }

第一个参数,int型的argc,为整型,用来统计程序运行时发送给main函数的命令行参数的个数;第二个参数,char型的argv[],为字符串数组,用来存放指向的字符串参数的指针数组,每一个元素指向一个参数。

JLI_Launch()函数进行了一系列必要的操作,如libjvm.so的加载、参数解析、Classpath的获取和设置、系统属性的设置、JVM 初始化等。函数会调用LoadJavaVM()加载libjvm.so并初始化相关参数,调用语句如下:

  1. LoadJavaVM(jvmpath, &ifn)

其中jvmpath就是”/home/mazhi/workspace/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/lib/amd64/server/libjvm.so”,也就是libjvm.so的存储路径,而ifn是InvocationFunctions类型变量,InvocationFunctions的定义如下: 

  1. 源代码位置:/home/mazhi/workspace/openjdk/jdk/src/share/bin/java.h
  2. typedef jint (JNICALL *CreateJavaVM_t)(JavaVM **pvm, void **env, void *args);
  3. typedef jint (JNICALL *GetDefaultJavaVMInitArgs_t)(void *args);
  4. typedef jint (JNICALL *GetCreatedJavaVMs_t)(JavaVM **vmBuf, jsize bufLen, jsize *nVMs);
  5.  
  6. typedef struct {
  7. CreateJavaVM_t CreateJavaVM;
  8. GetDefaultJavaVMInitArgs_t GetDefaultJavaVMInitArgs;
  9. GetCreatedJavaVMs_t GetCreatedJavaVMs;
  10. } InvocationFunctions;

可以看到结构体InvocationFunctions中定义了3个函数指针,3个函数的实现在libjvm.so这个动态链接库中,查看LoadJavaVM()函数后就可以看到有如下实现:

  1. ifn->CreateJavaVM = (CreateJavaVM_t) dlsym(libjvm, "JNI_CreateJavaVM");
  2. ifn->GetDefaultJavaVMInitArgs = (GetDefaultJavaVMInitArgs_t)dlsym(libjvm, "JNI_GetDefaultJavaVMInitArgs");
  3. ifn->GetCreatedJavaVMs = (GetCreatedJavaVMs_t) dlsym(libjvm, "JNI_GetCreatedJavaVMs");

所以通过函数指针调用时,最终会调用到libjvm.so中对应的以JNI_Xxx开头的方法,其中JNI_CreateJavaVM()方法会在InitializeJVM()函数中调用,用来初始化2个JNI调用时非常重要的2个参数JavaVM和JNIEnv,后面在介绍JNI时还会详细介绍,这里不做过多介绍。

JVMInit()函数的源代码如下: 

  1. 源代码位置:/openjdk/jdk/src/solaris/bin/java_md_solinux.c
  2. int JVMInit(InvocationFunctions* ifn, jlong threadStackSize,
  3. int argc, char **argv,
  4. int mode, char *what, int ret){
  5. // ...
  6. return ContinueInNewThread(ifn, threadStackSize, argc, argv, mode, what, ret);
  7. }

这个方法中没有特别的逻辑,直接调用continueInNewThread()函数继续执行相关逻辑。

在JVMInit()函数中调用的ContinueInNewThread()函数的实现如下: 

  1. 源代码位置:/openjdk/jdk/src/share/bin/java.c
  2. int ContinueInNewThread(InvocationFunctions* ifn, jlong threadStackSize,
  3. int argc, char **argv,
  4. int mode, char *what, int ret){
  5. ...
  6. { /* Create a new thread to create JVM and invoke main method */
  7. JavaMainArgs args;
  8. int rslt;
  9.  
  10. args.argc = argc;
  11. args.argv = argv;
  12. args.mode = mode;
  13. args.what = what;
  14. args.ifn = *ifn;
  15.  
  16. rslt = ContinueInNewThread0(JavaMain, threadStackSize, (void*)&args);
  17. /* If the caller has deemed there is an error we
  18. * simply return that, otherwise we return the value of
  19. * the callee
  20. */
  21. return (ret != 0) ? ret : rslt;
  22. }
  23. }

在调用ContinueInNewThread0()函数时,传递了JavaMain函数指针和调用此函数需要的参数args。

ContinueInNewThread()函数调用的ContinueInNewThread0()函数的实现如下: 

  1. 位置:/openjdk/jdk/src/solaris/bin/java_md_solinux.c
  2. int ContinueInNewThread0(int (JNICALL *continuation)(void *), jlong stack_size, void * args) {
  3. int rslt;
  4. ...
  5. pthread_t tid;
  6. pthread_attr_t attr;
  7. pthread_attr_init(&attr);
  8. pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
  9.  
  10. if (stack_size > 0) {
  11. pthread_attr_setstacksize(&attr, stack_size);
  12. }
  13.  
  14. if (pthread_create(&tid, &attr, (void *(*)(void*))continuation, (void*)args) == 0) {
  15. void * tmp;
  16. pthread_join(tid, &tmp); // 当前线程会阻塞在这里
  17. rslt = (int)tmp;
  18. }
  19.  
  20. pthread_attr_destroy(&attr);
  21. ...
  22. return rslt;
  23. }

Linux 系统下(后面所说的Linux系统都是指基于Linux内核的操作系统)创建一个 pthread_t 线程,然后使用这个新创建的线程执行JavaMain()函数。

方法的第一个参数int (JNICALL continuation)(void )接收的就是JavaMain()函数的指针。关于指针函数与函数指针、以及Linux下创建线程的相关知识点后面会介绍,到时候这里会给出链接。

下面就来看一下JavaMain()函数的实现,如下: 

  1. 位置:/openjdk/jdk/src/share/bin/java.c
  2. int JNICALL JavaMain(void * _args){
  3.  
  4. JavaMainArgs *args = (JavaMainArgs *)_args;
  5. int argc = args->argc;
  6. char **argv = args->argv;
  7. InvocationFunctions ifn = args->ifn;
  8.  
  9. JavaVM *vm = 0;
  10. JNIEnv *env = 0;
  11. jclass mainClass = NULL;
  12. jclass appClass = NULL; // actual application class being launched
  13. jmethodID mainID;
  14. jobjectArray mainArgs;
  15.  
  16. // InitializeJVM 初始化JVM,给JavaVM和JNIEnv对象正确赋值,通过调用InvocationFunctions结构体下
  17. // 的CreateJavaVM()函数指针来实现,该指针在LoadJavaVM()函数中指向libjvm.so动态链接库中JNI_CreateJavaVM()函数
  18. if (!InitializeJVM(&vm, &env, &ifn)) {
  19. JLI_ReportErrorMessage(JVM_ERROR1);
  20. exit(1);
  21. }
  22. // ...
  23.  
  24. mainClass = LoadMainClass(env, mode, what);
  25.  
  26. appClass = GetApplicationClass(env);
  27.  
  28. mainID = (*env)->GetStaticMethodID(env, mainClass, "main", "([Ljava/lang/String;)V");
  29.  
  30. /* Build platform specific argument array */
  31. mainArgs = CreateApplicationArgs(env, argv, argc);
  32.  
  33. /* Invoke main method. */
  34. (*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);
  35.  
  36. // ...
  37. }

代码主要就是找出Java源代码的main()方法,然后调用并执行。

  1. 调用InitializeJVM()函数初始化JVM,主要就是初始化2个非常重要的变量JavaVM与JNIEnv,在这里不过多探讨这个问题,后面在讲解JNI调用时会详细介绍初始化过程。
  2. 调用LoadMainClass()函数获取Java程序的启动类,对于前面举过的实例来说,由于配置了参数 “com.test/Test”, 所以会查找com.test.Test类。LoadMainClass()函数最终会调用libjvm.so中实现的JVM_FindClassFromBootLoader()方法来查找启动类,涉及到的逻辑比较多,后面在讲解类型的加载时会介绍。
  3. 调用GetStaticMethodId()函数查找Java启动方法,其实就是获取Test类中的main()方法。
  4. 调用JNIEnv中定义的CallStaticVoidMethod()方法,最终会调用JavaCalls::call()函数执行Test类中的main()方法。JavaCalls:call()函数是个非常重要的方法,后面在讲解方法执行引擎时会详细介绍。

以上步骤都还在当前线程的控制下。当控制权转移到Test.main()之后当前线程就不再做其它事儿了,等Test.main()函数返回之后,当前线程会清理和关闭JVM。调用本地函数jni_DetachCurrentThread()断开与主线程的连接。当成功与主线程断开连接后,当前线程一直等待程序中所有的非守护线程全部执行结束,然后调用本地函数jni_DestroyJavaVM()对JVM执行销毁。

其它文章:

1、在Ubuntu 16.04上编译OpenJDK8的源代码(配视频)  

2、调试HotSpot源代码(配视频)

搭建过程中如果有问题可直接评论留言或加作者微信mazhimazh。

作者持续维护的个人博客  classloading.com

关注公众号,有HotSpot源码剖析系列文章!

   

 

版权声明:本文为mazhimazhi原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/mazhimazhi/p/13997925.html