JVM 运行时内存区域如何划分的?


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书

  • 《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于 Spring AI + Spring Boot 3.x + JDK 21...查看介绍

  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...查看介绍;演示链接:http://116.62.199.48:7070/

  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/

  • 新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍

截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. 基础掌握度:面试官不仅仅是想知道你能不能背出几个区域名称,更是想知道你是否清楚每个区域的作用、存储的内容,以及它们之间的协作关系。纯背八股文和真正理解的区别,面试官一追问就能看出来。

  2. 线程安全意识:考察你是否区分了线程私有区域和线程共享区域,这直接关系到你对并发问题的理解深度。

  3. 版本演进认知:JDK 8 对内存区域做了一次大调整(永久代 → 元空间),面试官想看你是否关注过这些变化,而不是停留在老版本的知识上。

核心答案

JVM 运行时内存区域按 线程归属 划分为两大类:

分类 区域 线程归属 存储内容 异常类型
线程私有 程序计数器 线程私有 当前执行的字节码指令地址 唯一不会 OOM
线程私有 虚拟机栈 线程私有 栈帧(局部变量表、操作数栈等) StackOverflowError / OOM
线程私有 本地方法栈 线程私有 Native 方法的调用信息 StackOverflowError / OOM
线程共享 线程共享 对象实例、数组 OutOfMemoryError
线程共享 方法区(元空间) 线程共享 类信息、常量、静态变量 OutOfMemoryError

还有个常被忽略的 "编外人员"——直接内存,它不属于 JVM 规范定义的运行时数据区,但 NIODirectByteBuffer 会用到,也可能导致 OOM。

深度解析

一、全局内存布局

先上一张全景图,把 JVM 内存区域的整体结构看清楚:

上图展示了 JVM 运行时内存区域的完整布局,整体分为 线程私有线程共享 两大部分。关键点在于:

  • 左侧线程私有区域:每个线程都有自己独立的一份,包括程序计数器、虚拟机栈、本地方法栈。线程之间互不干扰,所以这些区域天然就是线程安全的。

  • 右侧线程共享区域:堆和方法区是所有线程共享的,这也是并发问题的 "重灾区"。你在多线程环境下遇到的很多数据竞争问题,根源就在这里。

  • 运行时常量池:它逻辑上是方法区的一部分,但在 JDK 7 之后,字符串常量池被移到了堆中,这个版本变化面试官特别爱追问。

二、线程私有区域逐个拆解

1. 程序计数器(Program Counter Register)

这块是最 "小透明" 的区域,但作用很关键。

  • 存什么:记录当前线程正在执行的字节码指令地址。如果执行的是 Native 方法,则为空(Undefined)。
  • 为什么需要它:CPU 在多线程之间切换时,需要知道每个线程执行到了哪里,恢复时才能接着往下跑。
  • 唯一不会 OOM 的区域:JVM 规范中没有规定任何 OutOfMemoryError 情况。

2. 虚拟机栈(VM Stack)

这块是面试的高频考点,栈帧的结构一定要搞清楚。

上图展示了虚拟机栈的内部结构,每个方法调用对应一个栈帧,遵循 "先进后出" 的原则。关键要点:

  • 栈帧:每个方法从调用到执行完成,对应一个栈帧的入栈和出栈过程。
  • 局部变量表:存放方法参数和局部变量。基本类型直接存值,对象引用存的是指向堆中对象的引用地址。其中 longdouble 占 2 个 slot,其余占 1 个 slot。
  • 操作数栈:方法执行过程中的计算工作区,比如 a + b,先把 ab 压栈,再执行加法。
  • 动态链接:指向运行时常量池中该栈帧所属方法的引用,支持多态的实现。
  • 方法返回地址:方法正常退出或异常退出后,返回到调用者的位置。

当递归调用太深时,栈帧不断入栈但不出栈,就会抛出 StackOverflowError

3. 本地方法栈

和虚拟机栈类似,只不过它服务的是 Native 方法(比如 System.currentTimeMillis() 底层调用的就是 Native 方法)。HotSpot 虚拟机把虚拟机栈和本地方法栈合二为一了。

三、线程共享区域

1. 堆(Heap)

