暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

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

码哥跳动 2024-11-26
127

大家好,我是码哥。

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

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

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

JVM 内存模型概述

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

什么是 JVM 运行时数据区?

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

图:小豆丁技术栈

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

图:小豆丁技术栈

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

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

运行时内存的划分

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

图:小豆丁技术栈
区域名称类型主要内容是否线程私有
程序计数器私有当前线程执行的字节码指令地址
Java 虚拟机栈私有方法调用的局部变量表、操作数栈、方法返回地址等
本地方法栈私有为本地方法(如 JNI)提供支持
共享对象实例和数组
方法区共享类元信息、运行时常量池、静态变量、编译后代码

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

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

图:小豆丁技术栈

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

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

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

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

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

堆的结构与分代模型

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

堆的分代模型

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

  1. 新生代(Young Generation)
    • 存储生命周期短的对象(大部分新建对象会存储在新生代)。
    • 新生代进一步分为 Eden 区 和两个 Survivor 区(S0 和 S1)
    • GC 时,Eden 中存活的对象会被复制到 Survivor 区。
  2. 老年代(Old Generation)
    • 存储生命周期较长的对象,例如缓存、连接池等。
    • 经过多次新生代 GC 后未被回收的对象会晋升到老年代。

堆内存的分代结构

img

堆的设计哲学

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

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

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

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

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

JDK 8 的方法区变迁

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

实践场景

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

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

程序计数器与栈内存详解

程序计数器(Program Counter)

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

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

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

Java 虚拟机栈

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

img
  1. 局部变量表
    • 保存基本数据类型(如 int、long)和对象引用。
    • 编译期分配固定大小,运行时不允许动态调整。
  2. 操作数栈
    • 用于字节码指令的临时操作数存储。
    • 典型操作:iadd
      从操作数栈取两个值,计算和并存回栈中。
  3. 动态链接
    • 用于方法调用时解析符号引用到实际内存地址。
  4. 返回地址
    • 方法执行完毕后,返回上层调用方法的位置。

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

最后

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

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

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

最后,介绍下我的Java 面试高手心法 58 讲专栏内容涵盖 Java 基础、Java 高级进阶、Redis、MySQL、消息中间件、微服务架构设计等面试必考点、面试高频点。

丢掉你收藏的那些所谓的「面试宝典」,因为它们大多数深度不够,甚至内容还有错误,这也是为何每次面试你都回答不好的原因,你只会看完就忘,还浪费时间。




往期推荐



面试官拷打:Redis 高可用篇章中面试最常见的 6 个问题!

重生之MySQL SQL 执行的 7 大关键步骤,解锁新技能

重生之从零设计 MySQL 架构

小米面试:什么是线程池?工作原理是什么?线程池可以动态修改吗?

一文讲透数据库与 Redis 缓存一致性问题

Spring事务失效的12种场景

10w 级的并发场景,JVM 有哪些方面值得优化呢?

文章转载自码哥跳动,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论