JVM规范之运行时数据区域
JVM运行时数据区
- 前言
- 为什么要阅读jvm规范?
- 阅读本篇文章可以学习到啥?
- 正文
- 概述
- JVM线程私有的运行时数据区
- pc(program counter) Register
- JVM Stack
- Native Method Stack
- JVM线程共享的运行时数据区
- Heap
- Method Area
- Run-time constant pool
- 总结
- 参考链接
前言
为什么要阅读jvm规范?
- 建立对JVM的基础认知,JVM 规范是对 Java 虚拟机的抽象描述,定义了 Java 虚拟机的架构、功能和行为。它提供了一个统一的标准,让初学者能够从整体上理解 Java 程序在虚拟机中的运行原理,包括类加载、内存管理、字节码执行等核心概念。
- 避免实现细节干扰,不同的 JVM 实现(如 HotSpot、OpenJ9 等)在具体的实现方式上可能存在差异,包括优化策略、内存布局、垃圾回收算法等。对于初学者来说,直接陷入这些实现细节中,容易造成混淆,增加学习的难度。先学习 JVM 规范,可以让初学者在理解了基本原理的基础上,再去学习具体实现时,更容易理解这些实现的特点和差异,而不是一开始就被复杂的实现细节所困扰。
- 理解JVM的跨平台设计,Java 语言的一个重要特性是 “一次编写,到处运行”,这依赖于不同平台上符合 JVM 规范的虚拟机实现。学习 JVM 规范能让初学者明白,只要遵循规范,Java 程序就能在各种不同的操作系统和硬件平台上运行。这种跨平台的思维方式是 Java 编程的重要理念,有助于初学者更好地理解 Java 的优势和应用场景,而不仅仅局限于某一种具体的 JVM 实现。
- 快速学习新技术,JVM 技术在不断发展,新的特性和优化不断被引入。阅读 JVM 规范能让初学者跟上技术发展的步伐,理解新特性的设计目的和原理。当新的 JVM 实现版本发布或出现新的 JVM 相关技术时,基于对规范的理解,初学者能够更容易地学习和掌握这些新知识,而不是依赖于特定实现的经验,因为特定实现可能会随着时间的推移而发生较大变化,而规范相对稳定且具有前瞻性。
阅读本篇文章可以学习到啥?
本篇文章根据jvm8规范的 §2.5编写,阅读本篇文章可以简单了解JVM的运行时数据区包含哪些部分,了解各个部分的角色和功能,方便后续更加深入地学习JVM关于运行时数据区的细节。
正文
概述
JVM中定义了多种运行时数据区域,但是按照生命周期可以分为两种,一种是跟随JVM启动创建和销毁的,另一种则是跟随线程创建和销毁的,每个内存区域都有自己的职责,相互之间也并不是完全的隔离的状态,存在包含关系,jvm对于内存的管理的实现并没有做严格的限制,这也为JVM的实现提供足够的灵活性和扩展性。
JVM线程私有的运行时数据区
pc(program counter) Register
JVM支持多线程同时运行,每个JVM线程都有自己的pc Register,用于记录当前正在执行的字节码指令地址,在任意时刻,一个JVM线程都正在运行一个方法中的字节码指令,对于pc寄存器中存储的内容,包含两种情况:
- 线程正在执行的方法是一个native method,则PC中的值是 undefined;
- 线程正在执行的方法是一个非native method,则PC寄存器中存储的是将要执行的字节码指令地址;
因此,JVM要求pc Register的宽度必须在特定的平台上足够容纳returnAddress和本地指针宽度
补充:
- native method是JVM为了实现与其它编程语言进行交互而设计的,指使用非Java语言实现的方法,程序员或者用户能够在java程序中调用这些方法,实现和非Java程序进行交互;
- returnAddress是JVM中的一种数据类型,存储的是某个字节码指令的地址,是jsr, ret, jsr_w指令使用的数据类型,可以用于实现程序逻辑跳转;
JVM Stack
JVM Stack,即Java虚拟机栈,每个JVM线程都独立拥有JVM Stack,它随JVM线程创建而创建,随线程销毁而销毁,栈中存储的是帧,和传统编程语言C很类似,栈用来保存局部变量、中间结果,也用于保存方法调用和返回的上下文信息;
JVM并不会直接操作栈,只能通过方法的调用和返回间接进行压栈和出栈的操作;有一点区别是,传统语言中栈通常是一段连续的内存空间,但是JVM并没有同样的要求,栈帧也可以在堆上进行分配,因此,可以更加充分的利用碎片化的内存空间;
JVM Stack的空间大小有两种设计:
- 如果采用固定大小的设计,则每个JVM线程被创建时,栈在初始化时也可以是独立配置的,JVM实现时可以提供给程序员配置栈大小的方法;
- 栈也可以根据实际的计算需求进行动态扩缩容,如果采用这种动态空间的设计,则JVM实现可以提供给程序员配置动态上下界的方法;(比如HotSpot JVM程序,可以在Java程序启动参数上找到蛛丝马迹)
关于Java虚拟机栈的两种异常情况:
- 如果一个JVM线程在计算时需要更大的栈中间(所需的空间超出了被允许的大小),则JVM会抛出StackOverflowError
- 如果没有额外的空间来满足栈动态扩展或者一个新的JVM线程的创建和初始化,则JVM会抛出OutOfMemoryError
Native Method Stack
本地方法栈,本地方法是使用非Java语言编写的,用于实现Java和其他编程语言进行交互,但这并不是一个必须的模块,根据用户的实际情况而定;
- 如果JVM在实现时提供了本地方法栈,则本地方法栈随着线程的创建而创建;
- 如果JVM在实现时不允许加载本地方法,则JVM无需提供本地方法栈的实现;
本地方法栈空间大小的设计:
- 本地方法栈的大小可以是固定的,也可以是动态扩缩容的,JVM的实现可以提供给用户或者程序员配置本地方法栈大小和上下界的方法;
关于本地方法栈的异常情况:
- 针对空间固定大小的实现,如果一个线程在运行时需要本地方法栈的大小超出允许的大小,则JVM会抛出StackOverflowError
- 如果本地方法栈是可以进行动态扩容的,但是所需的额外空间无法满足或者初始化一个本地方法栈时没有足够的空间,则JVM会抛出OutOfMemoryError
JVM线程共享的运行时数据区
Heap
Heap,即堆,是所有JVM线程共享的一块内存空间,当JVM启动时创建,所有的对象实例和数组都会从堆上分配空间,对象占用的内存从来都不会显式地被释放,而是通过自动存储管理系统进行回收,也就是大名鼎鼎的GC(garbage collector);关于自动存储管理系统,JVM并没有假设任何类型的内存管理系统,而是让实现者根据自己的需求选择相应的内存管理技术;
JVM Heap的空间大小有两种设计:
- 堆内存不一定是连续的;
- 如果堆的大小是固定的,JVM在实现时可以为程序员或者用户提供配置堆空间大小的方法;
- 堆的大小可以根据实际情况进行动态扩缩容,JVM在实现时可以为程序员或者用户提供配置堆空间大小上下界的方法;(例如HotSpot JVM,但是一般线上使用时,上下界配置大小是一样的,目的也是避免JVM在运行时频繁的扩缩容带来的性能损耗)
关于堆异常的情况:
- 如果一次计算需要的内存空间超出自动内存管理系统所能申请的空间大小,则JVM会抛出OutOfMemoryError
Method Area
Method Area,即方法区,所有的JVM线程共享的一块内存区域,方法区的概念和操作系统进程中的代码段(text segment)有些相似,方法区存储了每个类的结构,比如运行时常量池、字段信息等,还有方法和构造器的指令代码;
方法区在JVM启动时创建,从逻辑上讲,方法区是堆的一部分,但是一种简单的实现是既不使用GC进行垃圾回收也不会对它进行压缩,JVM规范并没有指定要求方法区的位置和字节码指令的管理策略;
方法区的内存空间大小设计:
- 方法区的内存不一定是连续的;
- 方法区的大小可以是固定的,也可以是动态的扩缩容的,JVM在实现时可以提供程序员和用户配置方法区大小或者上下界的方法;
关于方法区的异常情况:
- 如果方法区的内存大小不能够满足一次内存分配的申请,则JVM会抛出OutOfMemoryError
Run-time constant pool
运行时常量池,是从方法区分配的一部分内存空间,是每个类或者接口对应的calss文件中的常量池表,常量池中保存了各种常量,包括数值字面量和字段引用,和传统编程语言中的符号表有点类似,
当一个类或者接口被创建时,运行时常量池也会被创建。
运行时常量池异常的情况:
- 当创建一个类或者一个接口时,如果运行时常量池所需的空间超出方法区可用空间,则JVM会抛出OutOfMemoryError
总结
本篇文章根据JVM8规范,总结了6种JVM运行时数据区,按照线程私有和全局共享,分成两类:
线程私有的运行时数据区:
- pc Register
- java虚拟机栈
- 本地方法栈(如果JVM允许加载本地方法的话)
线程共享的运行时数据区:
- 堆
- 方法区
- 运行时常量池
参考链接
jvm8s