堆是 JVM 中最大的一块内存,几乎所有对象实例和数组都在这里分配。

  • 新生代:新对象先在 Eden 区分配。Eden 区满时触发 Minor GC,存活对象复制到 Survivor 区,年龄达到阈值(默认 15)晋升到老年代。默认比例 Eden : S0 : S1 = 8 : 1 : 1
  • 老年代:存放长期存活的对象和大对象(直接进入老年代的场景)。老年代满时触发 Major GC / Full GC。

这块有个经典考点:几乎所有对象都在堆上分配。为啥说 "几乎"?因为 JDK 7 之后引入了逃逸分析标量替换,如果一个对象没有逃逸出方法,JVM 可能会直接在栈上分配,避免堆内存的分配和 GC 压力。

2. 方法区(Method Area / 元空间)

方法区在 JDK 7 及以前叫 "永久代"(PermGen),JDK 8 开始改名为 "元空间"(Metaspace),并且从 JVM 堆内存移到了本地内存

对比项 JDK 7 永久代 JDK 8+ 元空间
存储位置 JVM 堆内存 本地内存(直接内存)
大小限制 -XX:MaxPermSize 限制 受物理内存限制(也可配置上限)
存储内容 类信息、常量池、静态变量 类信息、常量池(字符串常量池已移至堆)
常见问题 容易 OOM: PermGen space 较少出现,但动态生成大量类时仍可能 OOM
GC 触发 Full GC 时回收 达到 MaxMetaspaceSize 时触发 Full GC

为什么要干掉永久代?因为永久代的大小很难预估,太大浪费内存,太小动不动就 OOM,尤其是用 CGLIB、动态代理大量生成类的框架(比如 Spring),永久代经常爆。元空间直接用本地内存,空间大了不少,这个改动确实优雅。

3. 运行时常量池

它是方法区的一部分,存放编译期生成的各种字面量符号引用。JDK 7 之后,字符串常量池被移到了堆中。

// 经典面试代码:体会常量池的位置变化
String s1 = new String("hello");  // 堆上创建对象
String s2 = "hello";              // 字符串常量池中查找或创建
String s3 = "hello";
System.out.println(s2 == s3);     // true,指向常量池同一个引用
System.out.println(s1 == s2);     // false,s1 在堆上,s2 在常量池

这段代码的要点在于:new String() 会在堆上创建一个新对象,而字面量赋值 "hello" 会先去字符串常量池查找,有就直接引用,没有就创建。这也是为什么 s2 == s3true,而 s1 == s2false

面试高频追问

  1. 追问:JDK 8 为什么用元空间替换永久代?

    三个核心原因:永久代大小难以预估容易 OOM;字符串常量池已经在 JDK 7 移到了堆中,永久代存在意义减弱;合并 HotSpot 和 JRockit 虚拟机时,JRockit 没有永久代的概念。

  2. 追问:什么情况下栈会溢出?堆会溢出?

    栈溢出通常是递归调用过深(StackOverflowError)或者线程数过多导致栈总内存不够(OOM)。堆溢出通常是内存泄漏或者创建了大量无法回收的对象(OOM: Java heap space)。

  3. 追问:直接内存是什么?和堆内存有什么区别?

    直接内存(Direct Memory)不受 JVM 堆大小限制,通过 NIODirectByteBuffer 分配,避免了 Java 堆和 Native 堆之间的数据拷贝,但分配和回收成本更高,也可能导致 OOM。

常见面试变体

  • "说说 JVM 的内存模型"(注意区分内存模型 JMM 和内存区域,这是两个不同的概念)
  • "JDK 7 和 JDK 8 的 JVM 内存区域有什么区别?"
  • "哪些区域会发生 OOM?哪些不会?"
  • "方法区和永久代是什么关系?"

记忆口诀

线程私有:计数器、虚拟机栈、本地方法栈("计栈本"——记下本子)

线程共享:堆、方法区("对方"——堆和方法)

OOM 归属:计数器不会 OOM,栈会溢出(StackOverflow),堆和方法区会 OOM

总结

JVM 运行时内存区域分为线程私有(程序计数器、虚拟机栈、本地方法栈)和线程共享(堆、方法区)两大类。面试时记住 "计栈本,对方堆" 就能快速回忆起所有区域,再结合每个区域存什么、会不会 OOM、JDK 7 到 8 的变化(永久代 → 元空间)来展开,这道题基本稳了。