jvm原理及性能调优合集(JVM系列二)
jvm原理及性能调优合集(JVM系列二)// solaris/bin/java_md_solinux.c /* * Block current thread and continue execution in a new thread */ int ContinueInNewThread0(int (JNICALL *continuation)(void *) jlong stack_size void * args) { int rslt; #ifdef __linux__ pthread_t tid; pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr PTHREAD_CREATE_JOINABLE); if (stack_size > 0) {

上一篇粗略讲了下jvm的启动过程,但很多路子还没跑通。其中非常核心的,加载vm的过程。这个可以在hotspot中找到端倪。但jvm启动,又是如何载入java代码呢。
1. JavaMain加载流程我们知道,java中入口是在main方法中,可以在命令行中指定main类,或者jar包中指定的manifest.xml中指定的main类。在java.c中,我们可以看到一个JavaMain方法,不知从何而来,但很像是直接加载java入口的方法。
// share/bin/java.c
// 加载 main 函数类
// 通过引入 JavaMain()  接入java方法
// #define JNICALL __stdcall
int JNICALL
JavaMain(void * _args)
{
    JavaMainArgs *args = (JavaMainArgs *)_args;
    int argc = args->argc;
    char **argv = args->argv;
    int mode = args->mode;
    char *what = args->what;
    // 一些jvm的调用实例,在之前的步骤中,通过加载相应动态链接方法,保存起来的
    /** 
     * ifn->CreateJavaVM =
     *   (void *)GetProcAddress(handle  "JNI_CreateJavaVM");
     * ifn->GetDefaultJavaVMInitArgs =
     *   (void *)GetProcAddress(handle  "JNI_GetDefaultJavaVMInitArgs");
     */
    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  end;
    // collector
    RegisterThread();
    /* Initialize the virtual machine */
    start = CounterGet();
    // 初始化jvm,失败则退出
    if (!InitializeJVM(&vm  &env  &ifn)) {
        JLI_ReportErrorMessage(JVM_ERROR1);
        exit(1);
    }
    // jvm检查完毕,如果只是一些展示类请求,则展示信息后,退出jvm
    if (showSettings != NULL) {
        ShowSettings(env  showSettings);
        /**
         * 宏是神奇的操作,此处 *env 直接引用
#define CHECK_EXCEPTION_LEAVE(CEL_return_value) \
    do { \
        if ((*env)->ExceptionOccurred(env)) { \
            JLI_ReportExceptionDescription(env); \
            ret = (CEL_return_value); \
            LEAVE(); \
        } \
    } while (JNI_FALSE)
         */
        CHECK_EXCEPTION_LEAVE(1);
    }
    // 调用 LEAVE() 方法的目的在于主动销毁jvm线程
    // 且退出当前方法调用,即 LEAVE() 后方法不再被执行
/*
 * Always detach the main thread so that it appears to have ended when
 * the application's main method exits.  This will invoke the
 * uncaught exception handler machinery if main threw an
 * exception.  An uncaught exception handler cannot change the
 * launcher's return code except by calling System.exit.
 *
 * Wait for all non-daemon threads to end  then destroy the VM.
 * This will actually create a trivial new Java waiter thread
 * named "DestroyJavaVM"  but this will be seen as a different
 * thread from the one that executed main  even though they are
 * the same C thread.  This allows mainThread.join() and
 * mainThread.isAlive() to work as expected.
 */
    /**
     *
     * 
#define LEAVE() \
    do { \
        if ((*vm)->DetachCurrentThread(vm) != JNI_OK) { \
            JLI_ReportErrorMessage(JVM_ERROR2); \
            ret = 1; \
        } \
        if (JNI_TRUE) { \
            (*vm)->DestroyJavaVM(vm); \
            return ret; \
        } \
    } while (JNI_FALSE)
     */
    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();
    }
    // 释放内存
    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[-] = '%s'\n"  i  argv[i]);
        }
    }
    ret = 1;
    /*
     * Get the application's main class.
     *
     * See bugid 5030265.  The Main-Class name has already been parsed
     * from the manifest  but not parsed properly for UTF-8 support.
     * Hence the code here ignores the value previously extracted and
     * uses the pre-existing code to reextract the value.  This is
     * possibly an end of release cycle expedient.  However  it has
     * also been discovered that passing some character sets through
     * the environment has "strange" behavior on some variants of
     * Windows.  Hence  maybe the manifest parsing code local to the
     * launcher should never be enhanced.
     *
     * Hence  future work should either:
     *     1)   Correct the local parsing code and verify that the
     *          Main-Class attribute gets properly passed through
     *          all environments 
     *     2)   Remove the vestages of maintaining main_class through
     *          the environment (and remove these comments).
     *
     * This method also correctly handles launching existing JavaFX
     * applications that may or may not have a Main-Class manifest entry.
     */
    // 加载 main 指定的class类
    mainClass = LoadMainClass(env  mode  what);
    CHECK_EXCEPTION_NULL_LEAVE(mainClass);
    /*
     * In some cases when launching an application that needs a helper  e.g.  a
     * JavaFX application with no main method  the mainClass will not be the
     * applications own main class but rather a helper class. To keep things
     * consistent in the UI we need to track and report the application main class.
     */
    appClass = GetApplicationClass(env);
    NULL_CHECK_RETURN_VALUE(appClass  -1);
    /*
     * PostJVMInit uses the class name as the application name for GUI purposes 
     * for example  on OSX this sets the application name in the menu bar for
     * both SWT and JavaFX. So we'll pass the actual application class here
     * instead of mainClass as that may be a launcher or helper class instead
     * of the application class.
     */
    // 加载main() 方法前执行初始化
    PostJVMInit(env  appClass  vm);
    CHECK_EXCEPTION_LEAVE(1);
    /*
     * The LoadMainClass not only loads the main class  it will also ensure
     * that the main method's signature is correct  therefore further checking
     * is not required. The main method is invoked here so that extraneous java
     * stacks are not in the application stack trace.
     */
    // 获取main()方法id  main(String[] args)
    mainID = (*env)->GetStaticMethodID(env  mainClass  "main" 
                                       "([Ljava/lang/String;)V");
    CHECK_EXCEPTION_NULL_LEAVE(mainID);
    /* Build platform specific argument array */
    // 构建args[] 参数
    mainArgs = CreateApplicationArgs(env  argv  argc);
    CHECK_EXCEPTION_NULL_LEAVE(mainArgs);
    /* Invoke main method. */
    // 调用java实现的main()方法
    // XX:: 重要实现
    (*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();
}
/*
 * Loads a class and verifies that the main class is present and it is ok to
 * call it for more details refer to the java implementation.
 */
