首页 > 其他分享 >3. 初窥全貌 - main方法执行全流程

3. 初窥全貌 - main方法执行全流程

时间:2024-09-24 18:19:54浏览次数:8  
标签:java int 流程 args char 全貌 JNI main JLI

0. 前言

一个类被编译为class file之后,使用java命令去执行,暂时抛开OS层面的syscall 及 glibc的入口函数, java中的main方法执行,经历了什么样的过程?要执行main方法,必须要有vm支持,那vm又是如何去构建的?

本章我们把握整体流程,建立一个初步认识。


1. 整体流程

1.main()    main.c
        1.JLI_Launch()    java.c
                1.1 CreateExecutionEnvironment()      java_md_solinux.c
                        SetExecname()                              java_md_solinux.c              
                1.2 SetJvmEnvironment()                     java.c
                1.3 LoadJavaVM()                                java_md_solinux.c 通过syscall加载libjvm.so 或 jvm.dll
                1.4 SetClassPath(..)                             设置目标为class文件或jar包路径
                1.5 JvmInit()                                          java_md_solinux.c
                        1.5.1 ContinueInNewThread()      java.c
                        1.5.2 ContinueInNewThread0()    java_md_solinux.c, 创建并等待“main线程"运行结束。java栈的大小在linux64位下默认1024k,32位默认320k.
    

2.JavaMain()                                 java.c  main线程的入口
        2.1 InitializeJVM()                 java.c  初始化vm        
                JNI_CreateJavaVM()    jni.cpp
                        create_vm()           thread.cpp  创建vm的主要工作都在这个函数里完成      
        2.2 LoadMainClass()             加载main class
        2.3 CallStaticVoidMethod()   真正去执行java类中的main函数

2. main() main.c - 一切从这里开始

启动Java程序,首先需执行class文件。

使用如下命令即可在Linux系统的shell环境下启动Java程序

java [options] xxx.class param1 param2 ... paramn

当执行命令时,shell会创建新进程来执行该命令,由于JVM是用C/C++实现的,我们只需定位到main函数即可。至于glibc的入口函数,此处不再赘述。

main.c - main(..) / WinMain(..)

openjdk-jdk8u/jdk/src/share/bin/main.c

openjdk-jdk8u/jdk/src/share/bin/main.c

// 这里我们忽略掉源代码中关于windows部分的编译选项

int main(int argc, char **argv) 
{      
    int margc;     // 命令参数个数,从java开始数,空格隔开,假设命令是:java xxx.class 1 2 3,那么此时margc = 5    
    char** margv;  // 参数指针
    const jboolean const_javaw = JNI_FALSE;
    
    margc = argc;
    margv = argv;
    
    // 调用java.c的JLI_Launch函数
    return JLI_Launch(margc, margv,  
        sizeof(const_jargs) / sizeof(char *),         // const_jargs字符串个数 
        const_jargs,                                  // 编译选项[JAVA_ARGS],默认为空,一般只有java tools编译时使用JAVA_ARGS
        sizeof(const_appclasspath) / sizeof(char *),  // const_appclasspath的个数
        const_appclasspath,                           // 1. 编译选项[JAVA_ARGS][APP_CLASSPATH],为APP_CLASSPATH选项的值
                                                      // 2. 只有编译选项[JAVA_ARGS], 默认为{ "/lib/tools.jar", "/classes" }  
                                                      // 3. 为空    
        FULL_VERSION,                                 // 编译选项[DFULL_VERSION,编译时必须填,否则编译不过
        DOT_VERSION,                                  // 编译选项 [JDK_MAJOR_VERSION] "." [JDK_MINOR_VERSION] 组成,编译时必须填,否则不通过
        (const_progname != NULL) ? const_progname : *margv, // 编译选项[JAVA_ARGS] 或 [PROGNAME]确定,这里是java
        (const_launcher != NULL) ? const_launcher : *margv, // 编译选项[LAUNCHER_NAME],这里是java          
        (const_jargs != NULL) ? JNI_TRUE : JNI_FALSE,       // 在java 这里是false 
        const_cpwildcard, // 编译选项[EXPAND_CLASSPATH_WILDCARDS]决定,定义EXPAND_CLASSPATH_WILDCARDS 时为true,否则为false。 这里是true
        const_javaw,      // 编译选项[JAVAW决定,windows]下为true,linux下为false。
        const_ergo_class);// 编译选项[NEVER_ACT_AS_SERVER_CLASS_MACHINE] / [ALWAYS_ACT_AS_SERVER_CLASS_MACHINE],决定ergo_policy,可以决定jvm运行哪种模式下  
}

TIP:JDK编译选项

编译选项

在JDK的bin目录下,几乎所有的命令程序 都是通过同一个main函数编译生成的。

这些命令包括java、javac、jconsole、jps、jmap和jhat等。

根据编译时选项的不同,这些命令在执行时会进行不同的处理。具体的编译配置信息可参见附录章节。

