我们在讨论java语言的内存问题时经常会听到一个词叫“JVM内存模型”,这个词在实际使用中容易产生歧义,因为它通常可能指代两个密切相关但不同的概念:
Java内存模型 (Java Memory Model, JMM):这是一个并发概念,定义了Java线程如何通过内存进行交互,特别是关于共享变量的可见性、顺序性和原子性。它规范了线程、主内存和工作内存之间的关系,是
volatile、synchronized、final等关键字语义的理论基础。JVM内存结构(JVM Memory Structure):这是JVM在执行程序时使用的内存区域的逻辑划分,也就是我们常说的“堆”、“栈”等。
大多数人日常所说的“JVM内存模型”指的是第一个——JVM内存结构,即JVM在运行时如何划分和管理其内存空间。而Java内存模型是一个更偏底层的、关于并发编程的规范。
下面我为你详细解释这两者的区别。
一、JVM内存结构(运行时数据区)
这是指JVM在执行Java程序过程中会把它管理的内存划分为若干个不同的数据区域。这些区域各有用途,创建和销毁的时间也不同。
根据《Java虚拟机规范》,主要分为以下几个区域:
1. 程序计数器
作用:可以看作是当前线程所执行的字节码的行号指示器。字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。
特点:
线程私有:每个线程都有自己独立的程序计数器。
唯一一个在JVM规范中没有规定任何
OutOfMemoryError情况的区域。
2. Java虚拟机栈
作用:描述Java方法执行的内存模型。每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。方法的调用和完成对应着栈帧在虚拟机栈中的入栈和出栈。
存储内容:主要存放编译期可知的各种基本数据类型(boolean, byte, char, short, int, float, long, double)、对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。
异常:
StackOverflowError:如果线程请求的栈深度大于虚拟机所允许的深度。OutOfMemoryError:如果虚拟机栈可以动态扩展,但扩展时无法申请到足够内存。
特点:线程私有,生命周期与线程相同。
3. 本地方法栈
作用:与虚拟机栈非常相似。其区别在于:虚拟机栈为执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务(如用C/C++编写的方法)。
异常:与虚拟机栈一样,也会抛出
StackOverflowError和OutOfMemoryError。
4. Java堆
作用:是JVM所管理的内存中最大的一块。此内存区域的唯一目的就是存放对象实例。几乎所有的对象实例以及数组都在这里分配内存。
特点:
所有线程共享。
是垃圾收集器管理的主要区域,因此也被称作“GC堆”。
从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区,以提升对象分配时的效率。
分代:为了更好的回收内存,或者更快的分配内存,堆空间还可以细分为:
新生代(Young Generation):新创建的对象首先在这里分配。分为Eden区和两个Survivor区(S0, S1)。
老年代(Old Generation/Tenured):在新生代中经历多次GC后仍然存活的对象会被移到这里。
元空间(Metaspace) (Java 8+) /永久代(PermGen) (Java 7及以前):用于存储类的元数据信息。注意:从Java 8开始,元空间使用的是本地内存,不再属于Java堆的一部分。
异常:如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,会抛出
OutOfMemoryError。
5. 方法区
作用:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
演进:
在Java 7及之前,方法区被实现为永久代,是堆的一个逻辑部分。
在Java 8及之后,JVM移除了永久代,引入了元空间作为方法区的实现。元空间不再使用JVM内存,而是使用本地内存。这大大降低了OutOfMemoryError的风险。
运行时常量池:是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。
二、Java内存模型(JMM)
这是一个概念性的模型,它是一个协议或规范,用来定义程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中取出变量这样的底层细节。
JMM的核心目标是解决在并发环境下,由于多线程访问同一共享数据所引发的数据不一致性、重排序等问题。
JMM的关键概念:
主内存与工作内存:
主内存:存储所有共享变量。对应物理硬件的主内存。
工作内存:每个线程都有自己的工作内存,其中保存了该线程使用到的变量的主内存副本。线程对所有变量的操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。工作内存对应CPU的缓存和寄存器。
内存间交互操作:
JMM定义了8种原子操作来规范主内存和工作内存之间的交互,如:
lock(锁定)、unlock(解锁)、read(读取)、load(载入)、use(使用)、assign(赋值)、store(存储)、write(写入)。
并发三大问题:
原子性:JMM保证了
read、load、assign、use、store、write这六个操作是原子的,可以大致认为基本数据类型的访问读写是具备原子性的。如果需要更大范围的原子性,可以使用synchronized关键字或Lock。可见性:当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。JMM通过在变量修改后将新值同步回主内存,在变量读取前从主内存重新读取值来实现可见性。关键字
volatile、synchronized和final都能保证可见性。有序性:在本线程内观察,所有操作都是有序的(“线程内表现为串行的语义”);但在另一个线程中观察,这些操作可能是无序的(“指令重排序”现象)。
volatile和synchronized关键字可以保证线程之间操作的有序性。
总结
| 特性 | JVM内存结构(运行时数据区) | Java内存模型(JMM) |
|---|---|---|
| 关注点 | 内存空间的划分和管理 | 并发环境下,共享变量的访问规则和可见性 |
| 内容 | 堆、栈、方法区、程序计数器等 | 主内存、工作内存、内存交互操作、happens-before原则等 |
| 解决的问题 | 对象创建、内存分配、垃圾回收 | 多线程下的原子性、可见性、有序性问题 |
| 比喻 | 仓库的物理布局(哪里放家具,哪里放食品) | 仓库的存取管理规定(如何登记、何时可取、如何保证记录一致) |
所以,当有人问你“JVM内存模型”时,你需要根据上下文判断他指的是物理上的内存布局(JVM内存结构),还是并发相关的抽象模型(Java内存模型)。在大多数关于内存溢出、GC的讨论中,指的是前者;而在讨论多线程、volatile、synchronized时,通常指的是后者。