static jclass
LoadMainClass(JNIEnv *env  int mode  char *name)
{
    jmethodID mid;
    jstring str;
    jobject result;
    jlong start  end;
    jclass cls = GetLauncherHelperClass(env);
    NULL_CHECK0(cls);
    if (JLI_IsTraceLauncher()) {
        start = CounterGet();
    }
    // checkAndLoadMain(String) 方法作为中间main()调用
    NULL_CHECK0(mid = (*env)->GetStaticMethodID(env  cls 
                "checkAndLoadMain" 
                "(ZILjava/lang/String;)Ljava/lang/Class;"));
    str = NewPlatformString(env  name);
    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;
}    
// 初始化jvm  主要是调用 CreateJavaVM() 方法,进行创建jvm操作
/*
 * Initializes the Java Virtual Machine. Also frees options array when
 * finished.
 */
static jboolean
InitializeJVM(JavaVM **pvm  JNIEnv **penv  InvocationFunctions *ifn)
{
    JavaVMInitArgs args;
    jint r;
    memset(&args  0  sizeof(args));
    args.version  = JNI_VERSION_1_2;
    args.nOptions = numOptions;
    args.options  = options;
    args.ignoreUnrecognized = JNI_FALSE;
    if (JLI_IsTraceLauncher()) {
        int i = 0;
        printf("JavaVM args:\n    ");
        printf("version 0xlx  "  (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[-] = '%s'\n" 
                   i  args.options[i].optionString);
    }
    r = ifn->CreateJavaVM(pvm  (void **)penv  &args);
    JLI_MemFree(options);
    return r == JNI_OK;
}
    
略去核心加载jvm的实现,要加载main类,还是比较简单的。主要就是通过前面查找出的main_class 然后通过在jvm的实例中获取到的各方法的函数指针,然后按照字节码的规范,调用 MainClass.main(String[]) 方法。当然了,为了兼容其他非main的场景,它还有很多附加处理逻辑。
另外,如何翻译java代码,并执行,这是jvm的重中之重。即编译原理的拿手好戏,也是我们普通程序员越不过的坎!但至少,我们来到了门槛前面!
2. jvm启动框架前面讲了加载main方法的过程,大致理解了c如何启动调用java的main的。那么,这又是如何调用准备的呢,在这之前都需要做哪些准备呢?
实际上,这是平台相关的实现。
// share/bin/java.c
/*
 * Entry point.
 */
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 */
        const char* lname                       /* launcher name */
        jboolean javaargs                       /* JAVA_ARGS */
        jboolean cpwildcard                     /* classpath wildcard*/
        jboolean javaw                          /* windows-only javaw */
        jint ergo                               /* ergonomics class policy */
)
{
    int mode = LM_UNKNOWN;
    char *what = NULL;
    char *cpath = 0;
    char *main_class = NULL;
    int ret;
    InvocationFunctions ifn;
    jlong start  end;
    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;
    // 初始化启动器
    InitLauncher(javaw);
    // 打印状态
    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);
    }
    /*
     * Make sure the specified version of the JRE is running.
     *
     * There are three things to note about the SelectVersion() routine:
     *  1) If the version running isn't correct  this routine doesn't
     *     return (either the correct version has been exec'd or an error
     *     was issued).
     *  2) Argc and Argv in this scope are *not* altered by this routine.
     *     It is the responsibility of subsequent code to ignore the
     *     arguments handled by this routine.
     *  3) As a side-effect  the variable "main_class" is guaranteed to
     *     be set (if it should ever be set).  This isn't exactly the
     *     poster child for structured programming  but it is a small
     *     price to pay for not processing a jar file operand twice.
     *     (Note: This side effect has been disabled.  See comment on
     *     bugid 5030265 below.)
     */
    // 解析命令行参数,选择一jre版本
    SelectVersion(argc  argv  &main_class);
    CreateExecutionEnvironment(&argc  &argv 
                               jrepath  sizeof(jrepath) 
                               jvmpath  sizeof(jvmpath) 
                               jvmcfg   sizeof(jvmcfg));
    if (!IsJavaArgs()) {
        // 设置一些特殊的环境变量
        SetJvmEnvironment(argc argv);
    }
    ifn.CreateJavaVM = 0;
    ifn.GetDefaultJavaVMInitArgs = 0;
    if (JLI_IsTraceLauncher()) {
        start = CounterGet();
    }
    // 加载VM  重中之重
    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;
    --argc;
    // 解析更多参数信息
    if (IsJavaArgs()) {
        /* Preprocess wrapper arguments */
        TranslateApplicationArgs(jargc  jargv  &argc  &argv);
        if (!AddApplicationOptions(appclassc  appclassv)) {
            return(1);
        }
    } else {
        /* Set default CLASSPATH */
        cpath = getenv("CLASSPATH");
        if (cpath == NULL) {
            cpath = ".";
        }
        SetClassPath(cpath);
    }
    /* Parse command line options; if the return value of
     * ParseArguments is false  the program should exit.
     */
    // 解析参数
    if (!ParseArguments(&argc  &argv  &mode  &what  &ret  jrepath))
    {
        return(ret);
    }
    /* Override class path if -jar flag was specified */
    if (mode == LM_JAR) {
        SetClassPath(what);     /* Override class path */
    }
    /* set the -Dsun.java.command pseudo property */
    SetJavaCommandLineProp(what  argc  argv);
    /* Set the -Dsun.java.launcher pseudo property */
    SetJavaLauncherProp();
    /* set the -Dsun.java.launcher.* platform properties */
    SetJavaLauncherPlatformProps();
    return JVMInit(&ifn  threadStackSize  argc  argv  mode  what  ret);
}
// macos/bin/java_md_macos.c
// MacOSX we may continue in the same thread
int
JVMInit(InvocationFunctions* ifn  jlong threadStackSize 
                 int argc  char **argv 
                 int mode  char *what  int ret) {
    if (sameThread) {
        JLI_TraceLauncher("In same thread\n");
        // need to block this thread against the main thread
        // so signals get caught correctly
        __block int rslt = 0;
        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
        {
            NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock: ^{
                JavaMainArgs args;
                args.argc = argc;
                args.argv = argv;
                args.mode = mode;
                args.what = what;
                args.ifn  = *ifn;
                // 调用 JavaMain()
                rslt = JavaMain(&args);
            }];
            /*
             * We cannot use dispatch_sync here  because it blocks the main dispatch queue.
             * Using the main NSRunLoop allows the dispatch queue to run properly once
             * SWT (or whatever toolkit this is needed for) kicks off it's own NSRunLoop
             * and starts running.
             */
            [op performSelectorOnMainThread:@selector(start) withObject:nil waitUntilDone:YES];
        }
        [pool drain];
        return rslt;
    } else {
        return ContinueInNewThread(ifn  threadStackSize  argc  argv  mode  what  ret);
    }
}
    
