zl程序教程

您现在的位置是:首页 >  系统

当前栏目

《HotSpot实战》—— 2.3 系统初始化

系统 实战 初始化 2.3 HotSpot
2023-09-11 14:17:37 时间

本节书摘来异步社区《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; // dont let caller call JNI_CreateJavaVM again

10 return JNI_ENOMEM;

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

10 MutexLocker ml(Notify_lock);

11 os::start_thread(vmthread);

12 while (vmthread- active_handles() == NULL) {

13 Notify_lock- wait();

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;

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 were starting with a clean slate

37 if (PrintFlagsFinal) {

38 CommandLineFlags::printFlags();

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


JVM--JVM回收机制图解整理 - 新生的对象直接分配到- 新生代(Eden) - S0是我们的Eden区出现无法存储某些对象的时候或者存储满了只有,整理Eden区就会存放到S0 - S1和S0的作用是一样的,但是他是针对S0的 - 当我们对象经历过15次GC之后,他就会被移入老年区(Old)
如何找到native方法对应的Hotspot源码 哈喽,我是子牙。十余年技术生涯,一路披荆斩棘从技术小白到技术总监到JVM专家到创业。技术栈如汇编、C语言、C++、Windows内核、Linux内核。特别喜欢研究虚拟机底层实现,对JVM有深入研究。分享的文章偏硬核,很硬的那种。
异步社区 异步社区(www.epubit.com)是人民邮电出版社旗下IT专业图书旗舰社区,也是国内领先的IT专业图书社区,致力于优质学习内容的出版和分享,实现了纸书电子书的同步上架,于2015年8月上线运营。公众号【异步图书】,每日赠送异步新书。