《HotSpot实战》—— 2.2 启动-阿里云开发者社区

开发者社区> 异步社区> 正文

《HotSpot实战》—— 2.2 启动

简介: 通用启动器(Generic Launcher)是指我们比较熟悉的JDK命令程序:java(含javaw)。java是由JDK自带的启动Java应用程序的工具。为启动一个Java应用程序,java将准备一个Java运行时环境(即JRE)、加载指定的类并调用它的main方法。
+关注继续查看

本节书摘来异步社区《HotSpot实战》一书中的第2章,第2.2节,作者:陈涛,更多章节内容可以访问云栖社区“异步社区”公众号查看。

2.2 启动

Launcher(启动器),是用来启动JVM和应用程序的工具。在这一节中,我们将看到HotSpot中提供了两种Launcher类型,分别是通用启动器和调试版启动器。

2.2.1 Launcher

通用启动器(Generic Launcher)是指我们比较熟悉的JDK命令程序:java(含javaw)。java是由JDK自带的启动Java应用程序的工具。为启动一个Java应用程序,java将准备一个Java运行时环境(即JRE)、加载指定的类并调用它的main方法。类加载的前提条件是由JRE在指定路径下找到类加载器和应用程序类。一般来说,JRE将在以下3种路径下搜索类加载器和其他类:

  • 引导类路径(bootstrap class path);
  • 已安装的扩展(installed extensions);
  • 用户类路径(user class path)。

类被加载进来之后,java会将全限定类名或JAR文件名之后的非选项类参数作为参数传递给main方法。

javaw命令等同于java,只是javaw没有控制台窗口。当你不想显示一个命令提示符窗口时,可以使用javaw。但是如果由于某些原因启动失败,javaw仍将显示一个对话框提供错误信息。

1.基本用法

java和javaw的命令格式如下所示:

java [ option ] class [ argument ... ]
java [ option ] -jar file.jar [ argument ... ]
javaw [ option ] class [ argument ... ]
javaw [ option ] -jar file.jar [ argument ... ]

其中class是要调用的类名,而file.jar是要调用的JAR文件名。

值得注意的是,我们需要区分选项和参数的不同用途。

选项(option)是传递给VM的参数。目前,有两类VM选项,包括标准VM选项和非标准VM选项。其中,非标准选择在使用时以“-X”或“-XX”指定。
参数(argument)是传递给main方法的参数。
注意 对于启动器,有一套标准选项(standard options),在当前和将来的版本中都将支持。此外, HotSpot虚拟机默认提供一套非标选项(non-standard options),这些非标选项有可能在将来版本中更改。另外,32位JDK和64位JDK命令选项也会有所不同。

2.标准VM选项

标准VM选项主要包括以下几项。

  • -client、-server:指定HotSpot以client或server模式运行虚拟机。对于64位JDK,将忽略此选项,默认以server模式运行虚拟机。
  • -agentlib:libname[=options]:按照库名libname载入本地代理库(agent library)。如-agentlib:hprof、-agentlib:jdwp=help、-agentlib:hprof=help。
  • -agentpath:pathname[=options]:按照完整路径名pathname载入本地代理库。
  • -classpath、-cp:指定类文件搜索路径。
  • -Dproperty=value:设置系统属性值
  • -jar:执行封装在jar文件中的应用程序。
  • -javaagent:jarpath[=options]:加载Java编程语言代理库,可参阅java.lang.instrument。
  • -verbose、-verbose:class:显示每个被加载的类信息。
  • -verbose:gc:报告每个垃圾回收事件。
  • -verbose:jni:报告关于调用本地方法和其他本地接口的信息。
  • -X:显示非标准选项信息,然后退出。

3.非标准VM选项

