堆、栈、方法区到底是什么?一文带你搞懂 JVM 运行时数据区内存模型!

开发 前端
本章我们将从 JVM 的内存模型入手,逐步拆解堆与方法区的核心结构及其角色,深入解析程序计数器与栈内存的设计原理,让你理解 JVM 的内存管理机制并为调优实践打下基础。

大家好,我是码哥。

在 JVM 的世界中,运行时数据区域是整个虚拟机的基础,它决定了程序的内存管理、线程的执行流以及垃圾回收的核心逻辑。

运行时数据区域的划分不仅体现了 JVM 的设计哲学,还在性能优化中起着至关重要的作用。

本章我们将从 JVM 的内存模型入手,逐步拆解堆与方法区的核心结构及其角色,深入解析程序计数器与栈内存的设计原理,让你理解 JVM 的内存管理机制并为调优实践打下基础。

JVM 内存模型概述

Java 虚拟机运行时内存被分为若干功能区域,每个区域承担特定的职责。

什么是 JVM 运行时数据区?

Java 虚拟机 (JVM) 可以分为三个主要的子系统,分别为 类加载器子系统、运行时数据区 和 执行引擎。

图:小豆丁技术栈

当 类加载子系统 完成了 加载、验证、准备、解析 和 初始化 等几个阶段后,执行引擎便开始对这些初始化完成的类进行使用。

图:小豆丁技术栈

在操作系统中,每个进程通常会被分配一个虚拟的内存空间,进程的操作都在这个内存空间中进行管理。而 Java 虚拟机作为一个进程,也同样会获得操作系统分配的内存空间。

这些区域既相互独立又彼此关联,共同支撑着 Java 程序的执行。

运行时内存的划分

JVM 的运行时内存区域按照功能可以划分为以下几部分:

图:小豆丁技术栈

区域名称

类型

主要内容

是否线程私有

程序计数器

私有

当前线程执行的字节码指令地址


Java 虚拟机栈

私有

方法调用的局部变量表、操作数栈、方法返回地址等


本地方法栈

私有

为本地方法(如 JNI)提供支持


共享

对象实例和数组


方法区

共享

类元信息、运行时常量池、静态变量、编译后代码


线程私有区域 :包括 程序计数器、虚拟机栈 和 本地方法栈,这些区域与线程生命周期绑定,每个线程独立管理,不存在并发问题。

线程共享区域 :包括 堆 和 方法区,多个线程共享这些区域,因此需要通过锁或其他同步机制解决并发访问冲突。

图:小豆丁技术栈

可以将 JVM 的内存模型类比为一座大厦:

  • 线程私有区域 是每个居民的私人房间,只有主人可以进入,互不干扰。
  • 线程共享区域 是大厦的公共设施(如电梯、健身房),需要所有人协同使用,并且需要制定规则避免冲突。

敲黑板:在多线程程序中,线程私有区域(如虚拟机栈)避免了共享资源争用,因此适合存储局部变量和操作数;

线程共享区域(如堆)因需要存储对象实例,成为垃圾回收的主要目标。

理解这些区域的划分,可以有效帮助我们定位内存溢出或线程争用的问题。

堆的结构与分代模型

堆是 JVM 中最大的内存区域,用于存储几乎所有对象实例和数组。堆的设计直接影响 Java 程序的性能,尤其在垃圾回收(GC)时对堆内存的操作至关重要。

堆的分代模型

JVM 中的堆被划分为两大代:

新生代(Young Generation)

  • 存储生命周期短的对象(大部分新建对象会存储在新生代)。
  • 新生代进一步分为 Eden 区 和两个 Survivor 区(S0 和 S1)
  • GC 时,Eden 中存活的对象会被复制到 Survivor 区。

老年代(Old Generation)

  • 存储生命周期较长的对象,例如缓存、连接池等。

  • 经过多次新生代 GC 后未被回收的对象会晋升到老年代。

堆内存的分代结构

堆的设计哲学

  • 优化垃圾回收:分代模型使得垃圾回收器可以针对不同代使用不同算法。例如,新生代使用复制算法(Copying GC),而老年代使用标记-清理(Mark-Sweep)或标记-整理(Mark-Compact)算法。
  • 分离对象生命周期:通过分代管理对象生命周期,提高内存分配效率。