$(eval $(call SetupLauncher,
        java, \                                      #1  launcher 名称
        -DEXPAND_CLASSPATH_WILDCARDS,\               #2  CFLAGS
        , \                                          #3  LDFLAGS
        , \                                          #4  LDFLAGS_SUFFIX_posix
        user32.lib comctl32.lib, \                   #5  LDFLAGS_SUFFIX_windows 
        $(JDK_OUTPUTDIR)/objs/jli_static.lib,\       #6  optional Windows JLI library (full path) : windows jli库的全路径  
        $(JAVA_RC_FLAGS), \                          #7  optional Windows resource (RC) flags     : windows 资源文件标识
        $(JDK_TOPDIR)/src/windows/resource/java.rc,\ #8  optional Windows version resource file (.rc) : windows 版本资源文件(.rc)
        $(JDK_OUTPUTDIR)/objs/java_objs,\            #9  different output dir : 不同的输出目录
        true                                         #10 if set, link statically with c runtime on windows. : 如果设置参数10 , 静态链接c运行时,在windows上.
    )
)
# jmap
$(eval $(call SetupLauncher,
        jmap, 
        -DJAVA_ARGS='{ "-J-ms8m"$(COMMA) 
        "-J-Dsun.jvm.hotspot.debugger.useProcDebugger"$(COMMA) 
        "-J-Dsun.jvm.hotspot.debugger.useWindbgDebugger"$(COMMA) 
        "sun.tools.jmap.JMap"$(COMMA) }' 
        -DAPP_CLASSPATH='{ "/lib/tools.jar"$(COMMA) "/lib/sa-jdi.jar"$(COMMA) "/classes" }' ,
        ,,,,,,,,
        Info-privileged.plist))     # Parameter 11 if set, override plist file on macosx. : 如果设置,覆盖 plist文件在 macosx上

// windows 上的java
ifeq ($(OPENJDK_TARGET_OS), windows)
    $(eval $(call SetupLauncher,javaw, 
    -DJAVAW -DEXPAND_CLASSPATH_WILDCARDS,,,user32.lib comctl32.lib, 
    $(JDK_OUTPUTDIR)/objs/jli_static.lib, $(JAVA_RC_FLAGS), 
    $(JDK_TOPDIR)/src/windows/resource/java.rc,,true))
endif

FULL_VERSION 和 DOT_VERSION

分别表示jdk的版本的2种表现形式,看图示

JLI_Launch() java.c

openjdk-jdk8u/jdk/src/share/bin/java.c

