本节书摘来异步社区《HotSpot实战》一书中的第2章,第2.3节,作者:陈涛,更多章节内容可以访问云栖社区“异步社区”公众号查看。
2.3 系统初始化
前面提到,系统初始化过程是JVM启动过程中的重要组成部分。初始化过程涉及到绝大多数的HotSpot内核模块,因此,了解这个过程对于理解HotSpot整体架构具有重要意义。图2-14描述了系统初始化的完整过程。
系统初始化的具体步骤如下所示。
(1)初始化输出流模块;
(2)配置Launcher属性
(3)初始化OS模块;
(4)配置系统属性;
(5)程序参数和虚拟机选项解析;
(6)根据传入的参数继续初始化操作系统模块;
(7)配置GC日志输出流模块;
(8)加载代理(agent)库;
(9)初始化线程队列;
(10)初始化TLS模块;
(11)调用vm_init_globals()函数初始化全局数据结构;
(12)创建主线程并加入线程队列;
(13)创建虚拟机线程;
(14)初始化JDK核心类,如java.lang.String、java.util.HashMap、java.lang.System、java.lang.ThreadGroup、java.lang.Thread、java.lang.OutOfMemoryError等;
(15)初始化系统类加载器模块,并初始化系统字典;
(16)启动SLT线程,即“SurrogateLockerThread”线程;
(17)启动“Signal Dispatcher”线程;
(18)启动“Attach Listener”线程;
(19)初始化即时编译器;
(20)初始化Chunk模块;
(21)初始化Management模块,启动“Service Thread”线程;
(22)启动“Watcher Thread”线程。
接下来,我们将对其中几个重要的步骤做详细的讲解。
2.3.1 配置OS模块
OS模块的初始化包括两个环节。
- init()函数:第一次初始化的时机是在TLS前,全局参数传入之前。
- init_2()函数:第二次初始化的实际是在args解析后,全局参数传入之后。
1.init()函数
第一次初始化能够完成一些固定的配置,主要包括以下几项内容。
- 设置页大小。
- 设置处理器数量。
- 初始化proc,打开“/proc/$pid”。
- 设置获得物理内存大小,保存在全局变量os::Linux::_physical_memory中;
- 获得原生主线程的句柄:获得指向原生线程的指针,并将其保存在全局变量os::Linux::_main_thread中。
- 系统时钟初始化:选用CLOCK_MONOTONIC类型时钟。从动态链接库“librt.so.1”或“librt.so”中将时钟函数clock_gettime装载进来,并保存在全局变量os::Linux::_clock_gettime中。
注意 Linux的时钟与计时器。CLOCK_MONOTONIC提供相对时间,它的时间值是通过jiffies值来计算的,jiffies取决于系统的频率,单位是Hz,是周期的倒数,一般表示为一秒钟中断产生的次数。该时钟不受系统时钟源的影响,较为稳定,只受jiffies值的影响。按照POSIX规范,与CLOCK_MONOTONIC相对的另外一种时钟类型是CLOCK_REALTIME,这是系统实时时钟(RTC)。这是一个硬件时钟,用来持久存放系统时间,系统关闭后靠主板上的微型电池保持计时。系统启动时,内核通过读取RTC来初始化Wall Time,并存放在xtime变量中,即xtime是从cmos电路中取得的时间,一般是从某一历史时刻开始到现在的时间,也就是为了取得我们操作系统上显示的日期,它的精度是微秒。
2.init_2()函数
OS模块还有一部分配置是允许外部参数进行控制的。当解析完全局参数后,就可以根据配置参数进行配置,具体包括以下几项内容。
- 快速线程时钟初始化。
- 使用mmap分配共享内存,配置大页内存。
- 初始化内核信号,安装信号处理函数SR_handler,用作线程执行过程中的Suspended/ Resumed处理。操作系统信号(signal),作为进程间通信的一种手段,用来通知进程发生了某种类型的系统事件。
- 配置线程栈:设置栈大小、分配线程初始栈等。
- 设置文件描述符数量。
- 初始化时钟,用来串行化线程创建。
- 若开启VM选项“PerfAllowAtExitRegistration”,则向系统注册atexit函数。
- 初始化线程优先级策略。
- OS模块初始化相关的VM配置、调试选项如表2-2所示,其中“Build”表示VM选项作用的Build版本,具体版本含义可参考globals.hpp中的相关定义。
练习7
阅读源代码并调试跟踪,了解信号初始化过程,并思考HotSpot中初始化的信号对系统的意义。
(提示:见SR_initialize())
练习8
阅读源代码并调试跟踪,了解线程优先级策略的初始化过程。
(提示:见prio_init())。
2.3.2 配置系统属性
配置虚拟机运行时的系统属性。首先介绍的是关于Launcher的属性,包括:“-Dsun.java.launcher”和“-Dsun.java.launcher.pid”。
接下来,要介绍的是一些与操作系统相关的系统属性,如表2-3所示。
2.3.3 加载系统库
在对虚拟机配置选项进行解析的阶段,Arguments模块根据虚拟机选项-agentlib或-agentpath,将需要加载的本地代理库逐一加入到代理库列表(AgentLibraryList)中。在加载代理库阶段,虚拟机将按照代理库列表中的库名,根据操作系统的库搜索规则,利用OS模块查找库并加载到虚拟机进程地址空间中。例如,若按照命令“java -agentlib:hprof”来启动应用程序的话,将加载JDK中代理库hprof,它的库文件名为libhprof.so或hprof.dll。
加载库操作需要在Java线程创建前完成,这样才能保证在Java线程需要调用时能够正确地找到本地库函数。
通过JVMTI接口,允许程序员开发自定义代理库,并通过选项-agentlib或–agentpath加载到虚拟机中。必须注意的是,由于agent代码将在虚拟机进程空间中运行,因此你的agent代码需要保证以下几点:多线程安全、可重入性、避免内存泄露或空指针、符合JVMTI和JNI规则等。如果你不小心触犯了这些准则,那么很有可能导致“out of memory”错误或虚拟机崩溃(JVM Crash,见第4章),这就是为什么我们在做JVM Crash分析时,需要考虑系统库或自定义库bug因素的原因。
除了JDK中代理库和自定义代理库,虚拟机还将加载本地库,如libc或ld库。为应用程序定位本地库可以通过两种方式:将库复制到应用程序的共享库路径下;或按照特定操作系统平台指定规则加载,如Solaris/Linux平台上根据环境变量LD_LIBRARY_PATH,而在Windows平台上根据环境变量PATH来定位本地库。
在系统初始化过程中,当代理库被加载进虚拟机进程后,虚拟机将在库中查找函数符号JVM_OnLoad或Agent_Onload并调用该函数,实现代理库与虚拟机的连接。
2.3.4 启动线程
- 1.线程状态和类型
在JDK中定义了6种线程状态。 - NEW:新创建但尚未启动的线程处于这种状态。通过new关键字创建了java.lang.Thread类(或其子类)的对象。
- BLOCKED:线程受阻塞并等待某个监视器对象锁。当线程执行synchronized方法或代码块,但未获得相应对象锁时处于这种状态。
- RUNNABLE:正在 Java 虚拟机中执行的线程处于这种状态。有三种情形,一种情形是Thread类的对象调用了start()函数,这时的线程就等待时间片轮转到自己,以便获得CPU;另一种情形是线程在处于RUNNABLE状态时并没有运行完自己的run()函数,时间片用完之后回到RUNNABLE状态;还有一种情形就是处于BLOCKED状态的线程结束了当前的BLOCKED状态之后重新回到RUNNABLE状态。
- TERMINATED:已退出的线程处于这种状态。
- TIMED_WAITING:等待另一个线程来执行取决于指定等待时间的操作。
- WAITING:无限期地等待另一个线程来执行某一特定操作。
- 在JVM层面,HotSpot内部定义了线程的5种基本状态。
- _thread_new,表示刚启动,正处在初始化过程中。
- _thread_in_native,表示运行本地代码。
- _thread_in_vm,表示在VM中运行。
- _thread_in_Java,表示运行Java代码。
- _thread_blocked,表示阻塞。
为了支持内部状态转换,还补充定义了其他几种过渡状态:__trans,其中thread_state_type分别表示上述5种基本状态类型。
在HotSpot中,定义了如清单2-28所示的几种线程类型,其类图如2-15所示。
清单2-28
来源:hotspot/src/share/vm/runtime/os.hpp
描述:线程类型
1 enum ThreadType {
2 vm_thread, // VM线程
3 cgc_thread, // 并发GC线程
4 pgc_thread, // 并行GC线程
5 java_thread, // Java线程
6 compiler_thread, // 编译器线程
7 watcher_thread, // watcher线程
8 os_thread // OS线程
9 };
图2-15 线程类型
2.创建主线程
主线程(main thread)是执行应用程序的“public static void main (String[] args)”方法的线程。对应OS线程ID为1的即为名为“main”为主线程,如图2-16所示。
系统初始化时,虚拟机首先创建的线程就是主线程。具体的创建过程如清单2-29所示。
清单2-29
来源:hotspot/src/share/vm/runtime/thread.cpp - Threads::creat_vm()
描述:创建main thread
1 JavaThread* main_thread = new JavaThread();
2 main_thread->set_thread_state(_thread_in_vm);
3 main_thread->record_stack_base_and_size();
4 main_thread->initialize_thread_local_storage();
5 main_thread->set_active_handles(JNIHandleBlock::allocate_block());
6 if (!main_thread->set_as_starting_thread()) {
7 vm_shutdown_during_initialization("Failed necessary internal allocation. Out of swap space");
8 delete main_thread;
9 *canTryAgain = false; // don't let caller call JNI_CreateJavaVM again
10 return JNI_ENOMEM;
11 }
12 main_thread->create_stack_guard_pages();
首先,第1行代码中,JVM创建一个JavaThread类型的线程变量(刚创建时状态为_thread_new)。紧接着,第2行将线程状态设置为_thread_in_vm,表明该线程正处于在JVM中执行的状态。接下来,第3行记录线程栈的基址和大小;第4行,初始化线程本地存储区(TLS);第5行为线程设置JNI句柄;在第6~11行中,将通过OS模块创建原始线程,即OS主线程,并设置为可运行状态。接下来,在第12行中初始化主线程栈。
现在,main_thread实际上是一个JVM内部线程,其状态为JVM内部定义的线程状态_thread_in_vm。接下来需要创建java.lang.Thread线程:
13 initialize_class(vmSymbols::java_lang_System(), CHECK_0);
14 initialize_class(vmSymbols::java_lang_ThreadGroup(), CHECK_0);
15 Handle thread_group = create_initial_thread_group(CHECK_0);
16 Universe::set_main_thread_group(thread_group());
17 initialize_class(vmSymbols::java_lang_Thread(), CHECK_0);
18 oop thread_object = create_initial_thread(thread_group, main_thread, CHECK_0);
19 main_thread->set_threadObj(thread_object);
20 java_lang_Thread::set_thread_status(thread_object, java_lang_Thread::RUNNABLE);
当Java层main线程创建完成后,就将其状态设置为RUNNABLE,开始运行,这样,一个Java主线程就开始运行了。
3.创建VMThread
VMThread是在JVM内部执行VMOperation的线程。VMOperation实现了JVM内部的核心操作,为其他运行时模块以及外部程序接口服务,在HotSpot中占有重要地位。
清单2-30描述了VMThread线程的创建过程。当VMThread线程创建成功后,在整个运行期间不断等待、接受并执行指定的VMOperation。
清单2-30
来源:hotspot/src/share/vm/runtime/thread.cpp - Threads::creat_vm()
描述:创建VMThread
1 // Create the VMThread
2 { TraceTime timer("Start VMThread", TraceStartupTime);
3 VMThread::create();
4 Thread* vmthread = VMThread::vm_thread();
5 if (!os::create_thread(vmthread, os::vm_thread))
6 vm_exit_during_initialization("Cannot create VM thread. Out of system resources.");
7 // Wait for the VM thread to become ready, and VMThread::run to initialize
8 // Monitors can have spurious returns, must always check another state flag
9 {
10 MutexLocker ml(Notify_lock);
11 os::start_thread(vmthread);
12 while (vmthread->active_handles() == NULL) {
13 Notify_lock->wait();
14 }
15 }
16 }
4.创建守护线程
守护线程包括“Signal Dispatcher”(该线程需要在“VMInit”事件发生前启动,详见os::signal_init()函数)、“Attach Listener”、“Watcher Thread”等。
2.3.5 vm_init_globals函数:初始化全局数据结构
在清单2-31中,vm_init_globals()函数实现了对全局性数据结构的初始化。
清单2-31
来源:hotspot/src/share/vm/runtime/initr.cpp
描述:初始化全局数据结构
1 void vm_init_globals() {
2 check_ThreadShadow();
3 basic_types_init();
4 eventlog_init();
5 mutex_init();
6 chunkpool_init();
7 perfMemory_init();
8 }
初始化的过程包括以下几个环节。
- 初始化Java基本类型系统。
- 分配全局事件缓存区,初始化事件队列。
- 初始化全局锁,如iCMS_lock、FullGCCount_lock、CMark_lock、SystemDictionary_lock、SymbolTable_lock等,表2-1列举了在一些主要模块中会涉及的锁,其中有部分所可以由VM选项UseConcMarkSweepGC和UseG1GC控制是否开启。
- 初始化ChunkPool,ChunkPool包括3个静态pool链表:_large_pool、_medium_pool和_small_pool。其实,这是HotSpot实现的内存池:系统全局中不会执行malloc/free操作,这样就能够有效避免malloc/free的抖动影响。内存池是系统设计的常用手段。
- 初始化JVM性能统计数据(Perf Data)区,可由VM选项UsePerfData控制是否开启。若开启VM选项PerfTraceMemOps,可在初始化时打印该空间的分配信息。
2.3.6 init_globals函数:初始化全局模块
如清单2-32所示,init_globals()函数实现了对全局模块的初始化。
清单2-32
来源:hotspot/src/share/vm/runtime/init.cpp
描述:初始化全局数据结构
1 jint init_globals() {
2 HandleMark hm;
3 management_init();
4 bytecodes_init();
5 classLoader_init();
6 codeCache_init();
7 VM_Version_init();
8 stubRoutines_init1();
9 jint status = universe_init(); // dependent on codeCache_init and stubRoutines_init
10 if (status != JNI_OK)
11 return status;
12 interpreter_init(); // before any methods loaded
13 invocationCounter_init(); // before any methods loaded
14 marksweep_init();
15 accessFlags_init();
16 templateTable_init();
17 InterfaceSupport_init();
18 SharedRuntime::generate_stubs();
19 universe2_init(); // dependent on codeCache_init and stubRoutines_init
20 referenceProcessor_init();
21 jni_handles_init();
22 vtableStubs_init();
23 InlineCacheBuffer_init();
24 compilerOracle_init();
25 compilationPolicy_init();
26 VMRegImpl::set_regName();
27 if (!universe_post_init()) {
28 return JNI_ERR;
29 }
30 javaClasses_init(); // must happen after vtable initialization
31 stubRoutines_init2(); // note: StubRoutines need 2-phase init
32 if (VerifyBeforeGC && !UseTLAB &&
33 Universe::heap()->total_collections() >= VerifyGCStartAt) {
34 Universe::heap()->prepare_for_verify();
35 Universe::verify(); // make sure we're starting with a clean slate
36 }
37 if (PrintFlagsFinal) {
38 CommandLineFlags::printFlags();
39 }
40 return JNI_OK;
41 }
这些模块构成了HotSpot整体功能的基础,也是本书后续章节所要探讨的核心内容。接下来,我们将对其中几个较为关键的内核模块做一个概念性的了解。
1.JMX:Management模块
在HotSpot工程结构中,我们了解到Services模块为JVM提供JMX等功能,JMX功能又可划分为如下4个主要模块,如图2-17所示。
- Management模块:启动名为“Service Thread”的守护线程(如清单2-33所示),注意在较早的版本中该守护线程名为“Low Memory Detector”。若系统开启了选项-XX:ManagementServer,则加载并创建sun.management.Agent类,执行其startAgent()方法启动JMX Server。
- RuntimeService模块:提供运行时模块的性能监控和管理服务,如applicationTime、jvmCapabilities等。
- ThreadService模块:提供线程和内部同步系统的性能监控和管理服务,包括维护线程列表、线程相关的性能统计、线程快照、线程堆栈跟踪和线程转储等功能。
- ClassLoadingService:提供类加载模块的性能监控和管理服务。
清单2-33
"Service Thread" daemon prio=6 tid=0x000000000b062000 nid=0x7274 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
在JVM初始化时,会相继对这4个模块进行初始化,如清单2-34所示。
清单2-34
来源:hotspot/src/share/vm/runtime/init.cpp
描述:初始化Management模块
1 void management_init() {
2 Management::init();
3 ThreadService::init();
4 RuntimeService::init();
5 ClassLoadingService::init();
6 }
2.Code Cache
Code Cache是指代码高速缓存,主要用来生成和存储本地代码。这些代码片段包括已编译好的Java方法和RuntimeStubs等。
通过VM选项CodeCacheExpansionSize、InitialCodeCacheSize和ReservedCodeCacheSize可以配置该空间大小。
此外,若在Windows 64位平台上开启SHE机制1(即通过VM选项UseVectoredExceptions关闭Vectored Exceptions机制2,默认关闭),则需要向OS模块注册SHE。
3.StubRoutines
StubRoutines位于运行时模块。该模块的初始化分为两个阶段,第一阶段初始化(stubRoutines_init1),将创建一个名为“StubRoutines (1)”的BufferBlob,并未其分配CodeBuffer存储空间,并初始化StubRoutines。在第二阶段(stubRoutines_init2)中,创建名为“StubRoutines (2)”的BufferBlob,并为其分配CodeBuffer存储空间。并生成所有stubs并初始化entry points。
4.Universe
Universe模块将按照两个阶段进行初始化。第一阶段,根据VM选项配置的GC策略及算法,选择垃圾收集器和堆的种类,初始化堆。根据VM选项UseCompressedOops进行相关配置。若VM选项UseTLAB开启TLAB,则初始化TLAB缓存区。第二阶段,将对共享空间进行配置以及初始化vmSymbols和SystemDictionary等全局数据结构。
5.解释器
位于解释器模块。初始化解释器(interpreter),并注册StubQueue。可开启VM选项TraceBytecodes跟踪。
6.模板表
同样位于解释器模块。初始化模板表模块,将创建模版解释器使用的模板表,更多解释器内容请参考本书第7章。
7.stubs
位于运行时模块。在系统启动时,创建供各个运行时组件共享的stubs模块,诸如“wrong_method_stub”、“ic_miss_stub”、“resolve_opt_virtual_call”、“resolve_virtual_call”、“resolve_static_call”等。
除了上述模块,init_globals还将对下面这些模块进行初始化:字节码模块Bytecodes、类加载器模块ClassLoader、虚拟机版本模块,以及ReferenceProcessor、JNIHandles、VtableStubs、InlineCacheBuffer、VMRegImpl、JavaClasses等模块。这些模块的作用和实现,在本书后续章节将会陆续看到。