【扫盲】Java虚拟机体系构成

一、Java虚拟机跨平台特性

Java虚拟机是依据《Java虚拟机规范》中描述的虚拟机行为而实现的,规范中没有明确限制或约束Java虚拟机实现的某些具体细节,这样的话,设计者完全可以自主决定规范中不曾描述的虚拟机内部细节,比如:类加载中的加载阶段并没有指明二进制字节流必须得从某个Class文件中获取、选择哪一种垃圾收集器实现内存回收、选择哪一种手段实现编译器等等。

Java语言自诞生起,就有着“一次编译,处处运行”的目标,这意味着作为执行Java程序的Java虚拟机肩负起了实现跨平台限制的艰巨任务,这里不得不提字节码文件了,它是各种不同平台的Java虚拟机,以及所有平台都统一支持的程序存储格式,也是平台无关性的基础。Java源程序从编译到执行生成计不同操作系统能识别的机器码指令,大致过程如下:

在这里插入图片描述

在1997年发表的第一版《Java虚拟机规范》中就曾经承诺过:“在未来,我们会对Java虚拟机进行适当的扩展,以便更好地支持其他语言运行于Java虚拟机之上”(Inthefuture,wewillconsiderboundedextensionstotheJavavirtualmachinetoprovidebettersupportforotherlanguages)。Java虚拟机发展到今天,尤其是在2018年,基于HotSpot扩展而来的GraalVM公开之后,当年的虚拟机设计者们已经基本兑现了这个承诺。

Java虚拟机不与包括Java语言在内的任何程序语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集、符号表以及若干其他辅助信息。

作为一个通用的、与机器无关的执行平台,任何其他语言的实现者都可以将Java虚拟机作为他们语言的运行基础,以Class文件作为他们产品的交付媒介,也就是说Java虚拟机是丝毫不关心Class文件的来源是什么语言,示意图如下:

在这里插入图片描述

二、Java虚拟机体系构成

上面重要描述了Java虚拟机跨平台的特性,依据《Java虚拟机规范》可以实现不同的Java虚拟机,目前主流的商用三大虚拟机有:Oracle的HotSpot、BEA的JRockit、IBM的J9,而得益于Sun/OracleJDK在Java应用中的统治地位,HotSpot虚拟机理所当然的成为了全世界使用最广泛的Java虚拟机。注意,接下来所描述的虚拟机仅仅指的是HotSpot虚拟机

Java虚拟机整体构成是什么样的呢?画了一张图示意:

在这里插入图片描述
Java虚拟机体系构成部分有:类加载子系统、运行时数据区域、垃圾收集器、执行引擎、本地库接口及本地库等等,下面一一进行简介,重点描述这些部分存在的用途和意义。

1、类加载子系统

经过编译器编译生成的Class文件,最终都是需要加载到虚拟机中之后才能被运行和使用的。Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制

在Java中,类型的加载、连接和初始化过程都是在程序运行期间完成的,因此,Java天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。

一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称为连接(Linking)。这七个阶段的发生顺序如图示:

在这里插入图片描述
Java虚拟机中类加载的全过程,指的就是加载验证准备解析初始化这五个阶段所执行的具体动作,其中,要特别注意加载只是类加载的一个阶段。

  • 加载(Loading):在该阶段,JVM会做三件事,①. 通过一个类的全限定名来获取定义此类的二进制字节流;②. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构; ③. 内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
  • 验证(Verification):在该阶段,目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,有文件格式验证、元数据验证、字节码验证、符号引用验证等,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
  • 准备(Preparation):在该阶段,正式为类中定义的静态变量分配内存并设置类变量初始值。在JDK7及之前,HotSpot使用永久代来实现方法区,而在JDK8及之后,放弃了永久代的概念,同时类变量也会随着Class对象一起存放在Java堆中。
  • 解析(Resolution):在该阶段,将常量池内的符号引用替换为直接引用。
  • 初始化(Initialization):在该阶段,JVM才真正开始执行类中编写的Java程序代码。

把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类,实现这个动作的代码被称为“类加载器”(ClassLoader)。类加载器只用于实现类的加载动作,对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间,也就是说即使两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等,这点需要特别注意。

类加载器,整体上采用的是三层类加载器、双亲委派的类加载架构,即使这套架构在Java模块化系统出现后有了一些调整变动,但依然未改变其主体结构,类加载器的双亲委派模型及Java9引入的模块化系统后面再专门整理。

2、运行时数据区域

内存管理中的运行时数据区域有:程序计数器、Java虚拟机栈、本地方法栈、、Java堆等,下面简介下各自的含义和用途:

程序计数器

是线程私有的、一块较小的内存空间,可看作当前线程所执行的字节码的行号指示器(对于Java方法记录的是正在执行的虚拟机字节码指令的地址,对于本地方法计数器值为空)。

字节码解释器工作过程中通过改变这个计数器的值来选取下一条需要执行的字节码指令,像分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。特别注意,它还是唯一的不会引起OutOfMemoryError情况的内存区域

Java虚拟机栈