jdk/src/share/bin/java.c
int
JLI_Launch(int argc, char ** argv,          /* main argc, argc 主参数 */
        int jargc, const char** jargv,          /* JAVA_ARGS 编译时设置的参数 */
        int appclassc, const char** appclassv,  /* app classpath */
        const char* fullversion,                /* full version defined */
        const char* dotversion,                 /* dot version defined */
        const char* pname,                      /* program name 这里是"java"*/
        const char* lname,                      /* launcher name 这里是"java"*/
        jboolean javaargs,                      /* 有无编译选项JAVA_ARGS, 这里是false*/
        jboolean cpwildcard,                    /* classpath wildcard,java时,这里为true,其它工具时,大部分为false*/
        jboolean javaw,                         /* windows-only javaw */
        jint ergo                               /* ergonomics class policy,java时这里0,default选项*/
)
{
    int mode = LM_UNKNOWN;
    char *what = NULL;
    char *cpath = 0;
    char *main_class = NULL;
    int ret;
   
    InvocationFunctions ifn;  // 用来保存将要调用的相关函数指针
    jlong start = 0, end = 0;
    char jvmpath[MAXPATHLEN];
    char jrepath[MAXPATHLEN];
    char jvmcfg[MAXPATHLEN];

    // 静态变量赋值
    _fVersion = fullversion;
    _dVersion = dotversion;
    _launcher_name = lname;
    _program_name = pname;
    _is_java_args = javaargs;
    _wc_enabled = cpwildcard;
    _ergo_policy = ergo;
    
    /*  Initialize platform specific settings 
        只有在windows下,编初始MFC窗体,根据环境变量_JAVA_LAUNCHER_DEBUG的取值决定是否输出JVM Trace信息,
            若_JAVA_LAUNCHER_DEBUG==1,输出JVM Trace信息,同时设置全局变量_launcher_debug = true
     */
    InitLauncher(javaw);
    
    // 打印Launcher状态信息
    DumpState();
    
    if (JLI_IsTraceLauncher()) {
        int i;
        printf("Command line args:\n");
        for (i = 0; i < argc ; i++) {
            printf("argv[%d] = %s\n", i, argv[i]);
        }
        AddOption("-Dsun.java.launcher.diag=true", NULL);
    }

    /*          
        校验JRE version是否有效,可略过不看
            1. 以jar方式包运行时 , 加载Manifest判断JRE Version是否有效 
            2. 以指定版本运行时,校验JRE Version是否存在。
                可以在Java命令中指定特定版本的Java,命令:java -version:<major>.<minor>.<security>
                例如:java -version:1.8 HelloWorld 
    */
    SelectVersion(argc, argv, &main_class);

    -------------------------------------------1. CreateExecutionEnvironment
    /*
      创建执行环境,主要做以下几件事
      1、处理一下参数设置,比如按32位运行还64位运行
      2、处理jrepath、jvmpath、jvmcfg(jvm的自身配置),后续有说明
      3、设置执行程序路径:/xxx/jdk/bin/java
    */
    CreateExecutionEnvironment(&argc, &argv,
               jrepath, sizeof(jrepath), jvmpath, sizeof(jvmpath), jvmcfg, sizeof(jvmcfg));

    -------------------------------------------2. SetJvmEnvironment     
    if (!IsJavaArgs()) { // 像jmap、jinfo、jhat 等编译时设置编译选项【JAVA_ARGS时】,以设置main class位置,而java不设此选项
        // 根据是否配置命令行参数:-XX:NativeMemoryTracking=[off | summary | detail]
        // 决定是否设置环境变量:NMT_LEVEL_${PID}=value,用以跟踪内存使用情况
        SetJvmEnvironment(argc,argv);
    }
    
    
    ifn.CreateJavaVM = 0;             // JNI_CreateJavaVM函数指针
    ifn.GetDefaultJavaVMInitArgs = 0; // JNI_GetDefaultJavaVMInitArgs函数指针
    if (JLI_IsTraceLauncher()) {
        start = CounterGet();
    } 
    
    -------------------------------------------3. LoadJavaVM        
    /*  加载VM,可以理解为java、javac 都是壳子,真正vm代码在动态链接libjvm.so(在windows下是jvm.dll)里面的几个函数(
        CreateJavaVM->JNI_CreateJavaVM、
        GetDefaultJavaVMInitArgs->JNI_GetDefaultJavaVMInitArgs、
        GetCreatedJavaVMs->JNI_GetCreatedJavaVMs),
        拿到函数指针,并由ifn来持有  
    */      
    if (!LoadJavaVM(jvmpath, &ifn)) {
        return(6);
    }
    

    if (JLI_IsTraceLauncher()) {
        end   = CounterGet();
    }
  
    JLI_TraceLauncher("%ld micro seconds to LoadJavaVM\n",
  (long)(jint)Counter2Micros(end-start));
    
    // 移除命令行第一条命令:argv[0] = C:\Tools\Java\open-jre1.8.0_202\bin\java.exe
    ++argv;
    --argc; 
 
      
    if (IsJavaArgs()) {
        /*  一般只有java tools才会进入这里, 以javac的编译配置为例
            $(eval $(call SetupLauncher,javac, \
            -DEXPAND_CLASSPATH_WILDCARDS  -DNEVER_ACT_AS_SERVER_CLASS_MACHINE \
            -DJAVA_ARGS='{ "-J-ms8m"$(COMMA) "com.sun.tools.javac.Main"$(COMMA) }'))
        */     
         /* For tools, convert command line args thus:
         *   javac -cp foo:foo/"*" -J-ms32m ...
         *   java -ms32m -cp JLI_WildcardExpandClasspath(foo:foo/"*") ...  
         */ 
        TranslateApplicationArgs(jargc, jargv, &argc, &argv);  
        // 2. 添加可选参数:
        // 1. -Denv.class.path=    -> CLASSPATH
        // 2. -Dapplication.home=    -> APP_HOME
        // 3. -Djava.class.path=    -> 取决于:宏定义变量JAVA_ARGS是否存在,见附录defines.h  
        if (!AddApplicationOptions(appclassc, appclassv)) {
            return(1);
        }
    } else {
        // 如果环境变量:CLASSPATH 未定义,默认设置为当前目录:"."        
        cpath = getenv("CLASSPATH");
        if (cpath == NULL) {
            cpath = ".";
        }
        // 添加可选参数:-Djava.class.path=cpath下的所有jar文件路径(以分隔符隔开,windows下是; 其它系统下是:)
        SetClassPath(cpath);
    }

    /*
        剥离以'-'开头的参数并设置相应的jvm可选参数,参数包装成JavaVMOption,放入全局变量options(这是数组)
        处理这三个参数时,会额外给这三个全局变量赋值
        -Xss threadStackSize
        -Xmx maxHeapSize
        -Xms initialHeapSize
        
        mode:LM_UNKNOWN | LM_CLASSPATH | LM_JAR
        what:
        1. 如果是jar包启动,what值为jar包路径:test.jar | D:\workdir\study\out\artifacts\test\test.jar
        2. 如果是Class启动,what值为class完整名:com.johnjoe.study.Test  
     */    
    if (!ParseArguments(&argc, &argv, &mode, &what, &ret, jrepath))
 {
        // 如果是 -version  -fullversoin -h -? -help, 从ParseArguments出来后,就结束了
        return(ret);
    }
 
    if (mode == LM_JAR) {
        // 以jar方式运行,重置可选参数:-Djava.class.path=
        SetClassPath(what);     /* Override class path */
    }

    // 添加可选参数:-Dsun.java.command=${java.exe完整路径} [ ${主程序完整名} | ${jar包}] ${命令行参数}
    SetJavaCommandLineProp(what, argc, argv);

    //添加可选参数:-Dsun.java.launcher=SUN_STANDARD
    SetJavaLauncherProp();

    //(**Linux only**)添加可选参数:-Dsun.java.launcher.pid=
    SetJavaLauncherPlatformProps();
    
    // 全局变量threadStackSize初始为0,在上面解析参数时,若碰到-Xss,则会更新为用户设置的值
    return JVMInit(&ifn, threadStackSize, argc, argv, mode, what, ret);
}

1. CreateExecutionEnvironment()

openjdk-jdk8u/jdk/src/solaris/bin/java_md_solinux.c

JVM初始化前置准备,创建执行环境

这个函数给jvm运行提前创建执行环境,主要做以下几件事情

1、找到执行程序的路径

2、确定执行平台的架构

3、确定执行模式:client/server

jdk/src/solaris/bin/java_md_solinux.c
void
CreateExecutionEnvironment(int *pargc, char ***pargv,
                           char jrepath[], jint so_jrepath,
                           char jvmpath[], jint so_jvmpath,
                           char jvmcfg[],  jint so_jvmcfg) {
  
    jboolean jvmpathExists;

    /* Compute/set the name of the executable */
    // 设置执行程序的路径
    SetExecname(*pargv);

    /* Check data model flags, and exec process, if needed */
    {
      // 得到linux平台下执行的架构:LIBARCH32NAME-32位机器、LIBARCH64NAME-64位机器、LIBARCHNAME-默认        
      char *arch        = (char *)GetArch(); /* like sparc or sparcv9 */
      char * jvmtype    = NULL;
      int  argc         = *pargc;
      char **argv       = *pargv;
      int running       = CURRENT_DATA_MODEL; // 表示正在执行的机器位数

      int wanted        = running;      /* What data mode is being
                                           asked for? Current model is
                                           fine unless another model
                                           is asked for */
   
      ............                                          
    }
}
SetExecname()

openjdk-jdk8u/jdk/src/solaris/bin/java_md_solinux.c

这个函数就是找出执行程序的路径,也就是java命令所在的路径,主要按4种方式找,下面按查找优先顺序列出

