OpenJDK8 Java程序启动解析(上) - Java程序是怎样执行到main方法的 置顶!

  |   0 评论   |   1,556 浏览

上篇主要简单分析一下JavaLauncher的启动过程,以及怎样执行到我们的Java main方法的. 其中涉及一些JNI的调用和类的解析加载. 这些部分本篇暂时不详细展开. 下篇会简单介绍下JNI部分虚拟机初始化.

这是跟我一起读OpenJDK源码的第二个系列.第一个系列为: OpenJDK8 编译构建基础设施详解,你可以在我的 跟我一起读OpenJDK源码 找到这些文章.

  1. OpenJDK8 编译构建基础设施详解(1) - A New OpenJDK Build-Infra Detail With GNU MAKE And AutoConf
  2. OpenJDK8 编译构建基础设施详解(2) - Make流程解析
  3. OpenJDK8 编译构建基础设施详解(3) - java命令是怎么编译出来的

现在我们已经知道了Java程序启动的大体框架. 我们的程序启动是从一个Launcher 入口开始执行的.而且这个Launcher入口被不同的Launcher共用. 在JDK的bin目录的各种命令都使用这个Launcher入口.具体为: ./jdk/src/share/bin/main.c,关于Launcher 的更多细节参考上一篇:Java命令是怎么编译出来的,下面我们就结合源代码具体看一看整个过程是怎样到达Java类的Main函数的.

概要

我们先上一张整体流程的图.让大家有一个整体的大概认识.后面再结合着图与源代码一步步的解析.

image.png

结合着上图的标注, 启动过程会涉及到两个线程.一个是main函数执行所在的线程,线程名称为: Thread-1 (注: Linux平台),另外一个是JavaMain方法所在的线程:Thread-2, 大概的核心流程是在Thread-2中完成. Thread-1的main方法完成对JavaMain的引导后,会处于阻塞等待状态. 直到Thread-2执行完成.

虚拟机的创建与初始化,是使用JNI方法进行的. 在单独的JNI模块里面, 他是一个动态链接库. 在Linux平台下的名称是: libjvm.so,它的目录大概是: /home/firfor/sourcecode/jdk8u/build/linux-x86_64-normal-server-slowdebug/jdk/lib/amd64/server/libjvm.so. 这个可以认为是JVM的本地实现的核心模块, 在此先有一个基本的概念就可以了.

注:
Launcher代码主要位置: https://github.com/AdoptOpenJDK/openjdk-jdk8u/tree/master/jdk/src/share/bin
这里没有给官方的hg的仓库,主要是为了在线阅读方便.

main

代码位置: ./jdk/src/share/main.c
代码链接: https://github.com/AdoptOpenJDK/openjdk-jdk8u/blob/master/jdk/src/share/bin/main.c
代码作用: 作为对外的编译入口, 封装不同平台的入口函数差异. 直接调用JLI_Launch函数

此处封装了windows平台和类unix平台不同的实现. windows平台是使用WinMain. 而Linux平台使用的C语言普通的main函数. 出现这个不同的原因是: 在windows平台带GUI入口的WIN程序和控制台类型的程序入口是不一样的,而Java程序本身是要处理GUI相关的编程的.因此在windows平台.即使一般的java程序没有处理gui.其执行入口也是带GUI的WIN程序.

int WINAPI
WinMain(HINSTANCE inst, HINSTANCE previnst, LPSTR cmdline, int cmdshow)
{
    int margc;
    char** margv;
    const jboolean const_javaw = JNI_TRUE;

    __initenv 
= _environ;
// ..........................
}

这里给出Linux平台的启动时的截图, 可以发现除了入口传入的margcmargv以外, 都是一些静态常量.我们可以看到有如下静态常量:

  • const_jargs: null
  • const_appclasspath: null
  • FULL_VERSION
  • DOT_VERSION
  • const_progname : java
  • const_launcher: openjdk
  • const_cpwildcard: 1
  • const_javaw: 0
  • const_ergo_class: 0

image.png

以上的大部分常量是在define.h 中定义的source链接:

#ifdef JAVA_ARGS
static const char* const_progname = "java";
static const char* const_jargs[] = JAVA_ARGS;
/*
 * ApplicationHome is prepended to each of these entries; the resulting
 * strings are concatenated (separated by PATH_SEPARATOR) and used as the
 * value of -cp option to the launcher.
 */
#ifndef APP_CLASSPATH
#define APP_CLASSPATH        { "/lib/tools.jar", "/classes" }
#endif /* APP_CLASSPATH */
static const char* const_appclasspath[] = APP_CLASSPATH;
#else  /* !JAVA_ARGS */
#ifdef PROGNAME
static const char* const_progname = PROGNAME;
#else
static char* const_progname = NULL;
#endif
static const char** const_jargs = NULL;
static const char** const_appclasspath = NULL;
#endif /* JAVA_ARGS */