以“-X”指定的非标准VM选项主要包括以下几项1。

  • -Xint:以解释模式运行虚拟机。禁用编译本机代码,并由解释器(interpreter)执行所有字节码。
  • -Xbatch:禁用后台编译。一般来说,虚拟机将编译方法作为后台任务,虚拟机在解释器模式下运行某方法时,需要等到后台编译完成该方法的编译任务。该参数将禁用后台编译,使方法的编译作为前台任务直到完成为止。
  • -Xbootclasspath:指定引导类和资源文件的搜索路径。
  • -Xcheck: jni:对于Java 本地接口JNI函数执行额外的检查。JVM验证传递给 JNI 函数的参数。在本机代码中遇到任何无效的数据将导致JVM终止。使用此选项时,会带来一些性能损失。
  • -Xfuture:执行严格的类文件格式检查。
  • -Xnoclassgc:禁用类垃圾回收。
  • -Xincgc:启用增量垃圾回收器。
  • -Xloggc::报告垃圾回收事件,并记录到指定的文件中。
  • -Xms:设置Java堆的初始化大小。
  • -Xmxn:设置Java堆的最大值。
  • -Xssn:设置Java线程的栈大小。
  • -Xprof:输出CPU性能数据。

4.隐藏的非标VM选项

这一类选项以“-XX”指定。该类VM选项数量十分可观,可以说有成百上千个也不为过。本书将在各章节中,附上一些相关的虚拟机选项和功能描述,以供参考。

5.gamma:调试版启动器

HotSpot提供了一个精简调试Launcher,称为gamma。相对于通用Launcher,gamma就安装在与JVM库相同的目录下,或者与JVM库静态链接为一个库文件,因此可以把gamma看作是精简了虚拟机选项解析等逻辑的java命令。

事实上,为便于维护,OpenJDK就是基于同一套Launcher代码维护了gamma launcher和通用launcher的,对于差异代码则使用#ifndef GAMMA进行注释区分。gamma启动器入口位于hotspot/src/share/tools/luncher/java.c;通用Launcher的入口并不在hotspot工程下,感兴趣的读者可以在与hotspot同级目录jdk下找到hotspot/../jdk/src/share/bin/main.c。

从本节开始,我们将以Launcher作为切入点,对HotSpot进行实战调试和分析。为方便调试,我们将在Linux平台上基于gamma启动器来讲解HotSpot启动过程。

2.2.2 虚拟机生命周期

图2-6描述了一个完整的虚拟机生命周期,具体过程如下。

(1)Launcher启动后,首先进入Launcher的入口,即main函数。正像稍后看到的那样,main工作的重点是:创建一个运行环境,为接下来启动一个新的线程创建JVM并跳到Java主方法做好一切准备工作。

0ffa34f6d386655c0ef47c5931f6577563ff170f

(2)环境就绪后,Launcher启动JavaMain线程,将程序参数传递给它。如清单2-21所示,Launcher调用ContinueInNewThread()函数启动新的线程并继续执行任务。新的线程将要执行的任务由该函数的第一个参数指定,即JavaMain()函数。这时,新线程将要阻塞当前线程,并在新线程中开启一段相对独立的历程,去完成Launcher赋予它的使命。

清单2-21

来源:hotspot/src/share/tools/luncher/java.c

描述:Launcher启动JavaMain线程
return ContinueInNewThread(JavaMain, threadStackSize, (void*)&args);

(3)一般来说,JavaMain线程将伴随应用程序的整个生命周期。首先,它要做的便是在Launcher模块内调用InitializeJVM()函数,初始化JVM。值得一提的是,在理解虚拟机生命周期复杂的模块调用过程时,我们不能对Launcher模块本身抱有过高的期待。毕竟,Launcher模块本身无力实现这些核心功能,它必须借助其他专门模块来提供相应功能。因此,在阅读源代码时,我们应当培养这样的意识,在遇到某个核心功能或重要组件时,首先问自己几个问题:核心功能是由哪个模块提供的?它最终是为系统哪个组件提供服务的?它是以什么形式向调用者提供服务的?养成这种意识,对于独立分析和思考系统运作具有重要的意义。

Launcher模块本身并不具有创建虚拟机的能力。下面我们将看到,有哪些模块参与了这个过程。由于Launcher模块需要借助自身以外的力量完成任务,理所当然地,它需要拥有访问外部接口的能力。稍后将提到一些数据结构,它们持有外部接口的函数指针,Launcher通过它们可以达到调用外部接口的目的。