以上是mac调用 javaMain的方式。在windows 以及linux上则稍有不同。
// windows/bin/java_md.c
int
JVMInit(InvocationFunctions* ifn  jlong threadStackSize 
        int argc  char **argv 
        int mode  char *what  int ret)
{
    ShowSplashScreen();
    return ContinueInNewThread(ifn  threadStackSize  argc  argv  mode  what  ret);
}
// java.c
int
ContinueInNewThread(InvocationFunctions* ifn  jlong threadStackSize 
                    int argc  char **argv 
                    int mode  char *what  int ret)
{
    /*
     * If user doesn't specify stack size  check if VM has a preference.
     * Note that HotSpot no longer supports JNI_VERSION_1_1 but it will
     * return its default stack size through the init args structure.
     */
    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;
      }
    }
    { /* Create a new thread to create JVM and invoke main method */
      JavaMainArgs args;
      int rslt;
      args.argc = argc;
      args.argv = argv;
      args.mode = mode;
      args.what = what;
      args.ifn = *ifn;
      // 传入 JavaMain() 函数参数,在新线程中运行 JavaMain
      rslt = ContinueInNewThread0(JavaMain  threadStackSize  (void*)&args);
      /* If the caller has deemed there is an error we
       * simply return that  otherwise we return the value of
       * the callee
       */
      return (ret != 0) ? ret : rslt;
    }
}
/*
 * Block current thread and continue execution in a new thread
 */