而我们知道,我们的编译时. 传入的宏定义或者参数如下:

注:
可以使用关键字在build.log中查找: Compiling *main.c* (for java)
以下的日志是从上篇文章中摘出来的.

/usr/bin/gcc\
-Wall\
-Wno-parentheses\
-Wextra\
-Wno-unused\
-Wno-unused-parameter\
-Wformat=2\
-pipe\
-fstack-protector\
-D_GNU_SOURCE\
-D_REENTRANT\
-D_LARGEFILE64_SOURCE\
-fno-omit-frame-pointer\
-D_LP64=1\
-D_LITTLE_ENDIAN\
-DLINUX\
-DARCH='"amd64"'\
-Damd64\
-DDEBUG\
-DRELEASE='"1.8.0-internal-debug"'\
-I/sourcecode/jdk8u/build/linux-x86_64-normal-server-slowdebug/jdk/include\
-I/sourcecode/jdk8u/build/linux-x86_64-normal-server-slowdebug/jdk/include/linux\
-I/sourcecode/jdk8u/jdk/src/share/javavm/export\
-I/sourcecode/jdk8u/jdk/src/solaris/javavm/export\
-I/sourcecode/jdk8u/jdk/src/share/native/common\
-I/sourcecode/jdk8u/jdk/src/solaris/native/common\
-fno-strict-aliasing\
-g\
-fPIE\
-I/sourcecode/jdk8u/jdk/src/share/bin\
-I/sourcecode/jdk8u/jdk/src/solaris/bin\
-I/sourcecode/jdk8u/jdk/src/linux/bin\
-DFULL_VERSION='"1.8.0-internal-debug-firfor_2022_02_15_20_53-b00"'\
-DJDK_MAJOR_VERSION='"1"'\
-DJDK_MINOR_VERSION='"8"'\
-DLIBARCHNAME='"amd64"'\
-DLAUNCHER_NAME='"openjdk"'\
-DPROGNAME='"java"'\
-DEXPAND_CLASSPATH_WILDCARDS\
-g\
-O0\
-DTHIS_FILE='"main.c"'\
-c\
-MMD\
-MF\
/sourcecode/jdk8u/build/linux-x86_64-normal-server-slowdebug/jdk/objs/java_objs/main.d\
-o\
/sourcecode/jdk8u/build/linux-x86_64-normal-server-slowdebug/jdk/objs/java_objs/main.o\
/sourcecode/jdk8u/jdk/src/share/bin/main.c

我们没有定义:JAVA_ARGS , 所以: const_jargsconst_appclasspath都为NULL.由于已经在宏定义中定义的:PROGNAME且为java,因此const_progname的值为java,同理: const_launcher也为传入的openjdk. 其它参数就不一一分析了.

#ifdef JAVA_ARGS
...
#else

#ifdef PROGNAME
static const char* const_progname = PROGNAME;
#else
static char* const_progname = NULL;
#endif

static const char** const_jargs = NULL;
static const char** const_appclasspath = NULL;

#endif

另外这两个常量:pnamelanme似乎并没有特殊的作用. 只是传入后作为全局变量存储起来了而已.

# 位置: java.c
static const char *_program_name;
static const char *_launcher_name;

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 */
)
{
    // ...... some code

    _fVersion = fullversion; // 保存一些全局变量
    _dVersion = dotversion;
    _launcher_name = lname;
    _program_name = pname;
    _is_java_args = javaargs;
    _wc_enabled = cpwildcard;
    _ergo_policy = ergo;

在main.c中的方法基本什么都没做. 就是封装出编译的入口.好处理一些编译时确认的常量.然后调用JLI_Lanch函数.

JLI_Lanch

文件位置: ./jdk/src/share/java.c
代码链接:https://github.com/AdoptOpenJDK/openjdk-jdk8u/blob/master/jdk/src/share/bin/java.c
代码作用: 加载hotspot虚拟机核心组件: libjvm.so

image.png

首先是调用:LoadJavaVM加载libjvm.so; 加载完成后数据存储到: InvocationFunctions;

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 */
)
{ 
      InvocationFunctions ifn;
      ifn.CreateJavaVM = 0;
      ifn.GetDefaultJavaVMInitArgs = 0;
      if (!LoadJavaVM(jvmpath, &ifn)) {   
        return(6);
      }
// ....... some code ......
}

我们看一下:InvocationFunctions 的定义.

// 代码定义:  jdk/src/share/bin/java.h

/*
 * Pointers to the needed JNI invocation API, initialized by LoadJavaVM.
 */
