JavaEE 初阶第三十期:JVM,一次Full GC的架构级思考(上)

专栏:JavaEE初阶起飞计划

个人主页:手握风云

目录

[一、JVM 简介](#一、JVM 简介)

[二、JVM 的运行流程](#二、JVM 的运行流程)

[三、JVM 内存布局](#三、JVM 内存布局)

[3.1. 程序计数器](#3.1. 程序计数器)

[3.2. 方法区](#3.2. 方法区)

[3.3. 栈区](#3.3. 栈区)

[3.4. 堆区](#3.4. 堆区)


一、JVM 简介

JVM 是 Java Virtual Machine 的简称,中文意为 Java 虚拟机。在计算机科学中,虚拟机通常是指通过软件技术模拟出来的、具有完整硬件功能,并且运行在一个完全隔离的环境中的完整计算机系统。市面上常见的虚拟机产品包括 JVM、VMware 以及 Virtual Box 等。然而,JVM 与 VMware 或 Virtual Box 等传统虚拟机在底层实现原理上存在着本质的区别。VMware 和 VirtualBox 主要是通过软件手段来模拟物理 CPU 的指令集,因此在它们构建的虚拟物理系统中会存在大量的寄存器。相比之下,JVM 则是通过软件专门模拟 Java 字节码的指令集,在 JVM 的架构设计中主要只保留了 PC 寄存器,而其他的硬件寄存器都经过了高度的裁剪和优化。因此,可以将 JVM 理解为一台被特殊定制过、但在现实物理世界中并不真实存在的计算机。

作为 Java 技术体系的核心,JVM 构成了 Java 程序能够正常运行的基础环境,它同时也是 Java 语言能够实现"一次编译,到处执行"这一重要跨平台特性的关键所在。在程序的实际运行流程中,开发者编写的 Java 源代码并不会直接交由底层计算机执行,而是需要首先被编译器转换成 JVM 能够识别的字节码(即 .class 文件)。随后,JVM 内部机制开始运作,首先通过类加载器(ClassLoader)以特定的方式将这些字节码文件加载到内存的运行时数据区(Runtime Data Area)中进行存储。由于字节码文件仅仅是 JVM 内部定义的一套指令集规范,底层的操作系统是无法直接解析和运行这些指令的。因此,JVM 还需要依赖一个特定的命令解析器------也就是执行引擎(Execution Engine),由它负责将这些抽象的字节码精准地翻译成底层操作系统能够理解的机器指令,然后再交由物理机器的 CPU 去真正执行。在这个复杂的指令翻译和执行的过程中,程序往往还需要调用底层操作系统或其他编程语言提供的本地库接口(Native Interface)来协同实现整个应用程序的完整功能。

此外,从整个 Java 生态的规范约束来看,市面上存在多种由不同厂商研发的 JVM 产品,这些不同版本的 JVM 都可以看作是实现 JVM 产品的具体工程化成果。但无论这些产品的底层优化策略有多么不同,它们的最终实现都必须要严格符合《Java虚拟机规范》。《Java虚拟机规范》是由 Oracle 官方发布的、在 Java 领域最为重要和权威的著作,它完整且极为详细地描述了 JVM 应当具备的各个组成部分与标准机制。通过这套高度标准化的规范,JVM 不仅成功屏蔽了不同底层操作系统和硬件平台之间的差异,还为程序提供了一个具备高度一致性、安全机制以及自动内存垃圾回收等特性的强大运行环境。

二、JVM 的运行流程

JVM 是 Java 程序得以正常运行的底层基础平台,同时也是 Java 语言能够真正实现"一次编译到处执行"这一核心跨平台特性的关键所在 。在任何 Java 程序正式开始执行之前,开发者编写的 Java 源代码并不能被计算机直接运行,而是必须首先经过编译阶段,将这些具有较高抽象级别的代码转换成为 JVM 能够专门识别和处理的字节码(即生成的 .class 文件) 。

当字节码文件生成就绪后,JVM 运行流程便正式启动,其首先需要依赖一个特定的机制------也就是类加载器(ClassLoader),将这些存储于外部介质上的字节码文件安全且有序地加载到计算机的内存之中,并统一放置在被称为运行时数据区(Runtime Data Area)的核心结构里 。这些加载进来的字节码文件本质上是 JVM 自身定义的一套高度抽象的指令集规范,由于其与具体的物理底层硬件解耦,因此并不能直接被抛给底层的操作系统去执行 。

为了跨越这种指令集上的鸿沟,JVM 内部还需要配置一个极为关键的特定命令解析器,即执行引擎(Execution Engine) 。执行引擎承担着翻译官的重任,它负责将内存中抽象的字节码指令精准地翻译成当前底层计算机操作系统能够直接识别和驱动的机器物理系统指令,然后再将其交由硬件的 CPU 去真正运算与执行 。不仅如此,在这个复杂的指令翻译和整体程序的执行生命周期中,Java 程序往往还需要与外部的系统资源进行交互,这就需要调用其他编程语言编写的接口,即本地库接口(Native Interface),借此来实现整个应用程序完整且丰富的业务功能 。

总结来看,JVM 的运行流程是一个精密配合的系统工程,它主要通过明确分工的以下四个组成部分来共同执行复杂的 Java 程序 。这四个核心模块分别是:负责字节码装载的类加载器(ClassLoader) 、负责内存空间管理与数据调度的运行时数据区(Runtime Data Area) 、负责指令翻译与运转的执行引擎(Execution Engine) ,以及负责打通底层环境生态的本地库接口(Native Interface) 。正是这四个主要组成部分各自履行职责并紧密相连,才构筑了 JVM 强大且跨平台的运行功能 。

三、JVM 内存布局

区域类型 体区 核心作用 关键特性
线程共享 存储所有对象实例 分新生代(Eden+2*Survivor)、老年代;通过 - Xms/-Xmx 设置大小
方法区 存储类信息、常量、静态变量 JDK7 = 永久代;JDK8 = 元空间(本地内存);运行时常量池属于此区
线程私有 虚拟机栈 描述 Java 方法执行内存模型 每个方法对应一个栈帧;-Xss 设置栈容量;易抛 StackOverflowError
本地方法栈 服务 Native 方法 HotSpot 中与虚拟机栈合二为一
程序计数器 记录线程执行字节码行号 唯一无 OOM 的区域

3.1. 程序计数器

程序计数器是一块较小的内存空间,它的核心功能是作为当前线程所执行的字节码的行号指示器,用来精准记录当前线程执行的指令位置。由于 JVM 的多线程机制是通过线程轮流切换并分配处理器执行时间的方式来实现的,为了保证线程在发生切换后能够准确恢复到正确的执行位置,每条线程都需要拥有一个独立、私有的程序计数器,各条线程之间的计数器互不影响、独立存储。在具体的执行场景中,如果当前线程正在执行的是一个 Java 代码方法,这个计数器记录的便是正在执行的虚拟机字节码指令的内存地址;但如果正在执行的是一个底层调用的 Native(本地)方法,由于不由 JVM 直接执行,这个计数器的值则为空(未定义)。值得特别强调的是,程序计数器是整个 JVM 内存布局中唯一一个在《Java虚拟机规范》里没有规定任何 OutOfMemoryError(内存溢出)情况的内存区域。

3.2. 方法区

方法区是一块被所有线程共享的内存区域,它的主要作用是用来集中存储已经被 JVM 成功加载的类信息、常量、静态变量以及即时编译器(JIT)编译后的代码等关键全局数据。在《Java虚拟机规范》中它被称为"方法区",而在主流的 HotSpot 虚拟机的具体实现历程中,其底层架构经历过重要演变:在 JDK 7 及之前的版本中,这块区域通常被实现为永久代(PermGen);而到了 JDK 8 时代,则全面替换为了元空间(Metaspace)这一新的结构。这种底层实现的架构变更带来了显著的优势,对于 HotSpot 而言,JDK 8 中元空间的内存直接使用了操作系统的本地内存,这意味着元空间的大小不再严格受制于 JVM 设定的最大内存参数限制,而是主要与宿主物理机的可用本地内存大小挂钩,从而大大降低了类加载过多导致内存溢出的风险。此外,方法区中还包含了一个非常核心的组件------运行时常量池,专门用于存放编译期生成的各种字面量(如 final 常量、基本数据类型的值,不过在 JDK 8 中字符串常量池已被移动至堆中)和符号引用(如类的完全限定名、字段以及方法的名称与描述符)。

3.3. 栈区

栈区(主要包含 Java 虚拟机栈和本地方法栈)是线程绝对私有的内存区域,其生命周期与所属的线程保持高度一致,主要用于描述和管理方法执行时的内存交互模型。每当有一个方法被触发执行时,JVM 都会同步在虚拟机栈中创建一个栈帧(Stack Frame),栈帧是支持虚拟机进行方法调用和逻辑执行的核心数据结构,内部用于存放局部变量表、操作数栈、动态链接以及方法出口(返回地址)等关键上下文信息。其中,局部变量表负责安全存放编译器可知的八大基本数据类型以及各种对象的引用指针,其所需的内存空间在代码编译期间就已经被计算并分配完成,在整个方法运行期间,局部变量表的大小是完全确定且不会改变的。在使用中,如果单线程请求的调用栈深度超出了虚拟机所允许的最大深度(例如无限递归),会抛出 StackOverflowError 异常;而在支持动态扩展的虚拟机中,如果拓展栈时无法申请到足够的物理内存空间,则会引发 OutOfMemoryError 异常。

3.4. 堆区

堆区是 JVM 内存布局中占用空间最大的一块区域,也是被所有运行线程所共享的核心内存主阵地,它的唯一目的和最核心的作用就是用来存放程序运行中创建的几乎所有的对象实例。为了配合底层垃圾收集器(GC)进行更高效的内存管理,现代 JVM 根据对象存活周期的不同,将堆内存进一步逻辑划分为新生代(Young Generation)和老年代(Old Generation)两个主要部分。新生代主要用于容纳刚刚通过程序代码新建产生的对象,它内部又被更加细致地划分为一个占比较大的 Eden 区和两个较小且大小完全相等的 Survivor 区(常被标记为 S0 和 S1,或者 From 区和 To 区)。当新生代中的对象经过一定次数的 Minor GC 内存回收考验后仍然存活,或者当新创建的对象体积过大(大对象)时,它们就会被晋升并安全转移到老年代中进行长久保存。在日常的后端性能调优工作中,开发者经常使用的核心 JVM 启动参数,如 -Xms(设置堆空间的最小启动内存)和 -Xmx(设置堆空间的最大运行内存)正是直接针对堆区容量进行配置和限制的。当堆内存中不断有新对象被创建,并且由于存在 GC Roots 可达路径导致垃圾回收器无法清除这些无用对象时,随着对象数量急剧攀升达到最大堆容量上限,便会产生在实际项目应用中最常见、最具破坏性的内存溢出异常------ java.lang.OutOfMemoryError: Java heap space。

相关推荐
ch.ju2 小时前
Java程序设计第二章——java数据类型:字符 转义字符
java
辉博士2 小时前
Spring Boot+EasyExcel实现Excel文件
java·spring boot·excel
小松加哲2 小时前
MyBatis完整流程详解
java·开发语言·mybatis
码码哈哈0.02 小时前
Spring AI 1.0.0 + ChromaDB 最新版踩坑:Collection does not exist 404 报错全记录
java·人工智能·spring
开开心心就好2 小时前
操作简单的ISO文件编辑转换工具
java·前端·科技·edge·pdf·安全威胁分析·ddos
卷卷说风控2 小时前
工作流的 Skill 怎么写?
java·javascript·人工智能·chrome·安全
SunnyDays10112 小时前
Java实战指南:如何高效将PDF转换为高质量TIFF图片
java·pdf转tiff
Seven972 小时前
【从0到1构建一个ClaudeAgent】规划与协调-TodoWrite
java
Yeh2020582 小时前
maven
java·maven