(4)虚拟机在Prims模块中定义了一些以“JNI_”为前缀而命名的函数,并向外部提供这些jni接口。JNI_CreateJavaVM()函数就是其中一个,它为外部程序提供创建JVM的服务。前面提到的创建JVM的任务,实际上就是调用了JNI_CreateJavaVM()函数。JNI模块是连接虚拟机内部与外部程序的桥梁,JVM系统内部的命名空间对JNI模块都是可见的,因此它可以调用内部模块并通过接口向外提供查看和操纵JVM的能力。JNI_CreateJavaVM()函数调用Threads模块create_vm()函数完成最终的虚拟机的创建和初始化工作。

(5)可以说,create_vm()函数是JVM启动过程的精华部分,它初始化了JVM系统中绝大多数的模块。

(6)调用add()函数,将线程加入线程队列。

(7)调用create()函数,创建虚拟机线程“VMThread”;

(8)调用vm_init_globals()函数,初始化全局数据结构;

(9)调用init_globals()函数,初始化全局模块;

(10)调用LoadClass()函数,加载应用程序主类;

(11)调用jni_CallStaticVoidMethod()函数,实现对Java应用程序的主方法的调用;

(12)调用jni_DetachCurrentThread()函数;

(13)调用jni_DestroyJavaVM()函数,销毁JVM后退出。

接下来,我们将选取一些重要过程展开详解。

2.2.3 入口:main函数

与其他应用程序一样,Launcher的入口是一个main函数。在不同操作系统中,main函数的原型看起来会有些差异。例如在UNIX或Linux系统中,按照POSIX规范的函数原型如清单2-22所示。

清单2-22

来源:hotspot/src/share/tools/luncher/java.c

描述:launcher入口
int
main(int argc, char ** argv)

而在Windows平台上,其原型如清单2-23所示。

清单2-23

来源:jdk/src/share/bin/main.c

描述:launcher入口
int WINAPI
WinMain(HINSTANCE inst, HINSTANCE previnst, LPSTR cmdline, int cmdshow)

main函数的程序流程如图2-7所示。

9cd49741f8027d819d1d3370bbec31c8a7ef29bc

在main函数执行的最后一步,程序将启动一个新的线程并将Java程序参数传递给它,接下来阻塞自己,并在新线程中继续执行。新线程也可称为为主线程,它的执行入口是JavaMain。

2.2.4 主线程

一般来说,主线程将伴随应用程序的整个生命周期。打个形象的比喻:JavaMain好比一个外壳,应用程序便是在这个外壳的包裹下完成执行的。它的函数原型如清单2-24(a)所示。

清单2-24(a)

来源:hotspot/src/share/tools/luncher/java.c

