本节书摘来异步社区《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模块;

a2382a8683c25e33df3a6e8f82bd3c2b773fe752

(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中的相关定义。

0997c1568496d242e6b79c0925ce9e52f525ac4b

练习7

阅读源代码并调试跟踪,了解信号初始化过程,并思考HotSpot中初始化的信号对系统的意义。

(提示:见SR_initialize())

练习8

阅读源代码并调试跟踪,了解线程优先级策略的初始化过程。

(提示:见prio_init())。

2.3.2 配置系统属性

配置虚拟机运行时的系统属性。首先介绍的是关于Launcher的属性,包括:“-Dsun.java.launcher”和“-Dsun.java.launcher.pid”。

接下来,要介绍的是一些与操作系统相关的系统属性,如表2-3所示。

66bbcc9742e43c446601484257f7d15073d5af1f
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所示。

1b9d9985a8022de27eb6161d4d625072120e63f0

系统初始化时,虚拟机首先创建的线程就是主线程。具体的创建过程如清单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所示。

cfd2d078ceb62a87fd5537bfa1bca3090ec09a2a
  • 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等模块。这些模块的作用和实现,在本书后续章节将会陆续看到。

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