JVM - 运行时数据区

82 阅读8分钟

JVM - 运行时数据区

计算机架构的本质,是在有限资源的约束下,通过一系列精妙的权衡,来追求极致的性能。在冯·诺伊曼体系下,CPU(中央处理器)、内存(Memory)、输入输出设备(I/O,磁盘),这三样构成了计算机硬件系统的最核心的闭环。任何想实现高性能、高稳定的程序,都必须将这三大核心资源的利用率和特性纳入设计考量,忽略其中一个任何一项,都可能导致程序出现瓶颈、延迟和崩坏。JVM架构也必须围绕这三大核心资源来设计,好消息是,JVM无需直接操作这些硬件,而是通过操作系统提供的抽象层来间接的管理它们。 在JVM引论里面,我们说詹姆斯·高斯林为解决C/C++的一些问题,引入了三大特性:可移植性安全性和健壮性简单性。我们从可移植性、安全性和健壮性出发,来看看JVM是如何实现的。

可移植性

为了解决可移植性,java采用中间语言的形式来实现,这个中间语言就是字节码(Class文件)。也就是说,JVM只认字节码,任何形式的高级语言,只要能够编译成字节码文件,符合java虚拟机规范,都能够被JVM所读取运行。由于计算机三大件间运行速度的巨大鸿沟,而编写好的Class文件,存储在磁盘上(或远程服务器),如果每次需要一个类的信息都去磁盘上获取,严重影响性能。此时内存就登场了,把已经加载过的Class文件信息保存在内存中,后续再使用时,直接从内存获取,而无需去磁盘加载。保存Class文件内容信息的地方就是方法区。

方法区(Method Area)

JVM的"信息中心",存放了关于类本身的元数据(MetaDate),包括如下内容:

类型信息:是每个被加载的类的"蓝图"或"元数据"。JVM需要这些信息来准确知道如何实例化对象、如何调用方法。包括类的完整限定名、类的直接父类的完整限定名、类的类型(类、接口)、类的访问修饰符、实现的接口列表、字段信息(字段名称、类型、访问修饰符)、方法信息(方法名称、返回类型、参数类型、访问修饰符、字节码指令、异常表等);

运行时常量池:是每个类独有的,是Class文件中”常量池表“的运行时表示形式。包括字面量(文本字符串、final修饰的基本类型静态变量的值、其他基本类型的值)、符号引用(需要在运行时转换为直接引用)。

静态变量:即被static修饰的变量。需要注意的是,静态变量本身(变量的引用和基本类型值)存储在方法区,但是如果静态变量是引用类型,则它指向的对象实例存储在堆中;

即时编译器编译后的代码:”热点代码“被编译成的本地机器码也存储在这里,下次调用就直接使用高效本地代码,大幅提升执行速度;

方法表:实现虚方法的多态调用。

在《java虚拟机规范》中,方法区被定义为一个逻辑概念,这就给了各个不同厂商自由发挥的空间。以当前最流行的HotSpot虚拟机来说,在JDK8之前,方法区是堆的一部分,也被称为"非堆"(即JVM自身运行使用的、用于存储和Java对象无关的数据的内存区域);在JDK8及以后版本,方法区则由"元空间"(使用本地内存,不再占用Java堆空间)替代。 巧妇难为无米之炊。方法区就像一个中央仓库,它不直接参与生产,却为整个JVM高效、正确的运行提供了最根本的信息支持。

安全性和健壮性

C/C++由于指针操作和手动管理内存容易导致系统崩溃(太过自由是有风险的),java的安全性和健壮性主要是为了解决这个问题。既然人工管理内存容易出问题,那人工就别管理了,直接交给程序做(垃圾收集器GC),程序员就专注于业务逻辑。既然要管理,集中、有序效率就会更高(GC效率更高)。

堆(Heap)

Java堆是JVM所管理的内存中最大的一块,在虚拟机启动时创建,唯一目的就是为了存放对象实例。《java虚拟机规范》中的描述是:”所有的对象实例及数组都应该在堆上分配“,但随着Java技术的发展,逃逸分析等技术的越发成熟,栈上分配、标量替换等优化手段能够减少不必要的堆上分配。”所有对象实例及数组,逻辑上都是在堆上分配“的说法如今可能更准确。堆是JVM垃圾收集的主要工作区域。

需要注意一下的是,Java堆和操作系统的堆在中文虽然都叫做"堆",但是两个完全不同层次的概念,解决的问题也完全不同。操作系统堆是操作系统提供的一个底层原始内存管理机制,管理并分配一块虚拟地址内存空间给进程,用于满足其动态内存需求;Java堆是Java虚拟机在操作系统之上抽象出来的一个高级对象管理生态系统,专属于Java程序。

程序计数器(Program Counter Register)

线程是操作系统能够进行运算调度的最小单位。JVM本身就是一个运行在操作系统之上的用户态进程。它必须遵守操作系统的所有规则。现代操作系统基于时间片的线程调度机制,操作系统给时间片,JVM就有资源来干点事了,具体干啥,是虚拟机栈说了算。既然是采用时间片轮转的方式轮流使用CPU,对每个线程来说,就需要进行现场保护(线程挂起时保存线程执行状态(上下文))和现场恢复(恢复上下文),程序计数器就是就是保存和恢复上下文的最关键的信息之一。程序计数器是一块较小的、线程私有的内存空间,可以看作是当前线程所执行字节码的行号指示器。如果没有程序计数器,线程切换回来之后,不知道之前执行到哪儿了,程序将无法正常运行。

虚拟机栈(VM Stack)

虚拟机栈是线程私有的内存区域,用于支持Java方法的调用和执行。每个线程在创建时都会分配一个独立的虚拟机栈,其生命周期与线程相同。每个方法被执行的时候,java虚拟机都会同步创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态连接、方法出口等信息;每一个方法被调用直至执行完毕的过程,就对应着一个虚拟机栈帧在虚拟机栈中从入栈到出栈的过程。更详细的信息在后续的虚拟机执行引擎篇章介绍。

本地方法栈(Native Method Stack)

Java的强大之处在于它"万物皆对象"的单一语言体系和强大的跨平台能力。但有时它需要与外部世界打交道:与操作系统交互(执行一些java本身无法完成的操作,比如硬件操作)、访问已有的本地库(重用大量C/C++编写的成熟库)等等。为了执行这些代码,JVM必须提供类似执行Java方法时的运行时环境,这就是本地方法栈。 总结一下是:本地方法栈是JVM为执行本地方法服务的运行时数据区。它是线程私有的,生命周期与线程相同。指的是那些用非Java语言(通常是C/C++)编写,并编译成本地机器码的方法,在Java中使用native关键字声明,没有方法体。

同一个线程,同时会拥有虚拟机栈和本地方法栈,当执行Java代码时,程序计数器指向的是虚拟机栈中的指令地址;当线程执行本地方法时,程序计数器的值为空(undefined),因为执行的是本地机器码,不在JVM管辖之内;在HotSpot等主流实现中,本地方法栈和虚拟机栈合二为一,没有严格区分。

运行时数据区

通过上面介绍,我们再来重温下那张驰名中外的架构图。运行时数据区是JVM的骨架和血液系统,它定义了数据存于何处、如何交互、如何管理,是理解JVM所有行为的基础。

下一篇:JVM - Class文件结构

扫描下方二维码,关注公众号(星翰成长日记),及时获取更新

二维码.jpg

文中所用图片均来自网络,如因图片使用侵犯了他人的版权或其他合法权益,请及时与我们联系,我们将第一时间进行处理。

参考资料:周志明老师《深入理解Java虚拟机》