本文简单介绍HotSpot虚拟机运行时子系统,内容来自不一样的版本,所以可能会与最新版本之间(当前为JDK12)存在一些偏差。java
HotSpot虚拟机中有大量的可影响性能的命令行属性,可根据他们的消费者进行简单分类:执行器消费(如-server -client选项),执行器处理并传递给JVM,直接由JVM消费(大多)。
这些选项可分为三个主要的类别:标准选项,非标准选项,开发者选项。标准选项是指全部的JVM不一样实现都可以处理且在不一样版本之间稳定可用的选项(可是也能够deprecated)。
以-X开头的选项是非标准选项(不保证全部JVM虚拟机的实现均支持),后续的JAVA SDK更新也不保证会对它进行通知。使用-XX开头的为开发者选项,它通常须要特定的系统环境(以保证明现正确的操做)和足量的权限(以访问系统配置参数),这些实现应当慎重使用,相应的选项在更新后也并不保证通知到用户。
命令行参数控制了JVM内部变量的属性值,这些参数同时具有"类型"与"值"。对于布尔型的属性值,以+或-置于参数以前,可分别表示该属性的值为true或false。对于须要其余数据的变量,一样有许多机制能够设置其值数据(很遗憾并不统一),一部分参数在格式上要求在属性名称后直接跟随属性值,有些不须要分隔符,有些又不得不加上分隔符,分隔符又多是":","="等,如-XX:+OptionName, -XX:-OptionName, and -XX:OptionName=。
大多整型的参数(如内存大小)可接受'k','m','g'分别表明kb,mb,gb这种简写形式。c++
执行器
HotSpot虚拟机有几种Java标准版的执行器,在unix系统上即java命令,在windows系统下即java和javaw(javaw实际上是指基于网络的执行器)。从属于虚拟机启动过程的执行器操做有:
a.解析命令行选项,部分选项直接由执行器自身消费,如-client和-sever属性被用来决断加载合适的vm库,其余的属性则做为虚拟机初始化参数(JavaVMInitArgs)传递给vm。
b.若是未明确指定选项,执行器来肯定堆的大小和编译器类型(是client仍是server)。
c.确立如LD_LIBRARY_PATH 和 CLASSPATH等环境变量。
d.若是未在命令行中明确指定主类,执行器会从jar文件清单中找出主类名称。
e.执行器会在一个新建立的线程(非原生线程)中使用JNI_CreateJavaVM来建立虚拟机实例。 注意,在原生线程中建立vm会极大的减小定制vm的可能性,如windows中的栈大小等。
f.一旦vm建立并初始化成功,加载主类成功,执行器可从主类中获得main方法的属性,而后使用CallStaticVoidMethod执行主方法并以命令行参数为它的方法入参。
g.当java主方法执行完成时,检查和清理任何可能已发生的挂起的异常,返回退出状态。它会使用ExceptionOccurred来清理异常,方法若是执行成功,它会给调用进程返回一个0值,不然为其余值。
h.使用DetachCurrentThread解除主线程的关联,这样减小了线程的数量,保证可安全调用DestroyJavaVM,它也能保证线程不在vm中执行操做,栈中再也不有存活的栈桢。程序员
最重要的两个阶段是JNI_CreateJavaVM以及DestroyJavaVM,下面详述。
JNI_CreateJavaVM执行步骤:
首先保证没有两个线程同时调用此方法,从而保证不在同一进程中出现两个vm实例。当在一个进程空间中达到一个初始化点时,该进程空间中不能再建立vm,该点也被称为“不返回的点”(point of no return)。缘由在于此时vm已建立的静态数据结构不可以从新初始化。
接下来,要对JNI的版本支持进行检查,检测gc日志的ostream是否初始化。此时会初始化一些操做系统模块,如随机数生成器,当前进程号,高分辨率的时间,内存页大小和保护页等。
解析传入的参数和属性值,并存放留用。初始化java标准系统属性。
基于上一步解析的参数和属性进一步建立和初始化系统模块,这一次的初始化是为同步,栈内存,安全点页作准备。在此时如libzip,libhpi,libjava,libthread等库也完成了加载,同时完成信号句柄的初始化和设定,并初始化线程库。
接下来初始化输出流日志,任何须需的代理库(hprof jdi等)均于此时完成初始化和开启。
完成线程状态的初始化以及持有了线程操做所需的指定的数据的线程本地存储(TLS Thread Local Storage)的初始化。
全局数据的初始化,如事件日志,操做系统同步,性能内存(perfMemory),内存分配器(chunkPool)等。
到此时开始建立线程,会建立java版本的主线程并绑定到一个当前操做系统线程上。然而这个线程还不能被Threads线程列表感知,完成java级别的线程初始化和启用。
紧接着进行余下部分的全局模块的初始化,它们包括启动类加载器(BootClassLoader),代码缓存(CodeCache),解释器(Interpreter),编译器(Compiler),JNI,系统字典(SystemDictionary),Universe。此时便已到达前述的“不返回的点”,也就是说,咱们此时已不能在进程的地址空间中再建立一个vm实例了。
主线程会在此时被加入到线程列表中,这一步首先要对Thread_Lock进行加锁操做。此处会对Universe(戏称为小宇宙,即所需的全局数据结构)进行健全检查。此时建立执行全部重要vm函数的VMThread,建立完成后,即达到一个合适的点,在这个点,可发出适当的JVMTI事件通知当前jvm的状态。
加载并初始化一些类,包含java.lang.String,java.lang.System,java.lang.Thread,java.lang.ThreadGroup,java.lang.reflect.Method,java.lang.ref.Finalizer,java.lang.Class,以及系统类中的其余成员。在这一刻,vm已经完成初始化而且可操做,可是并未具有完整的功能。
到这一步,信号处理器线程也被开启,同时也完成了启动编译器线程和CompileBroker线程,以及StatSampler和WatcherThreads等辅助线程,此时vm具有了完整的功能,生成JNIEnv信息并返回给调用者,此时的vm已经准备就绪,可服务新的JNI请求。算法
DestroyJavaVM执行步骤segmentfault
DestroyJavaVM的调用有两种状况:执行器调用它拆解vm,或vm自身在出现严重错误时调用。拆解虚拟机的基本步聚以下:
首先,要等待到自身成为惟一一个正在运行的非守护线程时,在整个等待过程当中,虚拟机仍旧是可工做的。
调用java.lang.Shutdown.shutdown()方法,它会执行java级别的关闭勾子方法,若是有退出终结器可用,运行相应的终结器(finalizer)。
调用before_exit()为vm退出作出准备,运行vm级别的关闭勾子(它们是用JVM_OnExit()注册的),中止剖析器(Profiler),采样器(StatSampler),Watcher和GC线程。将相应的事件发送给JVMTI/PI,禁用JVMPI,并终止信号线程。
调用JavaThread的exit方法,释放JNI句柄块,移除栈保护页,把此线程从线程列表中移除,从这个点起,任何java代码不可被执行。
终止vm线程,它会把当前的vm带到安全点并终止编译器线程。在安全点,应注意任何可能会在安全点阻塞的功能都不可以使用。
禁用JNI/JVM/JVMPI屏障的追踪。
给native代码中依旧在运行的线程设置_vm_existed标记。
删除这个线程。
调用exit_globals删除IO和PerfMemory资源。
返回调用者。windows
虚拟机要负责常量池符号的解析,它须要对有关的类和接口前后进行装载(loading),连接(linking)而后初始化。通常用“类加载机制”来描述把一个类或接口的名称映射到一个class对象的过程,相应的,JVMS定义了详细的装载,连接和初始化阶段的协议。
类的加载是在字节码解析过程当中完成的,典型是当一个类文件中的常量池符号须要被解析时。有一些JAVA的api会触发这个过程,如Class.forName(),classLoader.loadClass(),反射api,以及JNI_FindClass均能初始化类的加载。虚拟机自身也能初始化类加载。虚拟机会在启动时加载如Object,Thread等核心类。装载一个类须要装载全部的超类和超接口。且对于连接阶段的类文件验证过程,可能须要装载额外的类。
虚拟机和JAVA SE类加载库共同承担了类的加载,虚拟机执行了常量池的解析,类和接口的连接和初始化。加载阶段是vm和特定的类加载器(java.lang.ClassLoader)之间的一个协做过程。api
类加载阶段数组
装载阶段,根据类或接口的名称,在类文件中找出二进制语义,定义类并建立java.lang.Class对象。若是在类文件中找不到二进制表示,则抛出NoClassDefFound错误。此外装载阶段也作了一些类文件的在语法上的格式检查,检查不经过会抛出ClassFormatError或UnsupportedClassVersionError。在完成装载以前,vm必须载入全部的超类和超接口。若是类继承树存在问题,如类直接或间接地本身继承或实现本身,则vm会抛出ClassCircularityError。若vm发现类的直接接口不是接口,或者直接父类是一个接口,则会抛出IncompatibleClassChangeError。
类加载的连接阶段首先作一些校验,它会检测类文件的语义,常量池符号以及类型检测,这个过程可能会抛出VerifyError。连接阶段接下来进行一些准备工做,它会为静态字段进行建立和初始化标准默认值,并分配方法表。注意,到此步为止不会进行任何java代码的执行。以后连接阶段还有一个可选的步骤,即符号引用的解析。
接下来是类的初始化阶段,它会运行类的静态初始化器,初始化类的静态字段。这是类的java代码的第一次执行。注意类的初始化须要超类的初始化,但不包含超接口的初始化。
JAVA虚拟机规范(JVMS)规定了类的初始化发生在类的第一次“活化使用”,java语言规范(JLS)容许连接阶段的符号解析过程在不破坏java语义前提下的灵活性,装载,连接和初始化的每个步骤都要在前一步骤完成后进行。为了性能考虑,HotSpot虚拟机通常会等到要去初始化一个类时才会去进行类的装载和连接。因此举个简单的例子,若是类A引用了类B,那么加载类A将不会必然致使B的加载,除非在验证阶段必需。当执行了第一个引用B的指令时,将会致使B的初始化,而这又须要先对类B进行装载和连接。缓存
类加载的委托机制
当一个加载器被要求查找和加载一个class时,它能够请求另外一个类加载器去作实际的加载工做。这个机制被称为加载委托。第一个类加载器是一个“初始化加载器”,而最终定义了该类的类加载器被称为“定义加载器”,在字节码解析的例子中,初始化加载器负责该类的常量池符号的解析。
类加载器是分层定义的,每一个类加载器可有委托的双亲。委托机制定义了二进制类表示的检索顺序。JAVASE类加载器按层序检索启动类加载器,扩展类加载器和系统类加载器。系统类加载器同时也是默认的应用类加载器,它会运行main方法并从类路径下加载类。应用类加载器能够是JAVASE 类加载器库中的实现,也能够由应用开发人员实现。JAVASE类库实现了扩展类加载器,它负责加载jre下lib/ext目录中的类。
做者在“54个JAVA官方文档术语”一文中曾说过,这一机制已经不适用于JAVA9以上版本的描述,若是去查询有关文章,能够发现这个经典的类加载委托机制其实已经历过三次破坏(委托机制出厂时晚于加载器自己,破坏一;线程上下文类加载器,破坏二;热部署的后门,破坏三),而做者我的认为类加载器支持JAVA9以后的模块路径的加载也是一种破坏,它们之间再也不是简单的委托加载,也不只从类路径下加载,不一样路径加载到的模块也有不一样的处理机制,详细描述见该文。
启动类加载器是由vm实现的,它从BOOTPATH下加载类,包含rt.jar中的类定义。为了快速启动,vm也会经过类数据共享(cds)来处来类的预加载。关于cds,在最新的几版jdk中有所更新,咱们在稍后的章节中简述。tomcat
类型安全
类或者接口名是由包含包名称的全限定名定义的。一个类的类型由该全限定名和类加载器所惟必定义,因此类加载器其实能够理解为一个名称空间,两个不一样类加载器定义的同一个类实际上会是两个class类型。
vm会对自定义类加载器进行限定,保证不能与类型安全发生冲突。当类A中调用类B的方法时,vm经过追踪和检查加载器约束保证两个类的加载器在方法参数和返回值上协商一致。
HotSpot中的类元数据
类加载的结果是在永久代(旧版)建立一个instanceKlass或者arrayKlass。instanceKlass指向一个java.lang.Class的实例,虚拟机c++代码经过klassOop访问instanceClass。
HotSpot内部类加载数据
HotSpot虚拟机为了追踪类加载而维护了三张主哈希表。分别是SystemDictionary表,它包含被加载的类,它们映射键为一个类名/类加载器对,值为一个klassOop,它同时包含了类名/初始化加载器对和类名/定义加载器对,目前只有在安全点才能够移除它们;PlaceholderTable表,它包含当前正在被载器的类,它被用于前述ClassCircularityError检查和支持多线程类加载的加载器进行并行加载;LoaderConstraintTable,它追踪类型安全检查约束。这些哈希表都由一个锁SystemDictionary_lock来保护,通常状况下vm中的类加载阶段是使用类加载器对象锁串行执行的。
JAVA语言是类型安全的,标准的java编译器会生产可用的类文件和类型安全的代码,可是jvm不能保证代码是由可信任的编译器生成的,所以它必须在连接时进行字节码校验(bytecode verification)重建类型安全。
字节码校验的规范详见java虚拟机规范的4.8节。规范中规定了JVM校验的代码动态和静态约束。若是发现了任何与约束冲突的地方,虚拟机将会抛出VerifyError并阻断类的连接。
可静态检查的字节码约束有不少,'ldc'码(Low Disparity Code 低差异编码)的操做数必须为一个可用的常量池索引,它的类型是CONSTANT_Integer, CONSTANT_String 或 CONSTANT_Float。其余指令须要的检查参数类型和个数的约束须要对代码动态分析,这样来决定执行时哪一个操做数可出如今表达式栈。
目前,有两种办法(截止1.6)分析字节码并决定在每一条指定中出现的操做数类型和个数。传统的办法被称做“类型推断”,它经过对每一个字节码进行抽象解释,在代码的分支处或异常句柄处进行类型状态的合并。整个分析过程会迭代所有的字节码,直到发现这些类型的“稳态”。若是不能达到稳态,或者结果类型与一些字节码的约束冲突,那么抛出VerifyError。这一步的验证代码位于外部库libverify.so中,它使用JNI去收集所需的类和类型的信息。
在JDK6中出现了第二种被称为“类型验证“的方法,在这种方法中,java编译器经过代码属性,StackMapTable来提供每个分支和异常目标的稳态类型信息。StackMapTable包含大量的栈图桢,每个桢表示方法的某一个偏移量的表达式栈和局部变量表中的条目类型。jvm接下来只须要遍历字节码并验证字其中的类型正确性。这是一个已经在JAVAME CLDC中使用的技术。由于它小而快,此验证方法vm自身便可构建。
对于全部版本号低于50,建立早于JDK6的类文件,jvm会使用传统的类型推荐方式验证类文件,不然会使用新办法。
类数据共享是一个JDK5引入的功能,旨在提升java程序语言应用的启动时间,尤为是小型应用,同时,它也能减小内存占用。当jre安装在32位系统时而且使用sun提供的安装器时,安装器会从系统jar中载入一组类并生成一种内部的格式,而后转储为一个文件,这个文件被称做”共享存档“。若是没有使用sun提供的jre安装器,也能够手动执行。在后续的jvm执行时,这个共享存档文件被映射进内存,节省了其余jvm装载类和元数据的时间。
目前官方对于cds的文档未整理完善,在截止到JAVA8的有关文档中,仍能够见到这样一句描述:Class data sharing is supported only with the Java HotSpot Client VM, and only with the serial garbage collector,即类共享目前只在HotSpot client虚拟机中支持,且只能使用serial垃圾收集器。而在JAVA9-12的若干新特性中,也对cds有过一些更新描述,如JAVA10中对类数据的共享包含了应用程序的类,在JAVA11中模块路径也支持了cds。但做者并未在专门的垃圾收集器中找到大篇幅的详述,不过根据jdk12的jvm文档中介绍,cds已经在 G1, serial, parallel, 和 parallelOldGC 几种垃圾收集器中支持,且默认使用G1的128M堆内存。且G1在JDK7中已出现,在JDK9中已经成为默认的垃圾收集器,parallel 出现的相对更早,所以做者严重怀疑JAVA8中相应文档描述的准确性,好在咱们能够直接去看最新版。
cds能够减小启动时间,由于它减小了装载固定的类库的开销,应用程序相对于使用的核心类越小,cds就至关节省了越多的启动时间。cds同时也有两种方式减小了jvm实例的内存占用。首先,一部分共享存档文件被映射进内存并做为只读的库,多个jvm进程不须要重复占用进程的内存空间;其次,由于共享存档文件中包含的类数据已是jvm使用的格式,处理rt.jar(低于9的版本)所需的额外内存开销也能够省去了,这使得多个应用在同一机器上可以更优的并发执行。
在HotSpot虚拟机中,类共享的实现实际是在永久代(元空间)中开辟了新的内存区域存放共享数据。存档文件名为”classes.jsa“,它会在vm启动时映射进这个空间。后续的管理由vm内存管理子系统负责。
共享数据是只读的,它包含常量方法对象(constMethodOops),符号对象(symbolOops),基本类型数组,多数字符数组。可读写的共享数据包含可变的方法对象(methodOops),常量池对象(constantPoolOops),vm内部的java类和数组实现(instanceKlasses和arrayKlasses), 以及大量的String,Class和Exception对象。
做者看来,近几版的jdk关于cds的几处更新明显借鉴了一些如tomcat等服务器的机制,适配愈来愈多的云生产环境,减小内存开销和启动开销都是为云用户省钱的方式。
当前HotSpot解释器是一个基于模板的解释器,它被用来执行字节码。HotSpot在启动时运行时用InterpreterGenerator在内存中利用TemplateTable(每一个字节码有关的汇编代码)中的信息生成一个解释器实例。模板是每一个字节码的描述,模板表定义了全部模板并提供了获取指定字节码的访问方法。在jvm启动时,可以使用-XX:+PrintInterpreter打印有关的模板表信息。
执行效果上看,模板好于经典的switch语句循环的方式,缘由也很简单,首先switch语句执行重复的比较操做来获得目标字节码,最极端状况它可能须要对一个给定的指定比较全部的字节码;第二,模板使用共享的栈来传递java参数,同时本地c方法栈被vm自身来使用,大量的jvm内部变量是用c变量存放的(如线程的程序计数器或栈指针),它们不保证永久存放在硬件寄存器中,管理这些软件的解释结构会消耗总执行时间中的至关可观的一部分。
从全局来看,HotSpot解释器大幅弥合了虚拟机和实体机器之间的裂缝,它大大加快了解释的时间,可是牺牲了不少代码的机器块,同时也增大了代码大小和复杂度,也须要一些代码的动态生成。很明显,debug机器动态生成的代码要比静态代码更加困难。
对于一些对汇编语言来讲过于复杂的操做,如常量池的查找,解释器会运行时调用vm来完成。
HotSpot解释器也是整个HotSpot自适应优化历史中重要的一部分,自适应优化解决了JIT编译的问题,大部分状况下,几乎全部的程序都是用大量时间执行极少许的代码,所以运行时不须要逐方法编译,vm仅使用解释器来当即运行程序,分析代码在程序中运行的次数,避免编译不频繁运行的程序代码(大多数),这样HotSpot编译器能够专一于程序中最须要性能优化的部分,并不增长全局的编译时间,在程序持续运行期间进行动态的监控,达到最适应用户须要的目的。
jvm使用异常做为一个信号,它说明程序中出现了与java语言语义相冲突的事件,数组越界是一个极简的案例。异常会致使控制流从异常发生或抛出的点转到程序指定的处理点或捕获点的一次非本地转换。HotSpot解释器和动态编译器在运行时协做实现了异常的处理。异常处理有两种简单案例,异常抛出并由同一方法捕获,异常抛出并由调用者捕获。后一种状况稍微复杂一些,由于须要展开栈来找出恰当的处理者。
要初始化一个异常有多个方式,如throw字节码,从vm内部调用中返回,JNI调用中返回,或java调用中返回,最后一个状况实际上是前三者的后一阶段。当vm意识到有异常抛出时,执行运行时系统去找出该异常最近的处理器,这一过程会用到三片信息:当前方法,当前字节码,异常对象。若是当前方法没有找处处理器,如上面提到的,将当前活化的栈桢出栈,进程将在此前的栈桢中迭代重复上述步骤。一旦找到了合适的处理器,vm更新执行状态,跳转到相应的处理器,java代码在相应位置继续执行。
普遍来说,能够把“同步”定义为一个阻止或恢复不恰当的并发交互(通常称为竞态)的一个机制。在java中,并发经过线程来表示,锁排他是java中常见的一个同步案例,这一过程当中,只有一个线程同时被容许访问一段保护的代码或数据。
HotSpot提供了java监视器的概念,线程可经过监视器来排它的运行应用代码。监视器只有两个状态:锁或者未锁,一个线程能够在任什么时候间持有(锁住)监视器。只有在获取了监视器后,线程才能进入被监视器保护的代码块。在java中这类被监视器保护的代码块称为同步代码块。
无竞态的同步包含了大多的同步状况,它由常量时技术实现。java对于同步机制作了大量的优化,偏向锁技术是其中之一,由于大多数的对象一辈子只被最多一个线程持有锁,所以容许该线程将监视器偏向给本身,一旦偏向,该线程后续锁和解锁再也不须要额外又昂贵的原子指令开销。
对于有竞态的同步操做场景,使用高级自适应自旋技术提升吞吐量。即便此时应用中有大量的竞态,在经历这些优化后,同步操做性能已经大幅提高,从jdk6开始,它再也不是如今的real-world程序中的重大问题。
在HotSpot中,大多的同步操做是由一种被称做“fast-path”(快路)代码的调用完成的。有两种即时编译器(JIT)和一个解释器,它们均可以产生快路代码。两种编译器分别是C1,即-client编译器,以及C2,即-server编译器。C1和C2均直接在同步点生成快路代码。在通常没有竞态的状况下,同步操做将会彻底在快路中执行,然而当发现须要去阻塞或者唤醒一个线程时(如monitorenter monitorexit),将会进入slow-path执行,它由本地C++代码实现。
单个对象的同步状态是在对象中的第一个word中编码存放的(mark word,详见前面的文章“54个JAVA官方文档术语”)。mark word对同步状态元数据来讲是多用的(其实mark word自己也是多用的,它还包含gc分代数据,对象的hash码值)。这些状态包含:
Neutral(中立): 未锁
Biased(偏向): 锁/未锁+非共享
Stack-Locked(栈锁): 锁+共享 无竞态
Inflated(膨胀锁): 锁/未锁+共享和竞态
线程管理覆盖线程从建立到销毁的整个生命周期,并负责在vm内协调各个线程。这个过程包含java代码建立的线程(应用代码或库代码),绑定到vm的本地线程,出于各类目的建立的vm内部线程。线程管理在绝大多数状况下是独立于运行平台的,但仍有一些细节与所运行的操做系统有所关联。
线程模型
在hotspot虚拟机中,java线程和操做系统线程是一对一映射的关系,java线程即一个java.lang.Thread实例,当它被开启(start)后,本地线程也随之建立,当它终止(terminated)时,本地线程回收。操做系统负责调度全部的线程以及派发可用的cpu资源。java线程的优先级以及操做系统线程的优先级机制很是复杂,在不一样的操做系统中表现也差别极大,此处略。
线程建立和销毁
有两种办地能够向虚拟机中引入一个线程:执行java.lang.Thread对象的start方法;或使用JNI将一个已存在的本地线程绑定到vm。出于一些目的,vm内部也有一些办法建立线程,本处不予讨论。
在vm的一个线程上实际上关联了若干个对象(HotSpot虚拟机是由面向对象的c++实现),具体有:
a.java.lang.Thread实例表现java代码中的一个线程。
b.JavaThread实例表示vm中的一个java.lang.Thread,JavaThread是Thread的子类,它包含额外的用以追踪线程状态的信息。一个JavaThread实例持有关联的java.lang.Thread对象的引用(指针),同时持有OSThread实例的引用。java.lang.Thread也持有JavaThread的引用(以一个整数表示)。
c.OSThread(直译为操做系统线程)实例表示了一个操做系统的线程,它包含额外的可用于追踪线程状态的操做系统级别的信息。OSThread包含了一个平台指定的可用于定位真实操做系统线程的句柄。
当java.lang.Thread实例启动,vm建立关联的JavaThread和OSThread对象,并最终建立了一个本地线程。在准备好全部vm状态后(如线程本地存储,分配缓存,同步对象等)以后,本地线程得以启动。本地线程完成初始化并执行一个start-up方法,它会导向java.lang.Thread对象的run方法。随后,在该方法返回或抛出未捕获的异常时终止线程,而且在终止时与vm交互,这个过程是用以判断是否它此时也须要终止vm。线程终止会释放掉全部关联的资源,从已知线程集中移除掉JavaThread实例,执行OSThread实例和JavaThread实例的销毁过程,并最终中止startup 方法的执行。
可以使用JNI调用AttachCurrentThread把本地线程绑定到虚拟机。做为此方法的响应,OSThread和JavaThread实例会被建立并进行基本的初始化。接下来会使用绑定线程命令提供的参数及Thread类的构造器反射初始化一个java线程。绑定完成后,线程能够经过可用的JNI方法调用所需的java代码。当本地线程不但愿继续在vm中进行执行时,可以使用JNI调用DetachCurrentThread来解除与vm的关联(会释放资源,丢弃指向java.lang.Thread实例的引用,销毁JavaThread和OSThread对象等)。
使用JNI调用CreateJavaVM建立vm是一个特殊的绑定本地线程的例子,它会由执行器(java.c)完成或经过一个本地应用来完成。这件事会形成一系列的初始化操做,也会在接下来出现相似执行AttachCurrentThread的行为。随后线程继续执行所需的java代码(此例中即反射执行main方法)
线程状态
vm维护了一组内部的线程状态来标识各线程的工做。协调各线程的交互,或当线程执行错误进行debug时均须要用到这些状态标识。当执行了不一样的动做时,线程的状态能够发生改变,可即时使用这些转换点检测相应的线程是否具有执行将要执行的动做的客观条件,安全点便是一个典型的例子。
以虚拟机的视图来看,线程有如下几个状态:
_thread_new:表示一个新线程处于初始化的过程当中。
_thread_in_Java:表示一个线程正在执行java代码。
_thread_in_vm:表示一个线程正在vm内部执行。
_thread_blocked:表示线程因某些缘由阻塞(缘由多是正在获取一个锁,等待一个条件,sleep,执行阻塞io等)。
出于debug的目的,可能须要一些额外的信息。如对于一些工具,或者用户须要进行线程栈转储或栈迹追踪等操做时,均须要额外的信息,OSThread维护了相应的一些信息,但部分信息如今已经再也不使用了,在线程转储时,可报告的状态额外包含:
MONITOR_WAIT:表示线程正在等待获取竞态锁。
CONDVAR_WAIT:表示线程正在等待vm使用的内部条件变量(与java级别的对象无关联)。
OBJECT_WAIT:线程执行了Object.wait方法。
虚拟机的其余子系统和库可能维护了本身的状态信息,如JVMTI工具,Thread类自己维护的ThreadState等。这些状态通常不被其余组件使用。
虚拟机内部线程
JAVA的执行有着严格的步骤,不一样于某些脚本语言,java即便运行简单的Hello World也须要相应的资源准备,所以,对于最简单的Hello World,也能够发现系统中其实建立了若干个线程,它们主要是由vm中的线程和有关代码库中使用的线程(包含引用处理器,终结者线程等)组成。主要的虚拟机线程有如下几种:
a.vm线程:它是VMThread的单例,负责执行虚拟机操做。
b.周期任务线程:它是在vm内部执行周期操做的线程,是WatcherThread的实例。
c.GC线程:顾名思义。
d.编译器线程:负责运行时执行字节码到本地代码的编译。
e.信号派发线程:负责等待进程信号并派发给java级别的信号处理方法。
以上全部线程是Thread类的实例,且全部执行java代码的线程均为JavaThread实例。vm内部维护了一个Threads_list的数据结构,它是一个追踪全部线程的链表,在vm内部有一个核心的同步锁Threads_lock,该锁就用于保护Threads_list。
VMThread会监测一个VMOperationQueue队列,该队列中存放的成员所有为“操做”,等待相应的操做入队后,它会执行相应的操做。这些操做被交给VMThread来执行,由于它们须要vm到达安全点才可执行。简单来讲,当vm在到达安全点时,全部vm内运行的线程均会阻塞,全部在本地代码中执行的线程在安全点期间被禁止返回vm执行。这意味着虚拟机操做能够在已知无线程处于正在更改java堆的前提下进行运行,且此时全部的线程处在一个特殊的,不改变java栈的可检视状态。
最著名的虚拟机操做之一即gc,或者更精确一点是不少gc算法中的“stop the world”阶段,但也存在不少基于安全点的其余操做,做者在“54个java官方文档术语”一文中简单列举了这些操做。
不少虚拟机操做是同步阻塞的,请求者会阻塞到操做完成,但也有一些异步并发的操做,请求者能够和VMThread并行执行。
安全点是使用协做轮询的机制初始化的。简单来讲,线程会去询问“我是否要为一个安全点阻塞”。这个询问机制的实现并不简单。当发生线程的状态转换时会常见询问这个问题,但并是全部的状态转换都会询问,如当一个线程离开vm并进入native代码块时。当从编译的代码返回时,或在循环迭代的阶段,线程也会询问这个问题。对于执行解释代码的线程来讲是不常询问的,但在安全点,它也有相应的方案,当请求安全点时,解释器会切换到一个包含了该询问的代码的转发表,当安全点结束后,从派发表切回。一旦请求了安全点,VMThread必须等到全部已知线程均处于安全点-安全状态,而后才可执行虚拟机操做。在安全点期间,使用Threads_lock来block住那些正在运行的线程,虚拟机操做完成后,VMThread释放该锁。
除了由JAVA堆管理者和gc维护的JAVA堆之外,HotSpot虚拟机也使用一个c/c++堆(即所谓的分配堆)来存放虚拟机的内部对象和数据。这些用来管理C++堆操做的类都由一个基类Arena(竞技场)派生而来。
Arena和它的子类提供了位于分配/释放机制顶层的一个快速分配层。每个Arena在3个全局的块池(ChunkPools)中进行内存块(Chunk)的分配。不一样的块池知足不一样大小区间的分配,举例说明,若是请求分配1k的内存,那么会用“small”块池分配,若是请求分配10k内存,则使用“medium”块池,这样能够避免内存碎片浪费。
Arena系统也提供了比纯粹的分配/释放机制更佳的性能。由于后者可能须要获取一个操做系统的全局锁,它会严重影响扩展性并伤害系统性能。Arena是一些缓存了指定内存数量的线程本地对象,这样的设计使得它能够在分配时使用“快路”分配而不用获取该全局锁,对于释放内存的操做,一般状况下Arena不须要得到锁。
Arena的两个子类,ResourceArena应用于线程本地资源管理,HandleArena用于句柄管理,在client和server编译器中均用到了这两种arena。
JNI表明本地程序接口。它容许运行在jvm中的java代码与使用其余语言(如c/c++)实现的应用或库进行交互。JNI本地方法能够用来作不少事情,如建立对象,检视对象,更新对象,调用java方法,捕获抛出的异常,加载类和获取类信息,执行运行时类型检测等。JNI也可使用Invocation api来启用jvm中嵌入的任意native应用,经过它,咱们能够轻易地让已有应用能够用java运行而不用去连接vm源码。
但有重要的一点,一旦使用了JNI,便失去了使用java平台的两个重要的好处。
第一,依赖jni的java应用不保证能在多平台上可用,尽管基于java实现的部分是能够跨宿主机环境的,使用本地程序语言实现的部分仍旧须要从新编译。
第二,使用java语言编写的程序是类型安全的,C或者C++则不是。结果就是使用了JNI的程序员必须额外注意这部分代码,行为不端的本地方法可能扰乱整个应用,出于这个缘由考虑,在执行jni功能前,相应使用到jni的应用必定要负责它的安全性检查。
原则上讲,应尽量少地使用本地方法,并作好这部分代码与java应用的隔离,做者看来,unsafe后门包是一个典型的案例。
在HotSpot虚拟机中,jni方法的实现相对直接,它使用各类vm内部原生规则来执行诸如对象建立方法调用等行为,一般状况,相应的如解释器等子系统也使用了这些运行时规则。
可以使用命令行选项-Xcheck:jni来帮助debug那些使用了本地方法的应用,该选项会使得JNI调用时用到一组debug接口。这些接口会更加严格地进行JNI调用的参数验证,同时还会作一些额外的内部一致性检查。
HotSpot对于执行本地方法的线程进行了额外“照顾”,对于一些vm的工做,好比gc过程当中,一部分线程必须保证在安全点阻塞,从而保证java堆在这些敏感过程当中不会再次更改。当咱们但愿把一个安全点上的线程带入到本地代码执行时,它会被容许进入本地方法,可是禁止从该方法返回java代码或者执行JNI调用。
毫无疑问,提供致命故障的处理对jvm来讲是很是之必需的。以oom为例,它是一个典型的致命错误。当发生这类错误时,必定要给用户提供一些合理且友好的方式来理解致命错误成因,从而能快速修复问题,这方面的问题不只包含应用自己,也包含jvm自己。
第一,通常当jvm在致命故障发生时crash掉,它会转储一个hotspot的错误日志文件,格式为:hs_err_pid<pid>.log。从JDK6开始大幅提高了这些致命错误的可诊断性,当发生crash,错误日志文件中会包含当前的内存图像,所以能够很容易搞清楚发生crash时的内存布局。
第二,也可使用-XX:ErrorFile=选项来指定错误日志的位置。
第三,发生oom时,也会触发生成该错误文件。
还有一个重要的功能,能够指定一个选项:-XX:OnError="cmd1 args...;com2 ...",这样当发生了crash时会执行这些指令,相应的指令就比较自由,好比咱们能够指定此时执行一些诸如dbx或Windbg之类的debugger执行相应的操做。早于jdk6的应用可以使用-XX:+ShowMessageBoxOnError来指定发生crash时使用的debugger。如下是jvm内部处理致命错误的一些摘要:
首先,用VMError类聚合和转储hs_err_pid<pid>.log文件,当发现未识别的信号/异常时,由操做系统指定的代码调用它生成该文件。
第二,vm使用信号来进行内部的交流,当出现未识别的信号,致命错误处理器被执行。而这个信号可能源自一个应用的jni代码,操做系统本地库,jre本地库,甚至是jvm自己。
第三,致命错误处理器是慎重编写的,这也是为了不它本身也出现错误,好比在出现StackOverFlow时,或在持有重要的锁期间发生crash(如持有分配锁)。
死锁是一种常见的错误,通常发生在应用程序在申请多个锁时顺序不正确的状况。当死锁发生时,找出相应的点也是比较困难的,此时能够抓出java进程id,发送SIGQUIT到该进程(Solaris/Linux),会在标准输出中输出java级别的栈信息,这对分析死锁帮助极大,不过在jdk6之上的版本,已经可使用Jconsole来轻松处理该问题。
顺便简单提一提除了Jconsole/VisualVM等集成工具以外,一些单一目的的自带工具。
jps:jvm进程工具,能够查看各jvm进程,名称和编号。
jstat:虚拟机统计信息。好比发生了多少次full gc等。
jinfo:java配置信息工具,运行时查看jvm进程的配置。
jmap:内存映像工具,能够将当前内存状况转储一个快照文件。
jhat:堆转储快照分析工具。
jstack:java堆栈跟踪工具。
本文简述了包含运行时参数处理,线程管理,类加载,类数据共享,运行时编译,异常处理,重大错误处理等java运行时技术。参考资料主要源自官方的若干文档,一部分资料是专属性的,如专门描述JVM或JIT,但根本没法肯定成做于哪一个版本(关于cds做者判断与JAVA8中的描述相同,但显然早已不适用),一部分资料是依托于较新版本的,由于新旧版本的文档并未保持同一目录结构,有些组件未能在新版中找到详尽的文档,所以不免会有不许确或过期的内容,做者争取在后面找到最新且更加权威的资料以修正。 做者我的认为有两点重要的收获,一是宏观上了解了官方出品的HotSpot虚拟机在运行时的框架设计,理解java在运行时为咱们不遗余力作了哪些事;二是了解某些具体模块在新版中的优化和取舍,从而间接了解接下来java的使用趋势。如“云友好”,“多适应”,“开放”等。 这三点是做者我的不成熟的简单总结,写到这里,也顺便对这三点进行一个“简单总结”。 云友好其实体现的方面不少,cds就是重要一点,它在最新几个版本的更新用一句俗化表示:帮用户省钱。G1定时释放无用内存的新特性也体现了这一点。 多适应和开放也很好理解,不止是gc方面,前面简单提过的zgc等针对超大堆的gc,以及G1这种放权让用户指定目标的gc,综合此前的各类gc,基本涵盖了咱们全部可能的应用环境。一样的,模块化系统也自然匹配了中小型到大型项目的需求,一个项目从初创到逐渐壮大,或许最终就是模块不断扩充的过程,模块化系统甚至容许对jdk自己进行按需定制,对于小型设备用户也无益因而一个福音。JIT自己就具有自适应的编译思想,最优化最常执行的代码,graal是新出的基于java的JIT编译器。同步机制也引入了“自适应”自旋锁,G1中对cs的选择也具有自适应性等。 再一次,膜拜前辈。