描述:启动新线程执行该方法
int JNICALL 
JavaMain(void * _args)```
在介绍JavaMain的主要流程前,我们先了解几个重要的基础数据结构,它们在调用主方法、断开主线程和销毁JVM的过程中发挥重要作用的数据结构。它们分别是JavaVM、JNIEnv和InvocationFunctions。

JavaVM类型是一个结构体,它拥有一组少而精的函数指针2。顾名思义,这几个函数为JVM提供了诸如连接线程、断开线程和销毁虚拟机等重要功能。在JavaMain的流程中,我们也可以看到这些功能的执行。HotSpot定义了大量的运行时接口,上述功能实际上是由这些接口提供的。如图2-8所示,在JavaMain运行时由InitializeJVM模块将JavaVM的这些成员赋上正确的值,指向相应的JNI接口函数上。

同JavaVM类型类似,JNIEnv也是拥有一组函数指针的结构体。不过,相对于JavaVM来说,它是一个重量级类型,JNIEnv容纳了大量的函数指针成员。同样地,在JavaMain运行时由InitializeJVM模块将JNIEnv的这些成员赋上正确的值,指向相应的JNI接口函数上。

InvocationFunctions中定义了2个函数指针,CreateJavaVM和GetDefaultJavaVMInitArgs,如图2-9所示,这2个函数在加载libjvm时中已经指派好了。

<div style="text-align: center"><img src="https://yqfile.alicdn.com/96605d8896aee78c16258ead7b0fad7f037b192f.png" width="" height="">
</div>

在JavaMain中,拥有3个局部变量:vm、env和ifn,分别对应着上述JavaVM、JNIEnv和InvocationFunctions这三种类型。JavaMain的主要流程如图2-10所示。

(1)初始化虚拟机:调用InitializeJVM模块,将JavaVM和JNIEnv类型的成员指向正确的jni函数上。

(2)获取应用程序主类(main class),如清单2-24(b)所示。

清单2-24(b)

jclass mainClass = LoadClass(env, classname);

(3)获取应用程序主方法(main method),如清单2-24(c)所示。

清单2-24(c)

jmethodID mainID = (*env)->GetStaticMethodID(env, mainClass, "main",

                                   "([Ljava/lang/String;]V"];
(4)传递应用程序参数并执行主方法,如清单2-24(d)所示。

清单2-24(d)

(*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);


<div style="text-align: center"><img src="https://yqfile.alicdn.com/f36bc6fc40177c9cfcdea3fa5183949542faf4ba.png" width="" height="">
</div>

(5)与主线程断开连接,如清单2-24(e)所示。

清单2-24(e)

(*vm)->DetachCurrentThread(vm);

(6)主方法执行完毕,等待非守护线程结束,然后创建一个名为“DestroyJavaVM”的Java线程执行销毁JVM任务,如清单2-24(f)所示。

清单2-24(f)

(*vm)->DestroyJavaVM(vm);

练习3

阅读源代码,认真分析JavaMain函数,体会它在JVM中的作用和地位。如果通过前面的学习,你已经掌握了调试的基本方法,请仔细调试这部分程序。
####2.2.5 InitializeJVM函数
在第1章中,我们知道,在编译HotSpot项目后,启动脚本hotspot中会默认设置一个断点,即InitializeJVM。启动GDB调试HotSpot,JVM开始运行HelloWorld程序,但是程序并不急于打印“Hello hotspot!”,而是先停在了断点“Breakpoint 1”上(如图1-6所示),即InitializeJVM函数。

现在,我们想将断点往前挪一点,以便于我们详细了解JavaMain的运行细节。利用GDB,我们将断点设置在JavaMain函数的入口处,GDB界面中输入如下命令:

(gdb)break java.c:JavaMain

或者直接使用代码行数:

(gdb)break java.c:396`
断点设置完毕后,我们再次启动调试,如图2-11所示。

7f2c1f083788b0196678d6a6648dad6823dbf6fb

HotSpot运行至第396行,停了下来。实际上,我们在这里共设置了2个断点(JavaMain和InitializeJVM)。输入continue命令让程序继续运行至第1270行,即InitializeJVM。

InitializeJVM的原型如清单2-25所示。

清单2-25

来源:hotspot/src/share/tools/launcher/java.c & jdk/src/share/bin/java.c

描述:InitializeJVM
static jboolean
InitializeJVM(JavaVM **pvm, JNIEnv **penv, InvocationFunctions *ifn)

数字1270的含义是下一条将要运行的语句行数,如图2-12所示,这行代码是一条memset语句,用来对main()函数的参数args进行初始化填零。

6b957499435ec6ebdd5d0b402306f3ba163a419d

利用GDB调试工具,我们还可以深入到InitializeJVM内部,看看InitializeJVM是如何初始化JVM的。由前文可以,InitializeJVM的任务之一就是需要完成对vm和env指派接口函数的重任。在调用InitializeJVM返回后,通过GDB查看命令,我们可以看到vm的函数指针成员得到了赋值,如图2-13所示。

此外,InitializeJVM中还会打印一些额外信息,如图2-12所示,可以看到InitializeJVM打印了一些与JVM的版本和选项相关的信息。

a03600a3c0d729d08c32f4d4bb3b93a76067f687

通过这些调试过程,相信读者对使用GDB调试HotSpot又有了新的认识。可是,到现在为止,我们仍然没有接触到与JVM的创建或初始化相关的实质内容,只是知道在调用CreateJavaVM之后,得到了大量的JNI函数。显然,这一过程向我们屏蔽了很多细节。但是换句话说,现在我们距离JVM初始化的核心内容仅一步之遥了。