int
ContinueInNewThread0(int (JNICALL *continuation)(void *)  jlong stack_size  void * args) {
    int rslt = 0;
    unsigned thread_id;
#ifndef STACK_SIZE_PARAM_IS_A_RESERVATION
#define STACK_SIZE_PARAM_IS_A_RESERVATION  (0x10000)
#endif
    /*
     * STACK_SIZE_PARAM_IS_A_RESERVATION is what we want  but it's not
     * supported on older version of Windows. Try first with the flag; and
     * if that fails try again without the flag. See MSDN document or HotSpot
     * source (os_win32.cpp) for details.
     * 调用底层内核方法,创建新线程,将JavaMain(args)作为函数调用处理。创建后立即执行
     * 如果第一次带参创建失败,则弃参重试一次
     */
    HANDLE thread_handle =
      (HANDLE)_beginthreadex(NULL 
                             (unsigned)stack_size 
                             continuation 
                             args 
                             STACK_SIZE_PARAM_IS_A_RESERVATION 
                             &thread_id);
    if (thread_handle == NULL) {
      thread_handle =
      (HANDLE)_beginthreadex(NULL 
                             (unsigned)stack_size 
                             continuation 
                             args 
                             0 
                             &thread_id);
    }
    /* AWT preloading (AFTER main thread start) */
#ifdef ENABLE_AWT_PRELOAD
    /* D3D preloading */
    if (awtPreloadD3D != 0) {
        char *envValue;
        /* D3D routines checks env.var J2D_D3D if no appropriate
         * command line params was specified
         */
        envValue = getenv("J2D_D3D");
        if (envValue != NULL && JLI_StrCaseCmp(envValue  "false") == 0) {
            awtPreloadD3D = 0;
        }
        /* Test that AWT preloading isn't disabled by J2D_D3D_PRELOAD env.var */
        envValue = getenv("J2D_D3D_PRELOAD");
        if (envValue != NULL && JLI_StrCaseCmp(envValue  "false") == 0) {
            awtPreloadD3D = 0;
        }
        if (awtPreloadD3D < 0) {
            /* If awtPreloadD3D is still undefined (-1)  test
             * if it is turned on by J2D_D3D_PRELOAD env.var.
             * By default it's turned OFF.
             */
            awtPreloadD3D = 0;
            if (envValue != NULL && JLI_StrCaseCmp(envValue  "true") == 0) {
                awtPreloadD3D = 1;
            }
         }
    }
    if (awtPreloadD3D) {
        AWTPreload(D3D_PRELOAD_FUNC);
    }
#endif /* ENABLE_AWT_PRELOAD */
    if (thread_handle) {
      // 等待线程结束,并获取返回码
      WaitForSingleObject(thread_handle  INFINITE);
      GetExitCodeThread(thread_handle  &rslt);
      CloseHandle(thread_handle);
    } else {
      // 如果实在是创建线程失败,则自身直接执行该方法即可
      rslt = continuation(args);
    }
#ifdef ENABLE_AWT_PRELOAD
    if (awtPreloaded) {
        AWTPreloadStop();
    }
#endif /* ENABLE_AWT_PRELOAD */
    return rslt;
}
    