1、/proc/self/exe 链接路径

2、绝对路径

3、相对路径

4、全局搜索

jdk/src/solaris/bin/java_md_solinux.c
const char*
SetExecname(char **argv)
{
    char* exec_path = NULL;
    #if defined(__solaris__)
    ........
    #elif defined(__linux__)
    {
        // 在linux操作系统中"/proc/self/exe"链接到当前进程的执行程序目录,
        // 比如,当我们在linux操作脚本的时候,这个目录就会链接到bash脚本程序目录,
        // 这里是链接到java程序,也就是我们的java命令的目录
        const char* self = "/proc/self/exe";
        char buf[PATH_MAX+1];
        int len = readlink(self, buf, PATH_MAX);
        if (len >= 0) {
            buf[len] = '\0';            /* readlink(2) doesn't NUL terminate */
            exec_path = JLI_StringDup(buf);
        }
    }
    #else /* !__solaris__ && !__linux__ */
    {
        /* Not implemented */
    }
    #endif
    /* 到这一步,还没拿到执行程序路径,那就再通过别的方式去找,具体方式如下:
       1、如果命令是/开头的,那就按绝对路径去找
       2、绝对路径找不到,那再相对路径找
       3、还找不到,就通过系统搜索的方式
    */
    if (exec_path == NULL) {
        exec_path = FindExecName(argv[0]);
    }
    // 将得到的执行程序路径存储到全局变量execname中,方便后续逻辑使用
    execname = exec_path;
    return exec_path;
}

2. SetJvmEnvironment()

这个函数最主要的 是在于管理NativeMemoryTracking参数。该参数通过监控点记录JVM向系统申请内存时的活动,虽然有助于内存管理,但会带来一定的性能开销,因此使用时应谨慎考虑。。

NMT_LEVEL_353191=[off | summary | detail]  (353191是进程的pid)

可以通过 -XX:NativeMemoryTracking=xx 来设置,选项有上面三种。

openjdk-jdk8u/jdk/src/share/bin/java.c.

jdk/src/share/bin/java.c
/*
 * static void SetJvmEnvironment(int argc, char **argv);
 *   Is called just before the JVM is loaded. 
 *   We can set env variables that are consumed by the JVM.  
 *   This function is non-destructive, leaving the arg list intact.  
 *   The first use is for the JVM flag  -XX:NativeMemoryTracking=value.
 */
static void SetJvmEnvironment(int argc, char **argv) {

    static const char*  NMT_Env_Name    = "NMT_LEVEL_";
    int i;
    for (i = 0; i < argc; i++) {
        char *arg = argv[i];

        if (i > 0) {
            char *prev = argv[i - 1];
            // 跳过一些jvm不需要的参数  /  非-开头的, / 如 -version or -help等  / 一些应用级参数     
            if (*arg != '-' &&  ((JLI_StrCmp(prev, "-cp") == 0
|| JLI_StrCmp(prev, "-classpath") == 0))) {
                continue;
            }
            if (*arg != '-'
                || JLI_StrCmp(arg, "-version") == 0
                || JLI_StrCmp(arg, "-fullversion") == 0
                || JLI_StrCmp(arg, "-help") == 0
                || JLI_StrCmp(arg, "-?") == 0
                || JLI_StrCmp(arg, "-jar") == 0
                || JLI_StrCmp(arg, "-X") == 0) {
                return;
            }
        }
    
        // NativeMemoryTracking 参数是记录jvm向系统申请内存时的埋点,有一定的性能消耗,使用需要谨慎   
        if (JLI_StrCCmp(arg, "-XX:NativeMemoryTracking=") == 0) {
            int retval;
            // get what follows this parameter, include "="
            size_t pnlen = JLI_StrLen("-XX:NativeMemoryTracking=");
            if (JLI_StrLen(arg) > pnlen) {
                char* value = arg + pnlen;
                size_t pbuflen = pnlen + JLI_StrLen(value) + 10; // 10 max pid digits
        
                /*
                * ensures that malloc successful
                * DONT JLI_MemFree() pbuf.  JLI_PutEnv() uses system call
 that could store the address.
                */
                char * pbuf = (char*)JLI_MemAlloc(pbuflen);
                JLI_Snprintf(pbuf, pbuflen, "%s%d=%s", NMT_Env_Name, JLI_GetPid(), value);
                retval = JLI_PutEnv(pbuf);
                if (JLI_IsTraceLauncher()) {
                    char* envName;
                    char* envBuf;
                    // ensures that malloc successful
                    envName = (char*)JLI_MemAlloc(pbuflen);
                    JLI_Snprintf(envName, pbuflen, "%s%d", NMT_Env_Name, JLI_GetPid());
                    printf("TRACER_MARKER: NativeMemoryTracking: env var is %s\n",envName);
                    printf("TRACER_MARKER: NativeMemoryTracking: putenv arg %s\n",pbuf);
                    envBuf = getenv(envName);
                    printf("TRACER_MARKER: NativeMemoryTracking: got value %s\n",envBuf);
                    free(envName);
                }
            }
        }
    }
}

3. LoadJavaVM()

加载libjvm.so 或 jvm.dll,并拿到其中部分函数地址后续使用。

openjdk-jdk8u/jdk/src/solaris/bin/java_md_solinux.c

