JVM内存区域

话不多说,先上图:

蓝框部分即为JVM,你可以发现JVM大致分为5个区域,即:堆、方法区(又称为non-heap),虚拟机栈,本地方法栈和程序计数器。但是它仍然不是最清晰的,下面给出的是两个版本JVM结构的对比:

按照线程运行的顺序,我们先从程序计数器开始讲解:

程序计数器

程序计数器可以看作是当前线程所执行的字节码的行号指示器。它通过标示下一条需要执行的字节码指令完成指令切换,可以说一个线程的运行就是在该计数器的不断变化推动下一步一步完成的。关于程序计数器的几点总结:

它是一块很小的内存空间,几乎可以忽略不计。也是运行速度最快的存储区域。

在 JVM 规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期一致

它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成

字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令

任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。如果当前线程正在执行的是Java 方法,程序计数器记录的是 JVM 字节码指令地址,如果是执行 native 方法,则是未指定值(undefined)

它是唯一一个在 JVM 规范中没有规定任何 OutofMemoryError 情况的区域。

虚拟机栈和本地方法栈

Java 虚拟机栈来描述 Java 方法的内存模型。每当有新线程启动时就会分配一个栈空间,线程结束后栈空间被回收,栈与线程拥有相同的生命周期。栈中元素用于支持虚拟机进行方法调用,每个方法在执行时都会创建一个栈帧存储方法的局部变量表、操作数栈、动态链接和方法出口等信息。每个方法从调用到执行完成,就是栈帧从入栈到出栈的过程。

有两类异常:

① 线程请求的栈深度大于虚拟机允许的深度抛出 StackOverflowError。

② 如果 JVM 栈容量可以动态扩展,栈扩展无法申请足够内存抛出 OutOfMemoryError(HotSpot 不可动态扩展,不存在此问题)。

本地方法栈与虚拟机栈作用相似,不同的是虚拟机栈为虚拟机执行 Java 方法服务,本地方法栈为本地方法服务。调用本地方法时虚拟机栈保持不变,动态链接并直接调用指定本地方法虚拟机规范对本地方法栈中方法的语言与数据结构无强制规定,虚拟机可自由实现,例如 HotSpot 将虚拟机栈和本地方法栈合二为一。

本地方法栈在栈深度异常和栈扩展失败时分别抛出 StackOverflowError 和 OutOfMemoryError。

方法区、运行时常量池、元空间

重点:方法区只是一个逻辑部分,实际上不存在。

这张图能很好看清它们之间的关系:

Java堆

Java堆是被所有线程共享的一块内存区域,“几乎”所有的对象实例都在这里分配内存。Java堆也是垃圾收集器管理的内存区域,以G1收集器的出现为分界,往前的收集器基本是采用分代收集理论进行设计,所以“新生代””老年代””永久代”Eden空间””From Survivor空 间”To Survivor空间”等概念都是分代设计下的产物,后面会介绍。垃圾分代的唯一目的就是优化GC性能。

Java 虚拟机规范规定,Java 堆可以是处于物理上不连续的内存空间中,只要逻辑上是连续的即可,像磁盘空间一样。实现时,既可以是固定大小,也可以是可扩展的,主流虚拟机都是可扩展的(通过-Xmx 和-Xms 控制),如果堆中没有内存完成实例分配,并且堆无法再扩展时,就会抛出OutofMemoryError 异常。

不放在堆中的数据:比如基本类型(放在栈中)和直接内存,还有final static 修饰的常量应该在方法区。

本地内存和直接内存

本地内存是一个宽泛的概念,可以理解为”Java堆外的所有内存使用”。

元空间(Metaspace)

元空间是本地内存的一个子集,具有特定用途:

  • Java 8引入,替代了早期JVM中的永久代(PermGen)
  • 专门用于存储类的元数据信息(类的结构、方法信息、字段信息等)
  • 位于本地内存中,而不是Java堆内
  • 默认情况下可以动态增长到操作系统允许的上限
  • 可通过参数-XX:MaxMetaspaceSize限制其大小