敲黑板:在 GC 日志中,频繁的 Minor GC(新生代垃圾回收)可能提示对象创建过于频繁,而 Full GC(老年代垃圾回收)的延迟通常反映老年代空间不足。通过调优堆内存的分配,可以改善程序性能。

方法区:元数据与常量的存储

方法区(Method Area) 和 堆 类似,是在 JVM 启动时创建的,也是 JVM 运行时数据区中的一块线程共享的内存区域。方法区的内存空间在逻辑上连续,但物理上不一定连续,主要用于存储一些 类信息、方法信息、域信息、JIT代码缓存、运行时常量池:

  • 类元数据:包括类名、字段描述、方法描述、访问权限等。
  • 运行时常量池:存储字面量(如字符串常量)和符号引用(如方法引用)。
  • 静态变量:存储类的 static 字段,这些字段生命周期与类一致。
  • 即时编译后的代码:如 JIT 编译器生成的优化代码。

JDK 8 的方法区变迁

  • 在 JDK 8 之前,方法区使用堆中的永久代(PermGen)实现。
  • 从 JDK 8 开始,永久代被移除,方法区由本地内存中的 元空间(Metaspace) 取代,解决了永久代的容量限制问题。

实践场景

如果程序运行时加载了过多的类,可能会导致元空间内存不足,从而触发 OutOfMemoryError: Metaspace。

在这种情况下,可以通过调整 -XX:MaxMetaspaceSize 参数来限制元空间的大小。

程序计数器与栈内存详解

程序计数器(Program Counter)

程序计数器(Program Counter)是 JVM 中最小的内存区域,用于记录当前线程正在执行的字节码指令地址。

  •  线程私有 的,每个线程有独立的计数器。
  • 如果当前方法是 Native 方法,程序计数器值为未定义。

程序计数器就像一本书的书签,记录了当前线程执行到哪一页,当线程被切换时可以恢复阅读位置。

Java 虚拟机栈

JVM 栈是线程执行方法调用的核心数据结构,保存了方法的局部变量、操作数栈和返回地址等信息。每个方法对应一个 栈帧(Stack Frame),栈帧以 后进先出(LIFO) 的顺序管理。

局部变量表

  • 保存基本数据类型(如 int、long)和对象引用。
  • 编译期分配固定大小,运行时不允许动态调整。

操作数栈

  • 用于字节码指令的临时操作数存储。

  • 典型操作:iadd 从操作数栈取两个值,计算和并存回栈中。

动态链接

  • 用于方法调用时解析符号引用到实际内存地址。

返回地址

  • 方法执行完毕后,返回上层调用方法的位置。

敲黑板:如果递归调用深度过高或方法嵌套调用过多,可能会导致虚拟机栈溢出,触发 StackOverflowError。调整 -Xss 参数可增大栈大小。

最后

通过本章的解析,我们对 JVM 的运行时数据区域有了系统性的理解,包括各区域的职责分工、具体实现和实践场景。

理解这些区域的运行逻辑是学习 JVM 垃圾回收机制与性能调优的基础。

在下一章中,我们将深入探讨对象的生命周期与内存分配策略,为垃圾回收优化奠定理论基础。

责任编辑:姜华 来源: 码哥跳动
相关推荐

2022-03-21 11:07:43

JVM内存字节码

2019-12-04 13:50:07

CookieSessionToken

2021-01-18 13:05:52

Serverless Serverfull FaaS

2021-07-14 07:21:57

JVM运行数据

2021-09-08 17:42:45

JVM内存模型

2020-10-27 10:26:03

编程开发Java

2019-12-12 11:19:33

JVM内存线程

2018-11-22 12:07:37

Java虚拟机结构

2022-01-17 22:09:50

JVM方法区数据

2021-08-13 08:15:23

JVM 虚拟机Java

2019-06-28 08:31:01

微内核安卓系统

2023-03-06 21:29:41

mmap技术操作系统

2017-09-20 08:07:32

java加载机制

2021-06-30 08:45:02

内存管理面试

2023-12-26 12:37:08

内存模型堆排序

2020-11-16 09:28:41

函数内存

2021-06-07 08:32:47

JVMGraalVM虚拟机

2023-03-29 15:21:18

2019-10-11 08:41:35

JVM虚拟机语言

2022-09-30 15:09:20

云计算边缘计算边缘云
点赞
收藏

51CTO技术栈公众号