jdk/src/solaris/bin/java_md_solinux.c
jboolean
 LoadJavaVM(const char *jvmpath, InvocationFunctions *ifn)
{
    void *libjvm;

    JLI_TraceLauncher("JVM path is %s\n", jvmpath);
    /*
    通过dlopen函数打开jvm的动态库libjvm.so,并把它装入内存中,为了后面做符号解析和重定位
    
    RTLD_NOW:在dlopen返回前,需要解析出所有未定义符号,否则,在dlopen会返回NULL
    RTLD_GLOBAL:动态库中定义的符号可被其后打开的其它库重定位
    */
    libjvm = dlopen(jvmpath, RTLD_NOW + RTLD_GLOBAL);
    if (libjvm == NULL) {
        #if defined(__solaris__) && defined(__sparc) && !defined(_LP64) /* i.e. 32-bit sparc */
          .............
        #endif
        JLI_ReportErrorMessage(DLL_ERROR1, __LINE__);
        JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());
        return JNI_FALSE;
    }
    
    // dlsym函数对动态库libjvm.so做符号解析,如果库中有函数 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.so做符号解析,如果库中有函数 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;
    }
    
    // dlsym函数对动态库libjvm.so做符号解析,如果库中有函数 JNI_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;
}

4. SetClassPath(cpath) / SetClassPath(what)

设置目标class文件路径 或 设置目标jar路径

5. JVMInit()

openjdk-jdk8u/jdk/src/solaris/bin/java_md_solinux.c

VM初始化入口

1、调用ShowSplashScreen函数展示Java虚拟机的欢迎画面

2、调用ContinueInNewThread函数创建一个新线程并在其中启动Java虚拟机,执行后续流程

jdk/src/solaris/bin/java_md_solinux.c
int
JVMInit(InvocationFunctions* ifn, jlong threadStackSize,
        int argc, char **argv,
        int mode, char *what, int ret)
{
    // 调用ShowSplashScreen函数展示Java虚拟机的欢迎画面
    ShowSplashScreen();
    
    // 创建一个新线程并在其中启动Java虚拟机,执行后续流程
    return ContinueInNewThread(ifn, threadStackSize, argc, argv, mode, what, ret);
}
ContinueInNewThread()

main线程创建、执行

threadStackSize参数表示线程执行时的栈空间,因为每个线程执行时都要有自己的私有栈空间做数据存储,所以这是必须的, 这个值可以自己设置,不设置的话,系统会自己默认给个值:

linux64位系统默认是1024k,32位系统默认是320k(见下方截图), 另外,自己查看threadStackSize栈大小可以通过下列方式:

java -XX:+PrintFlagsFinal -version | grep ThreadStackSize
jdk/src/share/bin/java.c
int
ContinueInNewThread(InvocationFunctions* ifn, jlong threadStackSize,
                    int argc, char **argv,
                    int mode, char *what, int ret)
{

     /*
     * 如果用户没有指定 threadStackSize 大小,那就要去检查下VM自带的默认值。
         Hotspot本身不再支持jdk1.1版本,但可以通过init args结构返回其默认堆栈大小,
         调用 GetDefaultJavaVMInitArgs(对应的实际函数是JNI_GetDefaultJavaVMInitArgs),
         这个函数可以取到VM设置的默认的 threadStackSize 大小。
         这里为什么用不支持jdk1.1但是又用jdk1.1来做参数,我想原因是历史遗留,当然这个也不重要,我们只需要知道这里是可以拿到默认 threadStackSize 大小的就行。
         可通过如下命令查看
         java -XX:+PrintFlagsFinal -version | grep ThreadStackSize
     */
    if (threadStackSize == 0) {
        struct JDK1_1InitArgs args1_1;
        memset((void*)&args1_1, 0, sizeof(args1_1));
        args1_1.version = JNI_VERSION_1_1;
        ifn->GetDefaultJavaVMInitArgs(&args1_1);  /* ignore return value */
        if (args1_1.javaStackSize > 0) {
            threadStackSize = args1_1.javaStackSize;
        }
    }
    
    /* 创建一个新的线程去构建JVM, 新线程的入口函数是JavaMain() */ 
    { 
      JavaMainArgs args;
      int rslt;

      args.argc = argc;
      args.argv = argv;
      args.mode = mode;
      args.what = what;
      args.ifn = *ifn;

      rslt = ContinueInNewThread0(JavaMain, threadStackSize, (void*)&args);
      
       // 如果调用者认为存在错误(ret就是调用者给过来的),我们只需返回错误,否则我们返回被调用者的值
      return (ret != 0) ? ret : rslt;
    }
}

hotspot/src/os_cpu/linux_x86/vm/globals_linux_x86.hpp

ContinueInNewThread0()

这个函数就是通过系统调用API创建一个新的线程,线程入口是 参数continuation指向的函数指针指向的函数(即JavaMain())。

/*
 * Block current thread and continue execution in a new thread
 */
jdk/src/solaris/bin/java_md_solinux.c
int
ContinueInNewThread0(int (JNICALL *continuation)(void *), jlong stack_size, void * args) {
    int rslt;
#ifndef __solaris__
    pthread_t tid;           // 线程ID
    pthread_attr_t attr;     // 线程属性    
    pthread_attr_init(&attr);// 初始化线程属性结构   
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);

    if (stack_size > 0) {
        pthread_attr_setstacksize(&attr, stack_size); // 设置栈大小
    }
    /* 设置线程的分离状态
     * detachstate:
     *         PTHREAD_CREATE_DETACHED 表示分离
     *         PTHREAD_CREATE_JOINABLE 表示结合
     * 如果设置成分离状态,表示无需关注新创建的线程执行结果,由它自行完成,并在完成后由操作系统回收资源;
     * 如果设置成非分离状态,表示父线程需要拿到创建的子线程的执行结果
     
       这里是Java进程,要等待Main线程的执行结果
    */
    
     
    // 创建一个新的线程,指定线程创建后需要执行的任务函数,返回0表示创建成功。                           
    if (pthread_create(&tid, &attr, (void *(*)(void*))continuation, (void*)args) == 0) {
        void * tmp;
        pthread_join(tid, &tmp); // 等待子线程执行结果
        rslt = (int)tmp;
    } else {
        // 线程创建失败,就在当前线程下执行任务函数
        rslt = continuation(args);
    }
    
    pthread_attr_destroy(&attr);
    #else /* __solaris__ */
    ..............
    #endif /* !__solaris__ */
    return rslt;
}