在继续深入了解JVM的创建和初始化过程之前,我们希望你能够做些小的练习,以便巩固刚才学过的知识,同时为接下来的深入学习打下良好的实践基础。

练习4

将断点设置在JavaMain跟踪调试,在InitializeJVM返回之后,确认env成员已指派到了正确的jni接口函数上。

练习5

试一试在你安装的正式版JDK中,找到下面这些函数符号:

JNI_CreateJavaVM

JNI_GetDefaultJavaVMInitArgs
提示 在Windows上,可以使用DLL export Viewer等dll查看工具列出其中包含的符号;在UNIX上,可以通过nm等工具查看。

2.2.6 JNI_CreateJavaVM函数

创建JVM的程序模块是JNI_CreateJavaVM。JNI_CreateJavaVM主要任务是调用Threads模块的create_vm()函数,以完成最终的虚拟机创建和初始化工作。

在Threads模块中,实现了对虚拟机各个模块的初始化,以及创建虚拟机线程。这些被初始化的模块,在本书后续章节中均有大量涉及,因此理解这一过程对于其余章节的理解十分重要。为了保证知识的连贯性,避免打断对启动过程的叙述,我们将具体的初始过程安排在2.3小节中继续探讨。

注意 vm和env是在JNI_CreateJavaVM接口中实现赋值的。

此外,JNI_CreateJavaVM还将为vm和env分配JNI接口函数。

练习6:

设置断点并调试HotSpot,跟踪vm和env的赋值。

2.2.7 调用Java主方法

在JavaMain中,虚拟机得到初始化之后,接下来就将执行应用程序的主方法。通过env引用jni_CallStaticVoidMethod函数(原型如清单2-26所示),可以执行一个由“static”和“void”修饰的方法,即Java应用程序主类的main方法。

清单2-26

来源:hotspot/src/share/vm/prims/jni.h

描述:JNI函数:调用静态void方法

void 
CallStaticVoidMethod(jclass cls, jmethodID methodID, ...)

读到这里,细心的读者可能会想弄明白:由清单2-24(c)和清单2-26可知,主方法是根据JVM内部一个唯一的方法ID(即methodID)定位到的。那么,我们不禁想问,JVM是如何根据methodID定位到要执行的方法的?方法在JVM内部又是什么样的呢?如果你还没想过这个问题,那么请闭上眼睛,花上几分钟思考一下这个问题。

这里我们暂时不急着回答这个问题,通过本书后续章节对类的解析以及方法区等知识点的学习,这些疑惑就可以迎刃而解了。

为了执行主类的main方法,将在jni_invoke_static中通过调用JavaCalls模块完成最终的执行Java方法。在HotSpot中,所有对Java方法的调用都需要通过类JavaCalls来完成。

清单2-27

来源:hotspot/src/share/vm/prims/jni.cpp

描述:JNI函数:jni_invoke_static

methodHandle method(THREAD, JNIHandles::resolve_jmethod_id(method_id));
JavaCalls::call(result, method, &java_args, CHECK);

清单2-27中是这部分逻辑的实现:首先根据method_id转换成方法句柄,然后调用JavaCalls模块方法实现从JVM对Java方法的调用。

2.2.8 JVM退出路径

前面讲述了JVM启动的过程,这里介绍JVM退出的过程。一般来说,JVM有两条退出路径。其中一条路径称为虚拟机销毁(destroy vm):当程序运行到主方法的结尾处,系统将调用jni_DestroyJavaVM()函数销毁虚拟机。而另外一条路径则是虚拟机退出(vm exit):当程序调用System.exit()函数,或当JVM遇到错误时,将通过这条路径直接退出。

这两条退出途径并不完全相同,但它们在Java层共享Shutdown.shutdown()和before_exit()函数,并在JVM层共享VM_Exit函数。