是用于描述Java方法执行线程内存模型的、线程私有的内存区域。每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(StackFrame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。我们常说的“Java栈”指的就是Java虚拟机栈,更多情况下更确切的说是虚拟机栈中局部变量表部分,关于Java虚拟机栈执行方法的过程和对应的栈操作,如图所示:
在这里插入图片描述

  • 局部变量表(LocalVariablesTable):它存放了编译期可知的各种Java虚拟机基本数据类型对象引用(即reference类型)及returnAddress类型(指向了一条字节码指令的地址),这些类型以局部变量槽(Slot)形式存储的。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小(指变量槽Slot的数量)。

  • 操作数栈(OperandStack):它是一个后入先出的栈结构,当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作,Java虚拟机的解释执行引擎被称为“基于栈的执行引擎”,这里的“栈”就是操作数栈了。

  • 动态连接(DynamicLinking):每个栈帧都包含一个指向运行时常量池[1]中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。

  • 方法出口(Return Address):该部分也称为方法返回地址,方法执行有两种状况:一是方法正常调用完成,二是方法遇到异常进行异常分派

特别注意,Java虚拟机栈规定了两类异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。

本地方法栈
是为虚拟机使用到的本地方法服务的、线程私有的内存区域。与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。

Java堆

是用于存放对象实例的、垃圾收集器所管理的、线程共享的、虚拟机所管理的内存中最大的一块内存区域(需要注意的是,由于即时编译技术的进步,尤其是逃逸分析技术的日渐强大,栈上分配、标量替换优化手段已经导致一些微妙的变化悄然发生,使得Java对象实例都分配在堆上也渐渐变得不是那么绝对了)。

《Java虚拟机规范》规定了Java堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的,对于大对象,如数组对象,多数虚拟机实现出于实现简单、存储高效的考虑,很可能会要求连续的内存空间。特别注意,在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。

方法区

是用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据的、线程共享的内存区域。我们也可以给方法区起个别名叫作“非堆”(Non-Heap),目的是与Java堆区分开来。

在JDK8之前,HotSpot虚拟机使用永久代来实现了方法区,这样使得HotSpot的垃圾收集器能够像管理Java堆一样管理这部分内存,省去专门为方法区编写内存管理代码的工作,这是HotSpot虚拟机独有的。考虑到HotSpot未来的发展,在JDK6的时候HotSpot开发团队就有放弃永久代,逐步改为采用本地内存(NativeMemory)来实现方法区的计划了,到了JDK7的HotSpot,已经把原本放在永久代的字符串常量池、静态变量等移出,而到了JDK8,终于完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Meta-space)来代替,把JDK7中永久代还剩余的内容(主要是类型信息)全部移到元空间中。

《Java虚拟机规范》对方法区的约束是非常宽松的,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,甚至还可以选择不实现垃圾收集。特别注意,如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常。

3、垃圾收集器

垃圾收集器(GC)所关注的是Java堆和方法区的内存该如何管理,因为他们存在显著的不确定性:一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样,只有处于运行期间,我们才能知道程序究竟会创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的。

Java堆的垃圾收集是不再被引用的对象,方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型,其中废弃的常量回收和Java堆的对象回收类似。

在《Java虚拟机规范》中对垃圾收集器应该如何实现并没有做出任何规定,因此不同的厂商、不同版本的虚拟机所包含的垃圾收集器都可能会有很大差别,不同的虚拟机一般也都会提供各种参数供用户根据自己的应用特点和要求组合出各个内存分代所使用的收集器。

关于垃圾收集算法,垃圾收集器种类和特点后面再专门整理。

4、执行引擎

执行引擎是Java虚拟机核心的组成部分之一,“虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的,而虚拟机的执行引擎则是由软件自行实现的。因此,可以不受物理条件制约地定制指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式。

在《Java虚拟机规范》中制定了Java虚拟机字节码执行引擎的概念模型,这个概念模型成为各大发行商的Java虚拟机执行引擎的统一外观(Facade)。在不同的虚拟机实现中,执行引擎在执行字节码的时候,通常会有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种选择,也可能两者兼备,还可能会有同时包含几个不同级别的即时编译器一起工作的执行引擎。但从外观上来看,所有的Java虚拟机的执行引擎输入、输出都是一致的:输入的是字节码二进制流,处理过程是字节码解析执行的等效过程,输出的是执行结果。

5、本地库接口及本地库

本地库接口及本地库,通过Java虚拟机可以使得Java程序与底层原生语言(如C,C++等)编写的应用程序和库进行交互操作,它没有对底层 Java 虚拟机的实现施加任何限制,因此Java虚拟机厂商可以在不影响虚拟机其它部分的情况下添加对该部分的支持。

引入本地库接口及本地库,目的是为了融合不同编程语言为Java所用。最初是基于执行效率的原因,现在Java应用程序的速度越来越快,很少使用native方法,但是与硬件有关的交互操作还是在继续使用。

小结

至此,根据Java虚拟机体系构成的思路,展开了对其中的一些重要概念进行了说明,目的是把一些基础概念先弄清楚,后面再深入的去理解和分析,对JVM构成有个大致的整体把握。

参考:《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》

猜你喜欢

转载自blog.csdn.net/qq_29119581/article/details/112569553