3. JavaMain() main线程在这里

在上一步创建main线程后, 其执行的入口就是JavaMain();

它主要做三件事:

1. 创建vm;

2. 加载主类;

3. 执行主类的main方法。

jdk/src/share/bin/java.c
int JNICALL
 JavaMain(void * _args)
{
    // 参数赋值
    JavaMainArgs *args = (JavaMainArgs *)_args;
    int argc = args->argc;
    char **argv = args->argv;
    int mode = args->mode;   // 类加载 还是jar包加载
    char *what = args->what; // 类路径 或 jar包路径
    InvocationFunctions ifn = args->ifn;

    JavaVM *vm = 0;
    JNIEnv *env = 0;
    jclass mainClass = NULL;
    jclass appClass = NULL; // actual application class being launched
    jmethodID mainID;
    jobjectArray mainArgs;
    int ret = 0;
    jlong start = 0, end = 0;

    // 这个函数在linux和windows实现中都是空白,把它看作java里的一个钩子函数 
    RegisterThread();

   

    start = CounterGet();
    
    ---1. 初始化虚拟机
    if (!InitializeJVM(&vm, &env, &ifn)) {
        // 初始化失败,打印日志,退出程序
        JLI_ReportErrorMessage(JVM_ERROR1);
        exit(1);
    }
    
    // 是否打印设置过的信息
    if (showSettings != NULL) {
        ShowSettings(env, showSettings);
        CHECK_EXCEPTION_LEAVE(1);
    }
    // 打印版本信息
    if (printVersion || showVersion) {
        PrintJavaVersion(env, showVersion);
        CHECK_EXCEPTION_LEAVE(0);
        if (printVersion) {
            LEAVE();
        }
    }

    /* If the user specified neither a class name nor a JAR file */
    if (printXUsage || printUsage || what == 0 || mode == LM_UNKNOWN) {
        PrintUsage(env, printXUsage);
        CHECK_EXCEPTION_LEAVE(1);
        LEAVE();
    }
    
    // 释放启动虚拟机时加载的jvm.cfg中配置项占用的空间,见之前章节CreateExecutionEnvironment, 略过...
    FreeKnownVMs();  /* after last possible PrintUsage() */

    if (JLI_IsTraceLauncher()) {
        end = CounterGet();
        JLI_TraceLauncher("%ld micro seconds to InitializeJVM\n",
               (long)(jint)Counter2Micros(end-start));
    }

    /* At this stage, argc/argv have the application's arguments */
    if (JLI_IsTraceLauncher()){
        int i;
        printf("%s is '%s'\n", launchModeNames[mode], what);
        printf("App's argc is %d\n", argc);
        for (i=0; i < argc; i++) {
            printf("    argv[%2d] = '%s'\n", i, argv[i]);
        }
    }

    ret = 1;

    ---2. 加载执行主入口类main class
    mainClass = LoadMainClass(env, mode, what);
    CHECK_EXCEPTION_NULL_LEAVE(mainClass);
    // LoadMainClass函数求出了mainClass,其实appClass跟mainClass是相同的,所以,这一步拿得就是mainClass,后面会讲这个细节 
    appClass = GetApplicationClass(env);
    NULL_CHECK_RETURN_VALUE(appClass, -1);
    
    // 看成钩子函数就行,linux/windows都没做具体实现
    PostJVMInit(env, appClass, vm);
    CHECK_EXCEPTION_LEAVE(1);
    
    // 取出主类mainClass的main函数地址,并赋值给mainID 
    mainID = (*env)->GetStaticMethodID(env, mainClass, "main",  "([Ljava/lang/String;)V");
    CHECK_EXCEPTION_NULL_LEAVE(mainID);

    /* 构建平台依赖的参数数组 */
    mainArgs = CreateApplicationArgs(env, argv, argc);
    CHECK_EXCEPTION_NULL_LEAVE(mainArgs);

    ---3. 这里才真正调用 java类中的main方法. 
    (*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);


    /*
     * 如果exit code不为0,那么main执行抛出了异常
     * 至此,整个java应用执行完毕
     */
    ret = (*env)->ExceptionOccurred(env) == NULL ? 0 : 1;
    LEAVE();  // 这是一个宏定义的函数,内部主要做一次资源回收的工作
}

InitializeJVM()

JVM初始化

/*
 * Initializes the Java Virtual Machine. Also frees options array when finished.
 * 初始化java 虚拟机,完成后释放options数组,option就是虚拟机启动时设置的参数,
 */
jdk/src/share/bin/java.c
static jboolean InitializeJVM(JavaVM **pvm, JNIEnv **penv, InvocationFunctions *ifn)
{
    JavaVMInitArgs args;
    jint r;
    
    // args空间填0
    memset(&args, 0, sizeof(args));
    args.version  = JNI_VERSION_1_2; // 版本
    args.nOptions = numOptions;
    // option数量
    args.options  = options;
    // options数组
    args.ignoreUnrecognized = JNI_FALSE;

    if (JLI_IsTraceLauncher()) {
        int i = 0;
        printf("JavaVM args:\n    ");
        printf("version 0x%08lx, ", (long)args.version);
        printf("ignoreUnrecognized is %s, ",
               args.ignoreUnrecognized ? "JNI_TRUE" : "JNI_FALSE");
        printf("nOptions is %ld\n", (long)args.nOptions);
        for (i = 0; i < numOptions; i++)
            printf("    option[%2d] = '%s'\n",
                   i, args.options[i].optionString);
    }
    // ifn->CreateJavaVM函数,实际指向的是加载的动态库libjvm.so 中的 JNI_CreateJavaVM函数,JNI_开头的函数,都定义在jni.cpp文件中,
    // 通过CreateJavaVM函数真正创建Java虚拟机
    r = ifn->CreateJavaVM(pvm, (void **)penv, &args);
    // 初始化结束, 释放options
    JLI_MemFree(options);
    return r == JNI_OK;
}