typedef jint (JNICALL *CreateJavaVM_t)(JavaVM **pvm, void **env, void *args);
typedef jint (JNICALL *GetDefaultJavaVMInitArgs_t)(void *args);
typedef jint (JNICALL *GetCreatedJavaVMs_t)(JavaVM **vmBuf, jsize bufLen, jsize *nVMs);

typedef struct {
    CreateJavaVM_t CreateJavaVM;
    GetDefaultJavaVMInitArgs_t GetDefaultJavaVMInitArgs;
    GetCreatedJavaVMs_t GetCreatedJavaVMs;
} InvocationFunctions;

里面存储了三个后面在初始化虚拟机时会用到的JNI函数指针.

LoadJavaVM

这个主要是根据平台加载动态链接库. linux环境的源文件信息为:
文件位置: jdk/src/solaris/bin/java_md_solinux.c
代码链接:https://github.com/AdoptOpenJDK/openjdk-jdk8u/blob/master/jdk/src/share/bin/java_md_solinux.c
代码作用: 加载hotspot虚拟机核心组件: libjvm.so , 返回三个函数指针: CreateJavaVM_t, GetDefaultJavaVMInitArgs_t, GetCreatedJavaVMs_t

jboolean
LoadJavaVM(const char *jvmpath, InvocationFunctions *ifn)
{
    void *libjvm;

    JLI_TraceLauncher("JVM path is %s\n", jvmpath);

    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;
    }

    ifn->CreateJavaVM = (CreateJavaVM_t)
        dlsym(libjvm, "JNI_CreateJavaVM");
    if (ifn->CreateJavaVM == NULL) {
        JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());
        return JNI_FALSE;
    }

    ifn->GetDefaultJavaVMInitArgs = (GetDefaultJavaVMInitArgs_t)
        dlsym(libjvm, "JNI_GetDefaultJavaVMInitArgs");
    if (ifn->GetDefaultJavaVMInitArgs == NULL) {
        JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());
        return JNI_FALSE;
    }

    ifn->GetCreatedJavaVMs = (GetCreatedJavaVMs_t)
        dlsym(libjvm, "JNI_GetCreatedJavaVMs");
    if (ifn->GetCreatedJavaVMs == NULL) {
        JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());
        return JNI_FALSE;
    }

    return JNI_TRUE;
}

JVMInit

文件位置: jdk/src/solaris/bin/java_md_solinux.c
代码链接:https://github.com/AdoptOpenJDK/openjdk-jdk8u/blob/master/jdk/src/share/bin/java_md_solinux.c
代码作用: 显示闪屏. 调用平台相关实现创建子线程.JavaMain

有点让人惊讶的是,这个文件的目录是在:solaris. 这个我猜测是Sun公司最开始的时候.自己是有自己的Unix系统平台的. 而Unix平台与Linux平台又很像. (因为他们都遵循POSIX标准,关于这个标准.我想你有兴趣读一下:posix是什么都不知道,就别说你懂Linux了!)

image.png

代码本身没有几行. 看名称,第一行为显示java商标类似的功能. 没有深究. 后面就是一个创建新线程的调用. 这里的调用把之前得到的IFN 指针集合对象进行了传入.

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);
}

ContinueInNewThread

