JVM内存模型简介

公众号二维码 公众号:逻魔代码

Java 程序在运行过程中,JVM 会将其管理的内存划分为几个不同的数据区域。这些数据区域各有用途,并且具备不同的创建和销毁时间。

JVM 运行时数据区主要分为五个部分:方法区、堆、虚拟机栈、本地方法栈、程序计数器。需要注意的是前两个部分是由所有线程共享的数据区,后面三个部分则是线程隔离的数据区。如下图所示。

JVM运行时数据区

程序计数器

程序计数器是一块很小的内存,可以看作是当前线程所执行的字节码的行号指示器。程序中的分支、循环、跳转、异常、线程恢复等都依赖于计数器的改变来完成。

因为每个线程是独立运行,并且,Java 中的多线程是通过对于处理器核心按照时间片轮转来切换的,所以每个线程都应该有一个独立的计数器,所以这块区域属于『线程隔离』的区域。

JVM 执行 Java 方法时,计数器记录的是正在执行的字节码指令地址;执行本地方法时,计数器值为空(Undefined)。

虚拟机规范未规定这块区域的OOME。

虚拟机栈

虚拟机栈为『线程隔离』区域,每个线程都有独立的虚拟机栈。Java 方法在执行时,虚拟机栈为每个方法创建一个栈帧,用于存储方法局部变量、操作数栈、动态链接、方法出口等信息。

虚拟机规范为这块区域规定了两种异常:

  • 线程请求的栈深度大于虚拟机允许的深度,抛出 SOE 异常。
  • 虚拟机栈动态扩展时无法申请到足够的内存,抛出 OOME 异常。

本地方法栈

类似于虚拟机栈,区别在于本地方法栈为执行本地方法服务。

堆是 JVM 所管理内存中最大的一块,属于『线程共享』区域,在虚拟机启动时创建。堆唯一的用途在于存放实例对象。

我们在说到垃圾收集时,大部分的情况提及的都是对堆内存的回收。

虚拟机规范为这块区域规范了一种异常:

  • 堆中没有足够的内存用于对象分配且无法再扩展时,抛出 OOME 异常。

方法区

方法区和堆一样,属于『线程共享』区域,用于存放虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,常说的『永久代』数据区就在这里。

虚拟机规范为这块区域规定了一种异常:

  • 方法区无法满足内存分配需求时,抛出 OOME 异常。

直接内存

直接内存不属于虚拟机运行时数据区的任何一部分。

NIO 中,可以直接通过本地函数库直接分配对外内存,然后通过保存在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用来操作,避免了在 Java 堆和 Native 堆中来回复制数据,从而提高性能。

OOME 发生时,也可能是由于直接内存与JVM占用内存的加和超出了物理内存的限制。