深入理解 Java 虚拟机-01 JVM 内存模型

深入理解 Java 虚拟机-01 JVM 内存模型

Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域。根据《Java虚拟机规范》的规定,Java 虚拟机所管理的内存将会包括以下几个运行时数据区域:

程序计数器

在 Java 虚拟机的概念模型里,程序计数器存的是下一条需要执行的字节码指令的地址,由于 Java 的多线程是通过分配处理器执行时间的方式来实现,所以为了在切换线程时保留执行状态(执行到了哪个位置等),每个线程都有自己的程序计数器

如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)。此内存区域是唯 一一个在《Java虚拟机规范》中没有规定任何 OutOfMemoryError 情况的区域。

Java 虚拟机栈

Java 虚拟机栈也是线程私有的,每个方法被执行的时候,Java 虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

局部变量表存放了编译期可知的基本数据类型(boolean、byte、char、short、int、 float、long、double)、对象引用类型和 returnAddress 类型。

  • 对象引用类型:可能是一个指向对象起始地址的引用指针,也可能是一个代表对象的句柄,总之可以通过该值关联到对象。
  • returnAddress 类型:执行一条字节码指令的地址。

这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中 64 位长度的 long 和 double 类型的数据会占用两个变量槽,其余的数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小(大小指槽的个数)。

《Java虚拟机规范》中,对这个内存区域规定了两类异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError 异常;如果 Java 虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出 OutOfMemoryError 异常(HotSpot 虚拟机的栈容量是不可以动态扩展的)。

本地方法栈

Java 虚拟机栈是为虚拟机执行 Java 方法服务,本地方法栈则是为虚拟机执行使用到的本地(Native)方法服务。

有的 Java 虚拟机(譬如Hot-Spot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出 StackOverflowError 和 OutOfMemoryError 异常。

Java 堆是被所有线程共享的一块内存区域,"几乎"所有的对象实例都在这里分配内存。它是垃圾收集器管理的内存区域,因此一些资料中它也被称作 "GC堆"(Garbage Collected Heap)。

由于垃圾收集器大部分基于分代收集理论设计,所以 Java 堆中经常出现 "新生代"、"老年代"等名词,但这些区域划分仅仅是一部分垃圾收集器的共同特性或者说设计风格而已,而非某个 Java 虚拟机具体实现的固有内存布局,,更不是《Java虚拟机规范》里对 Java堆的进一步细致划分。

从分配内存的角度看,所有线程共享的 Java 堆中可以划分出多个线程私有的分配缓冲区 (Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率。因为堆是线程所共享的,因此在分配内存时需要考虑线程安全,如果每个线程都有一定的私有缓冲区,则只需在为线程分配私有缓冲区时加锁即可。

如果在 Java 堆中没有内存完成实例分配,并且堆也无法再扩展时(-Xmx),Java 虚拟机将会抛出 OutOfMemoryError 异常。

方法区

方法区(Method Area)与 Java 堆一样是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作"非堆"(Non-Heap),目的是与 Java 堆区分开来。

在 JDK8 以前,Java 程序员习惯称呼方法区为"永久代"(Permanent Generation)或将其混为一谈,但本质上这两者并不是等价的,只是因为当时的 HotSpot 虚拟机设计团队选择把收集器的分代设计扩展至方法区,或者说使用永久代来实现方法区,使得 HotSpot 的垃圾收集器能够像管理 Java 堆一样管理这部分内存,省去了为这个区域专门编写内存管理代码。但是对于其他虚拟机实现,譬如 BEA JRockit、IBM J9 等来说,是不存在永久代的概念的。

在收购了 BEA 后,为了移植 JRockit 中的优秀特性,在 JDK 6 的 时候 HotSpot 开发团队就有放弃永久代,逐步改为采用本地内存(Native Memory)来实现方法区的计划了。JDK 7 的 HotSpot,已经把原本放在永久代的字符串常量池、静态变量等移出,而到了 JDK 8,终于完全废弃了永久代的概念,改用与 JRockit、J9 一样在本地内存中实现的元空间(Meta space)来代替,把 JDK 7 中永久代还剩余的内容(主要是类型信息)全部移到元空间中。

相对而言,垃圾收集行为在这个区域的确是比较少出现的,但并非数据进入了方法区就如永久代的名字一样"永久"存在了。这区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收有时又确实是必要的。

运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只有编译期才能产生,也就是说,并非预置入 Class 文件中常量池的内容才能进入方法区运行时常 量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是 String 类的 intern() 方法。

《Java虚拟机规范》的规定,如果方法区(包括运行时常量池)无法满足新的内存分配需求时,将抛出 OutOfMemoryError 异常。

================================================================================

直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,但也会导致 OutOfMemoryError 的出现。

NIO(New Input/Output)类引入了一种基于通道(Channel)与缓冲区 (Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。

注意:直接内存不再受 Java 堆内存的限制,只受本机物理内存限制(物理大小和操作系统级限制),但如果设置 -Xmx 等信息时不注意,可能会导致各个内存区域和大于物理内存,从而导致 OutOfMemoryError 。

相关推荐
王家视频教程图书馆2 小时前
android java 开发网路请求库那个好用请列一个排行榜
android·java·开发语言
花卷HJ2 小时前
Android 文件工具类 FileUtils(超全封装版)
android·java
rchmin2 小时前
ThreadLocal内存泄漏机制解析
java·jvm·内存泄露
黎雁·泠崖2 小时前
Java 方法栈帧深度解析:从 JIT 汇编视角,打通 C 与 Java 底层逻辑
java·c语言·汇编
java资料站2 小时前
springBootAdmin(sba)
java
AscendKing2 小时前
接口设计模式的简介 优势和劣势
java
❀͜͡傀儡师2 小时前
Docker快速部署一个轻量级邮件发送 API 服务
jvm·docker·容器
Vincent_Vang2 小时前
多态 、抽象类、抽象类和具体类的区别、抽象方法和具体方法的区别 以及 重载和重写的相同和不同之处
java·开发语言·前端·ide
qualifying2 小时前
JavaEE——多线程(3)
java·开发语言·java-ee