直接内存(Direct Memory)

直接内存也是本地内存的一个子集,但用途与元空间完全不同:

  • 通过Java NIO库的ByteBuffer.allocateDirect()创建
  • 位于Java堆外,但被Java代码引用和管理
  • 直接被操作系统访问,减少了Java堆和本地内存之间的数据复制
  • 特别适合I/O操作,如文件和网络传输
  • 可通过参数-XX:MaxDirectMemorySize限制其大小

关系如下:

1
2
3
4
5
6
7
8
9
10
11
操作系统内存
└── JVM进程内存
├── Java堆内存 (受-Xmx控制)
│ └── (对象实例存储区域)

└── 本地内存 (Native Memory)
├── 元空间 (Metaspace) - 存储类元数据
├── 直接内存 (Direct Memory) - NIO操作
├── 线程栈
├── 代码缓存
└── 其他JVM内部结构

形象化举例:

员工(线程)的完整工作流程

员工(线程)在工作过程中:

  1. 查看自己的步骤提示器(程序计数器)
    • 每位员工都有一个小型电子设备,显示当前应该执行的步骤编号
    • 当员工被店长叫去做其他事情时,设备会保存当前的步骤编号
    • 返回工作时,员工查看设备,立即知道应该从哪一步继续
    • 这确保了在多任务切换中不会混淆当前进度
  2. 查看配方墙(方法区)获取制作步骤
    • 根据步骤提示器指示的编号,去配方墙查找相应的操作指南
    • Java 7中,这些配方直接挂在店内墙上(永久代)
    • Java 8后,配方被移到了店外专门的技术资料室(元空间)
  3. 参考信息板(运行时常量池)获取标准数值和文本
    • 墙上的数字参考表和术语解释表提供精确数值和标准描述
    • “倒入90ml水”中的”90ml”和”水”都来自这个信息板
    • 多个员工可以同时参考这一信息板
  4. 在自己的工作台(栈)上执行具体操作
    • 每位员工在自己的工作台上放置工具和临时材料
    • 按照配方指示,使用正确的工具和计量单位
    • 工作台上还有做工作笔记的纸(局部变量)
  5. 使用公共区域的原料(堆)制作咖啡
    • 从公共存储区获取咖啡豆、牛奶、糖等原料
    • 这些原料被所有员工共享使用
    • 使用完的包装由清洁工(垃圾收集器)定期清理

元空间的具体应用

在Java 8引入的改革中:

  • 外部技术资料室(元空间)

    • 咖啡店不再把所有配方和制作技术挂在内部墙上(永久代),而是建立了专门的外部技术资料室
    • 员工需要某个特殊咖啡的配方时,会到资料室查询
    • 这个资料室使用的是店铺之外的空间(操作系统的本地内存),不占用店内空间
    • 资料室可以根据需要扩展,存放更多种类的咖啡配方(类信息)
    • 当新的咖啡品种推出时,相关配方会被添加到这个资料室

一个完整的工作场景

小明(一个线程)接到制作拿铁的任务:

  1. 小明查看自己的步骤提示器(程序计数器),显示应该从”准备原料”开始
  2. 小明前往外部技术资料室(元空间),查阅拿铁咖啡的标准配方
  3. 他注意到配方上写着”使用阿拉比卡咖啡豆”和”加入65°C温度的牛奶”,这些具体数值和名称来自信息板(运行时常量池)
  4. 小明回到自己的工作台(栈),准备了拿铁专用杯和计量工具
  5. 他前往公共原料区(堆),取出需要的咖啡豆和牛奶
  6. 按照步骤制作咖啡,每完成一步,步骤提示器(程序计数器)就自动更新到下一步
  7. 当店长临时叫小明去接待顾客时,步骤提示器记录了他停在”打奶泡”这一步
  8. 服务完顾客后,小明回来查看提示器,直接从”打奶泡”步骤继续,不需要重新开始

这个完善后的例子展示了JVM中线程工作时涉及的所有主要内存区域,包括程序计数器的作用和Java 8后引入的元空间概念。