文件位置: jdk/src/solaris/bin/java_md_solinux.c
代码链接:https://github.com/AdoptOpenJDK/openjdk-jdk8u/blob/master/jdk/src/share/bin/java_md_solinux.c
代码作用: 收集创建线程的必要参数JavaMainArgs并封装. 调用内部函数进行创建线程.并且传入了:JavaMain

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;
      }
    }
      /* ****************************************************************************** */ 
      /* 收集参数,准备创建JVM的主线程, 同时调用Main方法                */
    { /* 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;

      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;
    }
}

ContinueInNewThread0

文件位置: jdk/src/solaris/bin/java_md_solinux.c
代码链接:https://github.com/AdoptOpenJDK/openjdk-jdk8u/blob/master/jdk/src/share/bin/java_md_solinux.c
代码作用: 创建JavaMain线程, 并阻塞当前线程.

int
ContinueInNewThread0(int (JNICALL *continuation)(void *), jlong stack_size, void * args) {
    int rslt;
#ifndef __solaris__
    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);
    }
    /* *******************************************************************************************
     * 创建线程: JavaMain
     */
    if (pthread_create(&tid, &attr, (void *(*)(void*))continuation, (void*)args) == 0) {
      void * tmp;
      // 创建完成后.当前线程在此等待
      // Make calling thread wait for termination of the thread TH
      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 /* __solaris__ */
   // solary implement. ommit it.....
#endif /* !__solaris__ */
    return rslt;
}

JavaMain

文件位置: jdk/src/solaris/bin/java_md_solinux.c
代码链接:https://github.com/AdoptOpenJDK/openjdk-jdk8u/blob/master/jdk/src/share/bin/java_md_solinux.c
代码作用: 创建JavaMain线程, 并阻塞当前线程.

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;
    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;

    RegisterThread();

    /* Initialize the virtual machine */
    start = CounterGet(); 
    /* ************************************************************************************************
     * 虚拟机初始化调用. 通过调用libjvm.so的JNI方法完成初始化
     */
    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();
    }

    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;

    /*
     * Get the application's main class
     */
    // 加载主类.
    mainClass = LoadMainClass(env, mode, what);
    CHECK_EXCEPTION_NULL_LEAVE(mainClass);
   
    appClass = GetApplicationClass(env);
    NULL_CHECK_RETURN_VALUE(appClass, -1);
   
    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.
     */
    
    mainID = (*env)->GetStaticMethodID(env, mainClass, "main",
                                       "([Ljava/lang/String;)V");  
    CHECK_EXCEPTION_NULL_LEAVE(mainID);

    /* Build platform specific argument array */
    mainArgs = CreateApplicationArgs(env, argv, argc);
    CHECK_EXCEPTION_NULL_LEAVE(mainArgs);

    // 调用Main方法
    /* 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();
}

以上是JavaMain的执行流程. 主要的核心一点是: 通过调用: InitializeJVM进行JVM初始化. 这个实际调用的是: libjvm.so中的导出方法:JNI_CreateJavaVM,这个才是JVM初始化的核心. 这里我们先放一放他.后面专门抽一篇文章来分析他.

// 定义: jni.h
_JNI_IMPORT_OR_EXPORT_ jint JNICALL
JNI_CreateJavaVM(JavaVM **pvm, void **penv, void *args);

整体调试

为了能够比较直观地看到Launcher的运行.以及怎么到达JavaMain的. 可以在运行调试时添加环境变量:_JAVA_LAUNCHER_DEBUG=1 这样在运行的时候就会比较详细的打印出Launcher的启动日志.

/home/firfor/sourcecode/jdk8u/build/linux-x86_64-normal-server-slowdebug/jdk/bin/java -cp .:/home/firfor/sourcecode/train com.train.test.HelloWorld -Da=b -Dtestc=d
----_JAVA_LAUNCHER_DEBUG----
Launcher state:
 debug:on
 javargs:off
 program name:java
 launcher name:openjdk
 javaw:off
 fullversion:1.8.0-internal-debug-firfor_2022_02_15_20_53-b00
 dotversion:1.8
 ergo_policy:DEFAULT_ERGONOMICS_POLICY
Command line args:
argv[0] = /home/firfor/sourcecode/jdk8u/build/linux-x86_64-normal-server-slowdebug/jdk/bin/java
argv[1] = -cp
argv[2] = .:/home/firfor/sourcecode/train
argv[3] = com.train.test.HelloWorld
argv[4] = -Da=b
argv[5] = -Dtestc=d
JRE path is /home/firfor/sourcecode/jdk8u/build/linux-x86_64-normal-server-slowdebug/jdk
jvm.cfg[0] = ->-server<-
jvm.cfg[1] = ->-client<-
44 micro seconds to parse jvm.cfg
Default VM: server
Does `/home/firfor/sourcecode/jdk8u/build/linux-x86_64-normal-server-slowdebug/jdk/lib/amd64/server/libjvm.so' exist ... yes.
mustsetenv: FALSE
JVM path is /home/firfor/sourcecode/jdk8u/build/linux-x86_64-normal-server-slowdebug/jdk/lib/amd64/server/libjvm.so
1639 micro seconds to LoadJavaVM
JavaVM args:
    version 0x00010002, ignoreUnrecognized is JNI_FALSE, nOptions is 6
    option[ 0] = '-Dsun.java.launcher.diag=true'
    option[ 1] = '-Djava.class.path=/home/firfor/ide/clion-2021.1.3/lib/bootstrap.jar:/home/firfor/ide/clion-2021.1.3/lib/util.jar:/home/firfor/ide/clion-2021.1.3/lib/jdom.jar:/home/firfor/ide/clion-2021.1.3/lib/log4j.jar:/home/firfor/ide/clion-2021.1.3/lib/jna.jar'
    option[ 2] = '-Djava.class.path=.:/home/firfor/sourcecode/train'
    option[ 3] = '-Dsun.java.command=com.train.test.HelloWorld -Da=b -Dtestc=d'
    option[ 4] = '-Dsun.java.launcher=SUN_STANDARD'
    option[ 5] = '-Dsun.java.launcher.pid=16523'
323422 micro seconds to InitializeJVM
Main class is 'com.train.test.HelloWorld'
App's argc is 2
    argv[ 0] = '-Da=b'
    argv[ 1] = '-Dtestc=d'
63183 micro seconds to load main class
----_JAVA_LAUNCHER_DEBUG----
hello, this file is compile by debug jdk

Process finished with exit code 0

评论

发表评论


取消