很多 Java 初学者都会有一个疑问:

Java 程序运行时,数据到底存在哪里?

例如这段代码:

public class Test {
    public static void main(String[] args) {
        int a = 10;
        String str = "hello";
        User user = new User();
    }
}

代码中出现了三个变量:

astruser

当程序运行时,它们并不会随意放在某个地方,而是被 JVM 按照一定规则存储在不同的内存区域中。

理解这些内存区域,就是理解 JVM 内存模型

JVM 内存结构不仅是 Java 的基础知识,也是面试中非常高频的问题。


JVM 运行时内存结构

当 Java 程序启动后,JVM 会把内存划分成多个区域,每个区域承担不同的职责。

整体结构可以简单理解为:

JVM 内存结构

方法区 Method Area

堆 Heap

虚拟机栈 Stack

本地方法栈 Native Stack

程序计数器 PC Register

在这些区域中,最重要的三个部分是:

堆、方法区和虚拟机栈。


程序计数器(Program Counter Register)

程序计数器是一块非常小的内存空间,它的作用是记录当前线程执行到哪一条字节码指令。

可以把它理解为:程序执行位置的记录器。

在多线程环境下,CPU 会频繁进行线程切换。当线程重新获得 CPU 时间片时,程序计数器能够帮助它恢复到正确的执行位置继续运行。

程序计数器是线程私有的,并且是 JVM 中唯一不会发生 OutOfMemoryError 的区域。


虚拟机栈(Java Stack)

虚拟机栈同样是线程私有的内存区域,它主要用于支持方法调用。

每当一个方法被调用时,JVM 就会为这个方法创建一个 栈帧(Stack Frame),并压入栈中。

栈帧内部包含以下几个部分:

栈帧 Frame

局部变量表

操作数栈

动态链接

方法返回地址

当方法执行完成后,栈帧会从栈中弹出。

例如方法调用关系:

main() -> methodA()-> methodB()

对应的栈结构是:

methodB
methodA
main

方法执行结束时,栈会按照 先进后出 的顺序逐层弹出。

如果方法递归调用过深,就可能导致 StackOverflowError


堆(Heap)

堆是 JVM 中最大的一块内存区域,用于存储对象实例。

当代码中执行:

User user = new User();

User 对象就会被分配到堆内存中。

为了提升垃圾回收效率,堆通常会进一步划分为新生代和老年代。

Heap

Young Generation

Old Generation

Eden

Survivor S0

Survivor S1

新创建的对象通常会进入 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 在内部大致会经历如下过程:

new 指令

检查类是否加载

在堆中分配内存

初始化对象

返回对象引用

最终变量 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 之后,其实现由“永久代”变为“元空间”,并移到了本地内存中

在这里插入图片描述

Logo

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

更多推荐