1. 前言

上文介绍了HotSpot编译和调试的方法,而这篇文章将迈出正式调试的第一步——调试HotSpot的启动过程。

学习启动过程可以帮助我们了解程序的入口,并对虚拟机的运行有个整体的把握,方便日后深入学习具体的一些模块。

2. 整体感知启动过程

整体的感知启动过程可以在启动时添加_JAVA_LAUNCHER_DEBUG=1的环境变量。这样JVM会输出详细的打印。

通过这些打印,我们大致能了解到启动过程发生了什么。

----_JAVA_LAUNCHER_DEBUG----
Launcher state:
debug:on
javargs:off
program name:java
launcher name:openjdk
javaw:off
fullversion:1.8.0-internal-debug-xieshang_2020_12_18_09_49-b00
dotversion:1.8
ergo_policy:DEFAULT_ERGONOMICS_POLICY
Command line args:
argv[0] = /home/xieshang/learn-jvm/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/bin/java
argv[1] = com.insanexs/HelloHotspot
JRE path is /home/xieshang/learn-jvm/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk
jvm.cfg[0] = ->-server<-
jvm.cfg[1] = ->-client<-
1 micro seconds to parse jvm.cfg
Default VM: server
Does `/home/xieshang/learn-jvm/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/lib/amd64/server/libjvm.so' exist ... yes.
mustsetenv: FALSE
JVM path is /home/xieshang/learn-jvm/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/lib/amd64/server/libjvm.so
1 micro seconds to LoadJavaVM
JavaVM args:
version 0x00010002, ignoreUnrecognized is JNI_FALSE, nOptions is 5
option[ 0] = '-Dsun.java.launcher.diag=true'
option[ 1] = '-Djava.class.path=/home/xieshang/learn-open-jdk'
option[ 2] = '-Dsun.java.command=com.insanexs/HelloHotspot'
option[ 3] = '-Dsun.java.launcher=SUN_STANDARD'
option[ 4] = '-Dsun.java.launcher.pid=4485'
1 micro seconds to InitializeJVM
Main class is 'com.insanexs/HelloHotspot'
App's argc is 0
1 micro seconds to load main class
----_JAVA_LAUNCHER_DEBUG----

从上面的打印大致可以看出有这么几步:

  1. 打印了启动器的状态,包括版本号、程序名等
  2. 打印了传给程序命令行参数,第一个是java命令的相信路径,第二个虚拟机将要执行的java代码
  3. 解析JRE路径,解析jvm.cfg
  4. 加载libjvm库
  5. 解析虚拟机参数
  6. 初始化虚拟机
  7. 虚拟机加载要执行的Java主类,解析参数并执行

3. 启动过程说明

我们就以上面划分的阶段为整体脉络,再深入的看看各阶段的具体逻辑。

3.1 启动入口

虚拟机程序运行的入口是在main.c/main方法中。之后会调用java.c/JLI_Launch方法。

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.)
*/
SelectVersion(argc, argv, &main_class); //版本检测 /************************** 创建执行环境 ********************/
CreateExecutionEnvironment(&argc, &argv,
jrepath, sizeof(jrepath),
jvmpath, sizeof(jvmpath),
jvmcfg, sizeof(jvmcfg));//解析相关环境 获取jre路径、jvmlib库和jvm.cfg /************************** 设置虚拟机环境 ********************/
if (!IsJavaArgs()) {
SetJvmEnvironment(argc,argv);
} ifn.CreateJavaVM = 0;
ifn.GetDefaultJavaVMInitArgs = 0; if (JLI_IsTraceLauncher()) {
start = CounterGet();
} /************************** 加载虚拟机 ********************/
if (!LoadJavaVM(jvmpath, &ifn)) { //加载 主要是从jvmlib库中解析函数地址 赋值给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"); //添加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) { //如果是java -jar 则覆盖classpath
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);
}

这个方法比较长,但是可以划分为几个部分去分析:

3.1.1 前期初始化工作和状态打印

这里的初始化部分包括一些参数值的声明,特殊结构体InvocationFuntions的声明,启动器的初始化。

其中声明的参数会在后续的启动过程用来存储相关信息,例如保存JVM、JRE相关路径等。

InvocationFuntions是个重要的结构体,其中包含了创建JVM会被调用的三个函数指针。

typedef struct {
CreateJavaVM_t CreateJavaVM; //指向负责创建JavaVM和JNIEnv结构的函数指针
GetDefaultJavaVMInitArgs_t GetDefaultJavaVMInitArgs; //指向获取默认JVM初始参数的函数指针
GetCreatedJavaVMs_t GetCreatedJavaVMs; //指向获取JVM的函数指针
} InvocationFunctions;

InitLaucher方法主要就是根据_JAVA_LAUNCHER_DEBUG这个环境变量会决定后续是否输出DEBUG的打印。

在开启了launcher_debug后,DumpState()方法会打印出启动状态,并且之后打印出命令行参数。

3.1.2 检验版本

SelectVersion会验证用户指定的java版本和实际执行的java版本是否兼容,如果不兼容会退出进程。用户可以通过_JAVA_VERSION_SET的环境变量或是jar包中manifest文件等方式指定运行的java版本。

3.1.3 创建执行环境

CreateExecutionEnvironment会为后续的启动创建执行环境,这一步骤中主要是确定jdk所在的路径,解析jvmcfg和确认libjvm是否存在等。

  1. 主要是根据处理器类型和主路径确定出JRE的路径
  2. 以同样的方式确定jvm.cfg的文件位置,并解析jvm.cfg(jvm.cfg里面是一些虚拟机的默认配置,如常见的指定以客户端或服务端模式运行)
  3. 检查虚拟机类型(-server/-client),可以是jvm.cfg指定或是由启动参数指定
  4. 确定libjvm库的位置,校验库是否存在,这个库核心的函数库
3.1.4 设置虚拟机环境

SetJvmEnviroment主要解析NativeMemoryTracking参数,可以用来追踪本地内存的使用情况

3.1.5 加载虚拟机

前期环境准备好之后,LoadJavaVM()会从之前确定的路径,加载libjvm库,并将其中的库中JNI_CreateJavaVM,JNI_GetDefaultJavaVMInitArgsJNI_GetCreatedJavaVMs三个函数赋值给ifn。

这三个函数会在之后创建虚拟机时被使用。

3.1.6 解析参数

这里有两个部分,一是解析命令行传入的参数,看是否有特定的JVM配置选项。这些参数会被用于后续虚拟机的创建上。这一过程主要发生在ParseArguments()中。另一个部分就是添加一些特定的虚拟机参数,发生在SetJavaCommandLinePropSetJavaLaucherPropSetJavaLaucherPlatformProps中。

3.1.7 虚拟机初始化

在环境都准备好之后,会由JVMInit()执行虚拟机初始化工作,首先会通过ShowSplashScreen()方法加载启动动画,之后会进入CountinueInNewThread()方法,由新的线程负责创建虚拟机的工作。

3.2 在新线程中继续虚拟机的创建

通过上文的介绍,我们找到了java.c/ConutinueInNewThread()的方法。这个方法分为两个部分,第一部分就是确定线程栈的深度,第二部分就是由ContinueInNewThread0()这个方法实现真正的虚拟机创建过程。

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);
} //创建线程并执行方法
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 /* __solaris__ */
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 /* !__solaris__ */
return rslt;
}

在这个方法中,首先调用了pthread_create()函数创建了一个新线程,同时旧线程被jion等待新线程运行完成后返回。

pthread_create()是unix操作系统创建线程的函数,它的第一个参数表示线程标识,第二参数表示线程属性,第三个参数表示创建线程所要执行函数的地址,第四个参数则是将要执行的函数的参数。

等到新线程运行完成后,旧的线程也会返回。此时说明运行结束,进程将会退出。

需要注意的是此时传入的函数地址,它是指向java.c/JavaMain()函数。也就是说新创建的线程将会开始执行该函数。

3.3 虚拟机创建、Java程序运行的主过程——JavaMain

新创建的线程会去执行JavaMain()函数,正式进入了创建虚拟机、运行Java代码的过程。

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, end; RegisterThread(); /*******************初始化JVM、打印相关信息********************************/
start = CounterGet();
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 */
//打印Java程序的参数
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]);
}
} /******************获取Java程序的主类***************************/
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.
*/
mainClass = LoadMainClass(env, mode, what);//加载mainClass
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); //获取application class
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.
*/
PostJVMInit(env, appClass, vm); // JVM 初始化后置处理
/*
* 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方法************************/
mainID = (*env)->GetStaticMethodID(env, mainClass, "main",
"([Ljava/lang/String;)V"); //获取main class的 main(String[] args)方法
CHECK_EXCEPTION_NULL_LEAVE(mainID); /*******************封装参数,调用main方法*****************/
/* Build platform specific argument array */
mainArgs = CreateApplicationArgs(env, argv, argc); //封装 main(String[] args) 方法的参数args
CHECK_EXCEPTION_NULL_LEAVE(mainArgs); /* Invoke main method. */
(*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs); //调用main(String args)方法 /*
* 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(); //线程解绑 销毁JVM
}
3.3.1 参数解析

之前上文解析得到的命令行参数等都被封装在JavaMainArgs结构体中,传给了JavaMain方法。因此需要从这个结构体中取回参数。

另外,还创建了一些变量用于之后的过程中存储值,譬如jclass,jmethodID等。

3.3.2 初始化虚拟机,打印相关信息

上述代码中的InitializeJVM()方法会负责虚拟机的初始化过程。其代码如下:

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 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);
} r = ifn->CreateJavaVM(pvm, (void **)penv, &args); //通过ifn的函数指针 调用CreateJavaVM函数初始化JavaVM 和 JNIEnv
JLI_MemFree(options);
return r == JNI_OK;
}

先获取虚拟机参数,在通过ifn结构体中CreateJavaVM指针,调用正式创建Java虚拟机的函数JNI_CreateJavaVM

JNI_CreateJavaVM代码的主要流程如下:

  1. 先由Threads::create_vm()方法创建虚拟机
  2. 给两个重要的指针赋值,分别是JavaVM * 和 JNIEnv
  3. 一些后置处理,例如通过JVMTI(可以说是虚拟机的工具接口,提供了对虚拟机调试、监测等等的功能)、事件提交等

针对第一点,Threads::create_vm()是负责创建虚拟机,整个过程相对复杂,需要初始化很多模块,创建虚拟机的后台线程,加载必要的类等等,这里不做深入分析。之后有时间可以单独分析这一过程。

针对第二点中提到的两个数据结构,非常重要。我们可以看看它们的具体的内容。

JavaVM

JavaVM结构内部包的是JNIInvokeInterface_结构,因此我们直接看一下JNIInvokeInterface_的结构

struct JNIInvokeInterface_ {
//预留字段
void *reserved0;
void *reserved1;
void *reserved2; jint (JNICALL *DestroyJavaVM)(JavaVM *vm); //销毁虚拟机的函数指针 jint (JNICALL *AttachCurrentThread)(JavaVM *vm, void **penv, void *args); //绑定线程的函数指针 jint (JNICALL *DetachCurrentThread)(JavaVM *vm); //解绑线程的函数指针 jint (JNICALL *GetEnv)(JavaVM *vm, void **penv, jint version); //获取JNIEnv结构的函数指针 jint (JNICALL *AttachCurrentThreadAsDaemon)(JavaVM *vm, void **penv, void *args);//将线程转为后台线程
};

可以看到主要是一些和虚拟机操作的相关函数。

JNIEnv

JNIEnv结构内部包的是JNINativeInterface结构,这个结构同样定义了很多函数指针,代码太长,这里就不直接贴出了。有兴趣的可以在jni.h中自行查看。如果对结构中的方法分类的话,可以分成以下几类:

  • 获取虚拟机信息
  • 获取相关类和方法,方法执行
  • 获取/设置对象字段
  • 静态方法、静态变量的获取与设置
  • 常见类型的对象的创建和释放
  • 创建直接内存、访问锁等

总之,提供了通过C++代码访问Java程序的能力(这对于从事JNI开发的人来说十分重要)。

3.3.3 确定Java程序的主类

了解完成虚拟机的初始化过程后,再回到JavaMain()方法中,之后是通过LoadMainClass()GetApplicationClass()方法确定Java代码的主类。

如果我们在运行指定了Java类,那么这个类就是主类。这里还会调用LauncherHelper.checkAndLoadMain()检验主类是否合法。LauncherHelper的Java代码,这里就是上面介绍的JNIEnv的能力在C++的代码中执行Java代码。

对于一些没有主类的程序,需要通过LaucherHelper.getApplicationClass()确定程序类。

3.3.4 从主类中获取main方法的methodID,并调用方法

再确定了mainClass之后,还需要找到该类定义的main(),获取main()方法,然后将程序参数封装,传递给main()执行,线程会以此为入口,开始执行Java程序。

这里的找方法和执行方法同样是依赖了JNIEnv中GetStaticMethodIDCallStaticVoidMethod

所以我们的main()方法总是static void的。

3.3.5 获取执行结果,退出虚拟机

当线程从Main()方法中返回,说明Java程序已经执行完成(或是异常退出),这时候虚拟机会检查运行结果,并解绑线程销毁虚拟机,最终退出。

HotSpot学习(二):虚拟机的启动过程源码解析的更多相关文章

  1. Spring IOC容器启动流程源码解析(四)——初始化单实例bean阶段

    目录 1. 引言 2. 初始化bean的入口 3 尝试从当前容器及其父容器的缓存中获取bean 3.1 获取真正的beanName 3.2 尝试从当前容器的缓存中获取bean 3.3 从父容器中查找b ...

  2. scrapy 源码解析 (二):启动流程源码分析(二) CrawlerProcess主进程

    CrawlerProcess主进程 它控制了twisted的reactor,也就是整个事件循环.它负责配置reactor并启动事件循环,最后在所有爬取结束后停止reactor.另外还控制了一些信号操作 ...

  3. Spring IOC容器启动流程源码解析(一)——容器概念详解及源码初探

    目录 1. 前言 1.1 IOC容器到底是什么 1.2 BeanFactory和ApplicationContext的联系以及区别 1.3 解读IOC容器启动流程的意义 1.4 如何有效的阅读源码 2 ...

  4. redis启动过程源码解析

    redis整个程序的入口函数在server.c中的main函数,函数调用关系如下图1,调用顺序为从上到下,从左至右. 图1 redis启动函数调用图 main函数源码如下,1-55行根据配置文件和启动 ...

  5. NioEventLoop启动流程源码解析

    NioEventLoop的启动时机是在服务端的NioServerSocketChannel中的ServerSocketChannel初始化完成,且注册在NioEventLoop后执行的, 下一步就是去 ...

  6. Flume-NG启动过程源码分析(二)(原创)

    在上一节中讲解了——Flume-NG启动过程源码分析(一)(原创)  本节分析配置文件的解析,即PollingPropertiesFileConfigurationProvider.FileWatch ...

  7. Spark(五十一):Spark On YARN(Yarn-Cluster模式)启动流程源码分析(二)

    上篇<Spark(四十九):Spark On YARN启动流程源码分析(一)>我们讲到启动SparkContext初始化,ApplicationMaster启动资源中,讲解的内容明显不完整 ...

  8. Android Content Provider的启动过程源码分析

    本文參考Android应用程序组件Content Provider的启动过程源码分析http://blog.csdn.net/luoshengyang/article/details/6963418和 ...

  9. Spark(四十九):Spark On YARN启动流程源码分析(一)

    引导: 该篇章主要讲解执行spark-submit.sh提交到将任务提交给Yarn阶段代码分析. spark-submit的入口函数 一般提交一个spark作业的方式采用spark-submit来提交 ...

随机推荐

  1. 第十一章 Python 支撑正则表达式处理的re模块

    re模块是Python中支持正则表达式处理的模块,老猿学了之后,发现这部分内容太多,要表述清楚需要开单章才能写清楚,但老猿觉得re模块的使用对多数人来说要通过教程学习去熟练掌握很难,需要经常接触练习加 ...

  2. es6 Object对象扩展新方法

    ES6给Object拓展了许多新的方法,如: keys(obj):获取对象的所有key形成的数组     var obj = { foo: 'bar', baz: 42 };  Object.keys ...

  3. 三、git学习之——管理修改、撤销修改、删除文件

    一.管理修改 现在,假定你已经完全掌握了暂存区的概念.下面,我们要讨论的就是,为什么Git比其他版本控制系统设计得优秀,因为Git跟踪并管理的是修改,而非文件. 你会问,什么是修改?比如你新增了一行, ...

  4. docker 添加Portainer容器图形化管理工具

    主要参照了这边博客,但还是有些问题https://www.cnblogs.com/Bug-Hunter/p/12023130.html 比如端口9000得开启,docker端口映射得开启,得开启ip4 ...

  5. AWT06-事件处理

    在AWT中,用户的所有操作都要由事件处理来完成.Frame和组件本身没有处理事件的能力. 1.GUI事件处理机制 定义:在某个组件上发生某种操作时,自动触发某段代码. 事件处理涉及4个重要概念: 事件 ...

  6. [从源码学设计]蚂蚁金服SOFARegistry之时间轮的使用

    [从源码学设计]蚂蚁金服SOFARegistry之时间轮的使用 目录 [从源码学设计]蚂蚁金服SOFARegistry之时间轮的使用 0x00 摘要 0x01 业务领域 1.1 应用场景 0x02 定 ...

  7. Private jre vs Public jre

    今天说一下java环境构建中,jre相关的小知识点. private jre:一般是C:\Program Files\jdk1.8.0\jre,必须安装,它为jdk的运行提供必需的环境. public ...

  8. Vue 组件化开发的思想体现

    现实中的组件化思想化思想体现 标准(同一的标准) 分治(多人同时开发) 重用(重复利用) 组合(可以组合使用) 编程中的组件化思想 组件化规范:Web Components 我们希望尽可能多的重用代码 ...

  9. RocketMQ集群搭建(3m-3s-async)

    RocketMQ集群搭建(3m-3s-async) 各角色介绍 角色 作用 Producer 消息发送者,将消息发送到 Broker.无状态,其与NameServer集群中的一个节点建立长连接,定期从 ...

  10. Linux vmstat 使用说明

    摘自 https://www.thomas-krenn.com/en/wiki/Linux_Performance_Measurements_using_vmstat Linux Performanc ...