在linux中的创建线程如下:
// solaris/bin/java_md_solinux.c
/*
 * Block current thread and continue execution in a new thread
 */
int
ContinueInNewThread0(int (JNICALL *continuation)(void *)  jlong stack_size  void * args) {
    int rslt;
#ifdef __linux__
    pthread_t tid;
    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);
    }
    // 常见的 pthread_xx 方式 创建线程
    if (pthread_create(&tid  &attr  (void *(*)(void*))continuation  (void*)args) == 0) {
      void * tmp;
      pthread_join(tid  &tmp);
      rslt = (int)tmp;
    } else {
     /*
      * Continue execution in current thread if for some reason (e.g. out of
      * memory/LWP)  a new thread can't be created. This will likely fail
      * later in continuation as JNI_CreateJavaVM needs to create quite a
      * few new threads  anyway  just give it a try..
      */
      rslt = continuation(args);
    }
    pthread_attr_destroy(&attr);
#else /* ! __linux__ */
    thread_t tid;
    long flags = 0;
    if (thr_create(NULL  stack_size  (void *(*)(void *))continuation  args  flags  &tid) == 0) {
      void * tmp;
      thr_join(tid  NULL  &tmp);
      rslt = (int)tmp;
    } else {
      /* See above. Continue in current thread if thr_create() failed */
      rslt = continuation(args);
    }
#endif /* __linux__ */
    return rslt;
}
    
即相同的语义,不同平台下的各自实现而已。
通过C语言的函数传递,从而进行调用。通过创建一个新线程,调用JavaMain(); 由javaMain处理所有java事务。而自身则只需等待JavaMain完成即可。这也符合面向过程编程思想。
3. jvm初始化小结我们平时是通过 java xxx.xxx 或者 java -jar xx.jar 启动jvm 通过这两篇文章,我们也看清了其背后的原理。就是通过解析各种参数,验证各种参数,验证jre环境,然后验证jre是否可用,最后将指定的mainClass加载出来。丢到一个新的线程中去执行,将执行权力转发给java代码,最后等待该线程完成。
应该说事个流程没有难点,或者说一切都很理所当然。但其中的核心,都是通过加载JavaVM()去做的,也就是真正的jvm. 然后通过或者对应的几个接口方法地址,进行调用,从而完成启动任务。所以,java虽是一个启动命令,但核心并不在这里。而是在 hotspot 或其他地方。jdk 毕竟只是一个工具箱而已。jre 才是关键。
看完三件事❤️如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
- 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
 - 关注头条号 『 JAVA后端架构 』,不定期分享原创知识。
 - 同时可以期待后续文章ing
 - 关注作者后台私信【888】有惊喜相送
 

作者:等你归去来
出处:https://www.cnblogs.com/yougewe/p/14400859.html