1. JNI_CreateJavaVM() 创建vm

_JNI_IMPORT_OR_EXPORT_ __attribute__((visibility("default"))) 用于gcc编译动态链接库时,向外暴露函数符号。

openjdk-jdk8u/hotspot/src/share/vm/prims/jni.cpp
_JNI_IMPORT_OR_EXPORT_ jint JNICALL JNI_CreateJavaVM(JavaVM **vm, void **penv, void *args) {
    #ifndef USDT2
    HS_DTRACE_PROBE3(hotspot_jni, CreateJavaVM__entry, vm, penv, args);
    #else /* USDT2 */
    HOTSPOT_JNI_CREATEJAVAVM_ENTRY((void **) vm, penv, args);
    #endif /* USDT2 */
    
    jint result = JNI_ERR;
    DT_RETURN_MARK(CreateJavaVM, jint, (const jint&)result); // 日志记录用
    
    // 这一段是检验Atomic::xchg(汇编命令:原子性更改变量的值)是否可用,
    // jvm用Atomic::xchg来实现synchronization同步。
    #if defined(ZERO) && defined(ASSERT)
    {
        jint a = 0xcafebabe;
        jint b = Atomic::xchg(0xdeadbeef, &a);
        void *c = &a;
        void *d = Atomic::xchg_ptr(&b, &c);
        assert(a == (jint) 0xdeadbeef && b == (jint) 0xcafebabe, "Atomic::xchg() works");
        assert(c == &b && d == &a, "Atomic::xchg_ptr() works");
    }
    #endif // ZERO && ASSERT
    
    
    // 原子操作:设置vm_created为1,防止其他线程创建jvm
    if (Atomic::xchg(1, &vm_created) == 1) {
        return JNI_EEXIST;   // 已经创建,或者在创建过程中
    }
    if (Atomic::xchg(0, &safe_to_recreate_vm) == 0) {
        return JNI_ERR;  // 尝试失败后就不允许重试
    }
    
    assert(vm_created == 1, "vm_created is true during the creation");
    
    
    bool can_try_again = true;

    result = Threads::create_vm((JavaVMInitArgs*) args, &can_try_again);
    if (result == JNI_OK) {
        JavaThread *thread = JavaThread::current(); // 当前线程,其实就是主线程
     
        *vm = (JavaVM *)(&main_vm);                  // 将vm指针指向create_vm中创建的vm,后续操作要用。
        *(JNIEnv**)penv = thread->jni_environment(); // 同样的pJNIEnv指针指向create_vm中在JavaThread中创建的JNIEnv。
      
        RuntimeService::record_application_start();
    
        // 通知 JVMTI(jvm的黑匣子,记录jvm的运行情况,可以通过调用jvmti的接口对jvm进行管理)
        if (JvmtiExport::should_post_thread_life()) {
           JvmtiExport::post_thread_start(thread);
        }
         
        post_thread_start_event(thread);

        ...

        // Since this is not a JVM_ENTRY we have to set the thread state manually before leaving.
        ThreadStateTransition::transition_and_fence(thread, _thread_in_vm, _thread_in_native);
    } else { // create_vm 创建失败
        if (can_try_again) {
            // 可重试就把safe_to_recreate_vm设置为1
            safe_to_recreate_vm = 1;
        }

        // 创建失败了,要重置vm_created 
        // Creation failed. We must reset vm_created   
        *vm = 0;
        *(JNIEnv**)penv = 0;
        // 释放创建过程vm_created空间
        OrderAccess::release_store(&vm_created, 0);
    }

    return result;
}
Threads::create_vm((JavaVMInitArgs*) args, &can_try_again)

这是个大块头,以后的章节我们会以它为入口,串联起VM的相关知识,现在先略过它。

2. LoadMainClass()  加载main类       

加载main class

/*
 * 加载一个类,并验证主类是否存在,
 
    JNIEnv *env: 指向 JNI 环境的指针。
    int mode: 模式参数,用于加载类的方式。
    char *name: 要加载的类名 或 jar包路径

 */
static jclass LoadMainClass(JNIEnv *env, int mode, char *name) {
    jmethodID mid; // 方法ID
    jstring str;   // 字符串对象
    jobject result; // 结果对象
    jlong start = 0, end = 0; // 计时器
    jclass cls = GetLauncherHelperClass(env); // 获取启动器帮助类 sun/launcher/LauncherHelper.java
    NULL_CHECK0(cls); // 检查类是否为空

    if (JLI_IsTraceLauncher()) {
        start = CounterGet(); // 开始计时
    }

    // 获取LauncherHelper类 静态方法 checkAndLoadMain() ID
    NULL_CHECK0(mid = (*env)->GetStaticMethodID(env, cls,
                "checkAndLoadMain",
                "(ZILjava/lang/String;)Ljava/lang/Class;"));

    // 将 C 字符串转换为 Java 字符串
    str = NewPlatformString(env, name);
    
    // 调用Java类LauncherHelper#checkAndLoadMain() 加载主类,支持类名 和 jar包两种加载方式
    // 这里它利用 应用类加载器(Application ClassLoade),(通过ClassLoader.getSystemClassLoader()得到)
    // 来加载目标类, 如果对类加载器的知识模糊了, 后续章节我们会详细介绍
    CHECK_JNI_RETURN_0(
        result = (*env)->CallStaticObjectMethod(
            env, cls, mid, USE_STDERR, mode, str));

    if (JLI_IsTraceLauncher()) {
        end = CounterGet(); // 结束计时
        printf("%ld micro seconds to load main class\n",
               (long)(jint)Counter2Micros(end-start)); // 打印加载主类所用时间
        printf("----%s----\n", JLDEBUG_ENV_ENTRY); // 打印调试信息
    }

    return (jclass)result; // 返回加载的类
}


