JVM 之 运行时数据区内存模型【堆、方法区、运行时常量池、虚拟机栈、本地方法栈、程序计数器是如何协作的?他们各自起什么作用】

27 阅读7分钟

JVM运行时数据区内存模型

在这里插入图片描述

场景类比

想象一个Java程序启动一个Web服务器,处理用户请求的场景,这就像一家繁忙的餐厅:

  • 堆‌:后厨的食材仓库,所有待处理的食材(对象)都存放在这里,由厨师(GC)管理食材的进出和过期清理。
  • 方法区‌:餐厅的菜谱库,存储了所有菜品的制作方法(类信息),所有厨师共享这个库。
  • 运行时常量池‌:菜谱中的特殊食材清单,记录了哪些食材是固定不变的(如“盐少许”),确保每道菜口味一致。
  • 虚拟机栈‌:厨师的工作台,每个厨师(线程)都有自己的工作台,上面放着当前正在处理的食材(局部变量)和工具(操作数栈)。
  • 本地方法栈‌:厨师处理特殊食材(如进口调料)的专用工作台,用于执行非Java语言(如C++)编写的功能。
  • 程序计数器‌:厨师的记事本,记录当前正在做哪道菜的哪一步,确保切换任务时能快速回到正确位置。

协作流程

  • 1、当用户发起请求时,
    • 服务器线程(厨师)从(仓库)取出食材,
    • 参照方法区(菜谱)的步骤,
    • 虚拟机栈(工作台)上处理,
    • 遇到特殊操作(如加密)则通过本地方法栈(专用工作台)完成,
    • 程序计数器(记事本)始终记录进度,
  • 最终确保高效准确地完成每一道“菜”(请求)。

程序计数器 (线程私有)

  • 1、线程私有(一块较小的内存)
  • 2、记录当前线程执行到了哪一行字节码。

程序计数器是什么?

说白了就是记录线程执行哪了。

程序计数器解决了什么问题?

java虚拟机执行时,并不是单线程顺序执行下去,而是多个线程之间轮流切换,分配处理器执行时间的方式实现的。 所以这自然就引发了一个问题 -> 线程切换回来后,如何继续之前的点执行? 程序计数器就是解决的这个问题。

程序计数器是怎么解决的?

每条线程独立维护一个属于自己的程序计数器。 java虚拟机概念模型中,字节码解释器工作时:先改变计数器的值,然后根据值确定下一步执行什么字节码指令。 所以在线程切换回来后,程序执行流程中的“分支、循环、跳转、异常处理、线程恢复等”自然可以基于其保证。

Java虚拟机栈 (线程私有)

  • 线程私有(生命周期与线程相同,随线程启动而创建,随线程销毁而销毁)
  • java方法执行时的线程内存模型

虚拟机栈是什么?

java方法执行时的线程内存模型

虚拟机栈解决了什么问题?怎么解决的?

  • 1、方法调用管理:通过入栈/出栈操作,确保方法执行的顺序和状态。
  • 2、数据隔离每个方法创建一个栈帧,避免了不同方法间的数据冲突。
  • 3、动态链接支持:简单讲就是支持使用反射(通过路径反射到具体内存地址的方法连接)。

虚拟机栈执行流程

  • 1、线程创建,虚拟机栈创建
  • 2、当java方法执行时,java虚拟机会同步创建一个栈帧。
    • 栈帧存储着:局部变量表、操作数栈、动态连接、方法出口信息
      • 局部变量表:存储方法数据
      • 操作数栈:处理计算过程
      • 动态链接:支持方法调用
      • 方法出口信息:管理返回机制
  • 3、当线程销毁时,虚拟机栈随之销毁

异常状况:

栈深度超过限制,将抛出StackOverflowError异常;(hotspotz虚拟机不会动态扩展,所以这里不会OOM)