这里,介绍一下destroy_vm的退出流程。

  • 当前线程等待直到成为最后一条非守护线程。此时,所有工作仍在继续。
  • Java层调用java.lang.Shutdown.shutdown()函数。
  • 调用before_exit()函数,为JVM退出做一些准备工作:首先,运行JVM层的关闭钩子函数(shutdown hooks)。这些钩子函数是通过JVM_OnExit进行注册的。目前唯一使用了这套机制的钩子函数是File.deleteOnExit()函数;其次,停止一些系统线程,如“StatSampler”,“watcher thread”和“CMS threads”等,并向JVMTI发送“thread end”和“vm death”事件;最后,停止信号线程。
  • 调用JavaThread::exit()函数,这将释放JNI句柄块,并从线程列表中移除本线程。
  • 停止虚拟机线程,使虚拟机进入安全点(safepoint)并停止编译器线程。
  • 禁用JNI/JVM 跟踪。
  • 为那些仍在运行本地代码的线程设置“_vm_exited”标记。
  • 删除当前线程。
  • 调用exit_globals()函数,删除tty和PerfMemory等资源。
  • 返回到上层调用者。

到目前为止,我们对启动过程已经有了较为整体的认识。接下来,在2.3小节中,我们将深入了解系统初始化过程。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
阿里云服务器怎么设置密码?怎么停机?怎么重启服务器?
如果在创建实例时没有设置密码,或者密码丢失,您可以在控制台上重新设置实例的登录密码。本文仅描述如何在 ECS 管理控制台上修改实例登录密码。
10013 0
《HotSpot实战》—— 2.3 系统初始化
前面提到,系统初始化过程是JVM启动过程中的重要组成部分。初始化过程涉及到绝大多数的HotSpot内核模块,因此,了解这个过程对于理解HotSpot整体架构具有重要意义。图2-14描述了系统初始化的完整过程。
3037 0
《HotSpot实战》—— 2.2 启动
通用启动器(Generic Launcher)是指我们比较熟悉的JDK命令程序:java(含javaw)。java是由JDK自带的启动Java应用程序的工具。为启动一个Java应用程序,java将准备一个Java运行时环境(即JRE)、加载指定的类并调用它的main方法。
2281 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,阿里云优惠总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系.
13812 0
《HotSpot实战》—— 1.2 动手编译虚拟机
由于开发环境各不相同,每个人遇到的问题可能都不尽相同;即使遇到相同的问题,在不同的平台上解决的方式可能也有所不同。当然,对于相同的问题,也会有多种办法解决。限于篇幅,在这里不能对所有错误信息和解决办法都列举出来。
4537 0
阿里云ECS云服务器初始化设置教程方法
阿里云ECS云服务器初始化是指将云服务器系统恢复到最初状态的过程,阿里云的服务器初始化是通过更换系统盘来实现的,是免费的,阿里云百科网分享服务器初始化教程: 服务器初始化教程方法 本文的服务器初始化是指将ECS云服务器系统恢复到最初状态,服务器中的数据也会被清空,所以初始化之前一定要先备份好。
11879 0
Android源码剖析之Framework层实战版(Ams管理Activity启动)
  本文来自http://blog.csdn.net/liuxian13183/ ,引用必须注明出处! 讲到实战,就不得不拿两个例子来说明,本篇想拿的是应用最广泛的两个:Ams和Wms,一个管理activity,一个管理窗口,而前面我们已经讲了不少,本篇不再赘述。
1070 0
阿里云ECS云服务器初始化设置教程方法
阿里云ECS云服务器初始化是指将云服务器系统恢复到最初状态的过程,阿里云的服务器初始化是通过更换系统盘来实现的,是免费的,阿里云百科网分享服务器初始化教程: 服务器初始化教程方法 本文的服务器初始化是指将ECS云服务器系统恢复到最初状态,服务器中的数据也会被清空,所以初始化之前一定要先备份好。
7336 0
+关注
异步社区
异步社区(www.epubit.com)是人民邮电出版社旗下IT专业图书旗舰社区,也是国内领先的IT专业图书社区,致力于优质学习内容的出版和分享,实现了纸书电子书的同步上架,于2015年8月上线运营。公众号【异步图书】,每日赠送异步新书。
12049
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
《2021云上架构与运维峰会演讲合集》
立即下载
《零基础CSS入门教程》
立即下载
《零基础HTML入门教程》
立即下载