3. CallStaticVoidMethod()  执行main方法。

真正去执行java类中的main函数

加载到了目标类,定位到了其main()函数,接下来就是调用了。

函数名CallStaticVoidMethodV中的static 已经指示出了它专用来调用静态方法的。

void CallStaticVoidMethod(jclass cls, jmethodID methodID, ...) {
    va_list args;
    va_start(args,methodID);
    functions->CallStaticVoidMethodV(this,cls,methodID,args);
    va_end(args);
}


总结

到此呢,从执行java命令 到 vm 执行java类的main()方法 的全过程就展现出来了。

当然了,这只是冰山一角,九牛中的一毛而已,但是我们已经初窥全貌了,对整体流程有了一定了解了。

初次探索总是略显痛苦,不用气馁,不用害怕,在后续的文章里我们会一步一步来探索更多的细节。

标签:java,int,流程,args,char,全貌,JNI,main,JLI
From: https://blog.csdn.net/capaabbcc/article/details/142426755

相关文章

  • 期盼已久!通义灵码 AI 程序员开启邀测,全流程开发仅用几分钟
    在AI程序员的帮助下,一个几乎没有专业编程经验的初中生,在人头攒动的展台上从零开始,两分钟就做出了一个倒计时网页。他需要做的,只是输入包含几句话的提示词。数秒钟后,大模型就生成了代码,还列出了环境需求,复制完代码就可以使用了。这不是程序员父亲带自家小孩做的网红项目,而是人......
  • 期盼已久!通义灵码 AI 程序员开启邀测,全流程开发仅用几分钟
    在AI程序员的帮助下,一个几乎没有专业编程经验的初中生,在人头攒动的展台上从零开始,两分钟就做出了一个倒计时网页。他需要做的,只是输入包含几句话的提示词。数秒钟后,大模型就生成了代码,还列出了环境需求,复制完代码就可以使用了。这不是程序员父亲带自家小孩做的网红项目,而是人......
  • 望繁信科技入选中国信通院“铸基计划”,流程智能引领企业数字化变革
    近日,上海望繁信科技有限公司(以下简称“望繁信科技”)的数字北极星流程智能管理平台,在中国信息通信研究院(以下简称“信通院”)的评选中,荣获2024年度数据治理技术解决方案奖项,并入选《高质量数字化转型产品及服务全景图》。同时,望繁信科技的数字北极星平台成功首批入驻信通院铸基计划应......
  • 考前须知:Oracle OCP考试流程和准备
    考前须知:OracleOCP考试流程和准备OCP(OracleCertifiedProfessional),是甲骨文数据库认证中很常见的一个,但却有着很重要的作用,对于从事大型数据库相关行业的人来说,几乎是必考的一种,OCP证书含金量较高,考试也有一定的难度,所以考前要对OCP考试有一些了解。​一.OCP认证考试流程:......
  • Vue3 流程图组件库 :Vue Flow
    VueFlow是一个轻量级的Vue3组件库,它允许开发者以简洁直观的方式创建动态流程图。本篇文章记录一下VueFlow的基本用法安装npmadd@vue-flow/core流程图的构成Nodes、Edges、Handles主题默认样式通过导入样式文件应用/*thesearenecessarystylesforvueflow*/@import'......
  • CNAS软件测试实验室能力验证全流程介绍
    能力验证是多个实验室间比对来确定实验室检测能力的活动,是维持实验室较高技术水平的一种确认和验证活动。CNAS软件检测实验室初次认可和扩大认可范围时,申请认可的每个子领域应至少参加过一次相关领域的能力验证且获得满意结果。通过认定认可后,只要存在可获得的能力验证,不同类目......
  • 55 mysql 的登录认证流程
    前言这里我们来看一下 mysql 的认证的流程 我们这里仅仅看 我们最常见的一个认证的处理流程我们经常会登录的时候 碰到各种异常信息  认证失败的大体流程大概的流程是这样 客户端和服务器建立连接之后, 服务器向客户端发送 salt然后 客户端根据salt 将客户端传入的密......
  • .net core开源工作流程框架elsa源码阅读之容器的理解
    官方文档:https://v3.elsaworkflows.io/这个框架的依赖注入容器,底层是靠原生的IServiceCollection,没有使用其他的三方容器;然后在这个基础上,作者进行了封装。主要是用了Module类和继承了IFeature接口的类完成了依赖注入容器的封装。Module是用来管理feature和依赖的。Module我称......
  • 项目流程(启动-规划-执行-跟进-收尾)
    产品经理要知道一个版本的需求所经历的各个阶段。一:启动启动一般情况下是老板要确定的,一般情况下产品经理接触不到这块。1.1行业调研决定要进入哪个行业。工作内容:调研行业的发展现状及发展趋势,目的是为产品的发展方向提供依据。工作产出:商业需求文档BRD(BusinessRequirementDo......
  • Python字典进阶:setdefault技巧让你的代码更优雅,用setdefault优化你的Python数据处理流
    推荐阅读:数据科学的秘密武器:defaultdict——Python字典的自动化填充神器,让数据结构更灵活一、什么是setdefaultPython中的setdefault方法是字典(dict)类型的一个非常实用的方法,它允许开发者在尝试访问字典中不存在的键时,自动为该键设置一个默认值,并返回这个默认值。 二、s......