虚拟机栈帧(局部变量表、操作数栈、动态链接、方法出口信息)各部分协同工作原理

  • 栈帧存储着:局部变量表、操作数栈、动态连接、方法出口信息
    • 局部变量表:存储方法数据
    • 操作数栈:处理计算过程
    • 动态链接:支持方法调用
    • 方法出口信息:管理返回机制
1. 局部变量表(Local Variable Table)
  • 1、作用:存储编译时确定的方法参数和局部变量(如int a = 10、String str = "hello")
    • 基本数据类型(boolean、byte、char、short、int、float、long、double)
    • 对象引用(reference类型,指向对象起始地址\指向代表对象的句柄\其他相关地址)
    • returnAddress(指向一条字节码指令的地址)
  • 2、存储机制:以变量槽(Slot,32位)为单位
    • 32位类型(如int)占用1个Slot,
    • 64位类型(如long、double)占用2个Slot。
  • 3、生命周期:方法执行时创建,方法结束时销毁,线程私有。
2. 操作数栈(Operand Stack)
  • 1、作用:存储计算过程中的临时结果(如a + b的中间值)。
  • 2、工作原理:后入先出(LIFO)栈结构(通过push/pop操作管理数据)。
  • 3、深度限制:编译时确定,通过max_stacks属性保存。
3. 动态链接(Dynamic Linking)
  • 1、作用:将符号引用(如方法名)转换为直接引用(如内存地址),支持方法调用。
  • 2、实现机制:通过运行时常量池中的引用实现,分为静态解析和动态连接
  • 3、生活场景类比:类似电话簿,将方法名映射到实际执行地址。
4. 方法出口信息(Return Address)
  • 1、作用:记录方法执行完毕后的返回地址,支持方法调用的返回机制。
  • 2、工作原理:方法返回时,栈帧将结果传递给前一个栈帧,当前栈帧出栈。
  • 3、生活类比:类似航班登机牌,记录返回航班的登机口。

本地方法栈 (线程私有)

与虚拟机栈所发挥的作用是非常相似的, 区别:虚拟机栈 -> Java方法(也就是字节码)服务; 本地方法栈 -> 本地(Native)方法服务。

Java堆 (线程共享)

  • 1、JVM内存管理中最大的区域(主要用于存储对象实例和数组)。
  • 2、线程共享的内存区域,
  • 3、由垃圾回收器(GC)自动管理。 注意: Java世界里“几乎”所有的对象实例都在这里分配内存,但随着逃逸分析技术的日渐强大,栈上分配、标量替换等优化手段,导致Java对象实例也不都是分配在堆上了

堆解决了什么问题?

  • 1、对象动态分配:运行时通过new -> 创建的对象动态的在堆中分配内存
  • 2、线程共享:实现对象的跨线程访问(需注意线程安全)。
  • 3、内存回收:GC自动回收不再被引用的对象(避免内存泄漏和溢出)。

堆解决了什么问题?

  • 1、分代收集:堆分为新生代和老年代,通过Eden区和Survivor区实现对象的生命周期管理。
  • 2、垃圾回收:标记-清除复制算法。
  • 3、内存分配:指针移动、空闲链表 实现高效内存分配。 这一部分是jvm的重点,后续会专门讲解

方法区(线程共享)

方法区是一个独立的内存区域(线程共享),存储类信息、常量、静态变量等元数据。

方法区解决了什么问题?怎么解决的?

线程共享:所有线程共享方法区(确保类信息全局一致,避免了类重复加载)

异常情况:

加载过多类,引发OOM,可通过参数(-XX:MetaspaceSize)调优

运行时常量池(线程共享)

在类加载后,编译期中生成的各种字面量与符号引用奖存放到运行时常量池中。

解决了什么问题?怎么解决的?

  • 内存共享‌问题:通过共享相同常量节省内存
  • 执行效率‌问题:通过直接引用快速定位常量提升效率。

直接内存

JDK 1.4的NIO(New Input/Output)引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式, 它可以使用Native函数库直接分配堆外内存,通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。 这块不受Java堆大小的限制,但也可能导致OutOfMemoryError异常出现。