JVM 内存模型详解:Java 程序到底是如何运行的?
很多 Java 初学者都会有一个疑问:
Java 程序运行时,数据到底存在哪里?
例如这段代码:
public class Test {
public static void main(String[] args) {
int a = 10;
String str = "hello";
User user = new User();
}
}
代码中出现了三个变量:
a、str、user。
当程序运行时,它们并不会随意放在某个地方,而是被 JVM 按照一定规则存储在不同的内存区域中。
理解这些内存区域,就是理解 JVM 内存模型。
JVM 内存结构不仅是 Java 的基础知识,也是面试中非常高频的问题。
JVM 运行时内存结构
当 Java 程序启动后,JVM 会把内存划分成多个区域,每个区域承担不同的职责。
整体结构可以简单理解为:
在这些区域中,最重要的三个部分是:
堆、方法区和虚拟机栈。
程序计数器(Program Counter Register)
程序计数器是一块非常小的内存空间,它的作用是记录当前线程执行到哪一条字节码指令。
可以把它理解为:程序执行位置的记录器。
在多线程环境下,CPU 会频繁进行线程切换。当线程重新获得 CPU 时间片时,程序计数器能够帮助它恢复到正确的执行位置继续运行。
程序计数器是线程私有的,并且是 JVM 中唯一不会发生 OutOfMemoryError 的区域。
虚拟机栈(Java Stack)
虚拟机栈同样是线程私有的内存区域,它主要用于支持方法调用。
每当一个方法被调用时,JVM 就会为这个方法创建一个 栈帧(Stack Frame),并压入栈中。
栈帧内部包含以下几个部分:
当方法执行完成后,栈帧会从栈中弹出。
例如方法调用关系:
main() -> methodA()-> methodB()
对应的栈结构是:
methodB
methodA
main
方法执行结束时,栈会按照 先进后出 的顺序逐层弹出。
如果方法递归调用过深,就可能导致 StackOverflowError。
堆(Heap)
堆是 JVM 中最大的一块内存区域,用于存储对象实例。
当代码中执行:
User user = new User();
User 对象就会被分配到堆内存中。
为了提升垃圾回收效率,堆通常会进一步划分为新生代和老年代。
新创建的对象通常会进入 Eden 区。当 Eden 区空间不足时,会触发 Minor GC。
经过多次垃圾回收仍然存活的对象,会被移动到老年代。
当老年代空间不足时,就可能触发 Full GC。
方法区(Method Area)
方法区用于存储类相关的信息,例如:
类的结构信息、常量、静态变量以及方法代码等。
例如下面的代码:
public class User {
static int age = 20;
}
静态变量 age 就会存储在方法区中。
在 Java 8 之后,方法区的实现被替换为 Metaspace(元空间)。
元空间与之前的永久代不同,它使用的是本地内存,而不是 JVM 堆内存。
这样可以减少因为类加载过多而导致的内存溢出问题。
本地方法栈
本地方法栈主要用于执行 Native 方法。
所谓 Native 方法,就是由 C 或 C++ 实现的方法。
例如一些底层操作,例如文件系统或操作系统接口,往往需要通过 Native 方法实现。
当 Java 调用这些底层功能时,就会使用本地方法栈。
对象创建过程
当执行:
User user = new User();
JVM 在内部大致会经历如下过程:
最终变量 user 会保存一个对象引用,该引用指向堆中的对象实例。
常见的 JVM 内存异常
理解 JVM 内存结构之后,就能更容易理解常见的内存问题。
当方法递归调用过深时,栈帧会不断增加,最终可能导致栈空间耗尽,从而出现 StackOverflowError。
而当程序不断创建对象且没有及时释放时,堆空间可能被耗尽,这时 JVM 就会抛出 OutOfMemoryError。
JVM 内存结构总结
JVM 运行时内存可以简单分为两类:线程私有区域和线程共享区域。
线程私有区域包括程序计数器、虚拟机栈和本地方法栈。这些区域在每个线程中都是独立存在的。
线程共享区域主要包括堆和方法区,它们在整个 JVM 中只有一份,所有线程都可以访问。
如果用一句话总结 JVM 内存结构,可以这样记:
线程私有的是 PC + 栈,线程共享的是 堆 + 方法区。
面试常见问题
面试中经常会问到 JVM 内存结构的问题,例如:
1. JVM 的运行时内存区域有哪些?
根据 Java 虚拟机规范,运行时数据区主要分为以下五个部分:
- 程序计数器: 线程私有。记录当前线程执行的字节码指令地址,是唯一不会发生
OutOfMemoryError的区域。 - Java 虚拟机栈: 线程私有。描述方法执行的内存模型,每个方法执行时会创建一个“栈帧”,存放局部变量表、操作数栈等。
- 本地方法栈: 线程私有。与虚拟机栈类似,不同的是它为 JVM 使用到的 Native 方法(如 C/C++ 编写的底层库)服务。
- Java 堆 : 线程共享。JVM 管理的最大一块内存,几乎所有的对象实例及数组都在这里分配内存。也是垃圾收集器(GC)管理的主要区域。
- 方法区: 线程共享。用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等。
2. 堆为什么是线程共享的?
堆之所以设计为线程共享,主要出于以下两个核心目的:
- 对象共享与通信: Java 是一门面向对象的语言。不同线程往往需要访问同一个对象(例如单例对象、全局配置、共享缓存)。如果堆是私有的,线程间传递大数据对象将涉及大量的内存拷贝,效率极低。
- 节约内存空间: 绝大多数对象生命周期较长或被多个逻辑复用。共享堆可以确保同一份数据只占用一份内存,避免了每个线程都冗余存储相同对象导致的资源浪费。
虽然堆是共享的,但为了解决分配内存时的竞争问题,JVM 使用了 TLAB (Thread Local Allocation Buffer)。即在堆中为每个线程预先分配一小块私有缓冲区,提高对象分配的效率。
3. 方法区和堆有什么区别?
这两者虽然都是线程共享的,但在职能上有本质区别:
| 特性 | Java 堆 | 方法区 |
|---|---|---|
| 存储内容 | 存对象实例(“是什么样子的个体”) | 存类结构、常量、静态变量(“是什么类型的模版”) |
| 回收频率 | 非常频繁(Minor GC / Full GC) | 频率极低(主要针对常量池回收和类卸载) |
| 生还逻辑 | 存放生命周期短或动态产生的实例 | 存放相对固定、全局共享的元数据 |
| 实现演变 | 逻辑与物理上一直相对独立 | Java 8 之后,其实现由“永久代”变为“元空间”,并移到了本地内存中 |

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)