Java内存模型包括: 程序计数器、虚拟机栈、本地方法栈、堆、方法区(元空间)、直接内存。重点理解线程私有和共享区域。
我们来系统地梳理和重点理解 Java 内存模型(JVM Runtime Data Area)的这几个核心组成部分。
这是一个非常核心的面试考点和性能调优基础。它的结构如下图所示:
一、线程私有区域 (Thread-Private)
这些区域的生命周期与线程相同,随线程的创建而创建,随线程的结束而销毁。每个线程都拥有自己独立的一份,因此不需要考虑并发问题。
1. 程序计数器 (Program Counter Register)
作用: 它可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时,就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
为什么是私有的: Java 支持多线程,是通过线程轮流切换、分配处理器执行时间的方式来实现的。在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储。
特点:
如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。
如果正在执行的是 Native 方法(本地方法,如 C++ 代码),这个计数器的值则为空(Undefined)。
此区域是唯一一个在《Java 虚拟机规范》中没有规定任何
OutOfMemoryError
情况的区域。
2. Java 虚拟机栈 (Java Virtual Machine Stack)
作用: 描述的是 Java 方法执行的线程内存模型。每个方法被执行时,Java 虚拟机都会同步创建一个栈帧(Stack Frame) 用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法从调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
为什么是私有的: 每个线程的调用链(执行了哪些方法)可能是不同的,必须独立存储。
核心组成部分 - 栈帧 (Stack Frame):
局部变量表 (Local Variable Table): 存放了编译期可知的各种基本数据类型(
boolean
,byte
,char
,short
,int
,float
,long
,double
)、对象引用(reference
类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress
类型(指向了一条字节码指令的地址)。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。(“大小”指变量槽的数量,对于64位的数据类型,可能会占用两个变量槽)。
异常:
StackOverflowError
: 如果线程请求的栈深度大于虚拟机所允许的深度(例如无限递归)。OutOfMemoryError
: 如果 Java 虚拟机栈容量可以动态扩展(大部分虚拟机都可动态扩展,但也允许固定长度的虚拟机栈),当栈扩展时无法申请到足够的内存时会抛出此异常。
3. 本地方法栈 (Native Method Stack)
作用: 与虚拟机栈所发挥的作用非常相似。
虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务。
本地方法栈则是为虚拟机使用到的本地(Native)方法(如 C/C++ 编写的方法)服务。
为什么是私有的: 原因同虚拟机栈。
异常: 与虚拟机栈一样,也会抛出
StackOverflowError
和OutOfMemoryError
。
注意: 在 HotSpot 虚拟机中,本地方法栈和虚拟机栈是合二为一的。因此对于 HotSpot,可以认为没有本地方法栈。
二、线程共享区域 (Thread-Shared)
这些区域被所有线程共享,是内存垃圾回收(Garbage Collection, GC)的重点关注区域,也是并发问题的高发地。
1. 堆 (Heap)
作用: 这是 JVM 中最大的一块内存区域。此内存区域的唯一目的就是存放对象实例和数组。“几乎”所有的对象实例都在这里分配内存(随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术会导致所有对象都分配到堆上这一说法变得不那么绝对)。
为什么是共享的: 所有创建出来的对象,本质上都是所有线程都可能访问到的。
GC 的主要战场: 由于对象生命周期不一,Java 堆是垃圾收集器管理的内存区域,因此也被称作 “GC 堆” (Garbage-Collected Heap)。
进一步划分: 从内存分配的角度看,线程共享的 Java 堆可能划分出多个线程私有的分配缓冲区 (TLAB - Thread Local Allocation Buffer),以提升对象分配时的效率。从垃圾回收的角度,现代收集器基本都基于分代收集理论,将堆划分为:
新生代 (Young Generation): 又分为 Eden 空间、From Survivor 空间、To Survivor 空间。
老年代 (Old/Tenured Generation)。
元数据区 (Metaspace): JDK8之后的方法区实现,但逻辑上不属于堆的一部分。
异常:
OutOfMemoryError
: 如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出此异常。
2. 方法区 (Method Area) / 元空间 (Metaspace)
作用: 用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
为什么是共享的: 这些类信息、常量、静态变量理所应当是所有线程共享的。
演进:
JDK8 之前: 方法区的实现被称为 “永久代” (PermGen),其垃圾回收由老年代收集器顺带处理,配置参数是
-XX:PermSize
和-XX:MaxPermSize
。JDK8 及之后: 永久代被完全移除,取而代之的是 “元空间” (Metaspace)。元空间不再使用 JVM 的内存,而是使用本地内存 (Native Memory)。这意味着只要操作系统有可用内存,它理论上可以一直扩展,配置参数变为
-XX:MetaspaceSize
和-XX:MaxMetaspaceSize
。
存储内容:
类型信息: 类的完整有效名、直接父类的完整有效名、类的修饰符、直接接口的一个有序列表等。
运行时常量池 (Runtime Constant Pool): 是方法区的一部分,存放编译期生成的各种字面量和符号引用。
静态变量 (Static Variables): 又称为类变量。
JIT 编译后的代码: 即时编译器 (Just-In-Time Compiler) 编译后的本地机器代码。
异常:
OutOfMemoryError
: 当方法区(元空间)无法满足内存分配需求时抛出。
三、直接内存 (Direct Memory)
地位: 并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。
作用: 在 JDK 1.4 中引入了 NIO(New Input/Output)类,引入了一种基于通道(Channel) 与缓冲区(Buffer) 的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里面的
DirectByteBuffer
对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为它避免了在 Java 堆和 Native 堆中来回复制数据。特点:
大小不受 Java 堆大小的限制,但受本机总内存大小及处理器寻址空间的限制。
虽然不属于 JVM 内存,但它的分配和回收同样可能导致
OutOfMemoryError
。配置 JVM 参数时(如
-Xmx
)需要考虑到直接内存的存在,以免各内存区域总和大于物理内存限制。
总结与对比
内存区域 | 线程私有/共享 | 作用 | 异常 |
---|---|---|---|
程序计数器 | 私有 | 存储当前线程执行的字节码行号指示器 | 无 |
虚拟机栈 | 私有 | 存储 Java 方法的栈帧(局部变量表、操作数栈等) | StackOverflowError OutOfMemoryError |
本地方法栈 | 私有 | 为 Native 方法服务 | StackOverflowError OutOfMemoryError |
堆 | 共享 | 存储对象实例和数组,GC 主战场 | OutOfMemoryError |
方法区/元空间 | 共享 | 存储类信息、常量、静态变量、JIT 代码等 | OutOfMemoryError |
直接内存 | (不属于 JVM) | NIO 使用的堆外内存,提高 I/O 性能 | OutOfMemoryError |
核心理解:
线程私有: 每个线程都有自己的“工作现场”,包括程序计数器(记录我执行到哪了)、虚拟机栈(记录我当前调用的一系列方法的状态)。这些数据其他线程不需要也不应该访问,因此私有化是安全且高效的。
线程共享: 堆和方法区存储的是所有线程都可能需要访问的“公共资源”,如对象实例和类定义。正因为共享,所以对它们的访问需要处理并发安全问题,也是垃圾回收器主要管理的区域。
希望这个详细的解释能帮助你彻底理解 JVM 的内存模型!