2. JVM 内存模型回顾:堆、栈、方法区(Metaspace)的基本概念与关系

在深入分析 Metaspace GC 停顿之前,必须重新建立对 JVM 内存模型的精确认知。数据库迁移升级后,类加载器数量、动态生成的类元数据、字符串常量池的膨胀,都会直接冲击 Metaspace 区域,进而引发频繁的 Full GC 甚至 Metaspace OOM。本节从堆、栈、方法区三个核心区域出发,厘清各自职责与交互关系。

2.1 三大核心区域概述

JVM 运行时数据区按照线程共享与线程私有分为两大类:

  • 线程私有:程序计数器、虚拟机栈、本地方法栈。生命周期与线程绑定,无需 GC 管理。
  • 线程共享:堆(Heap)、方法区(Method Area,JDK 8+ 实现为 Metaspace)。是 GC 和内存调优的主战场。
区域 线程归属 存储内容 GC 行为 常见异常
堆(Heap) 共享 对象实例、数组 Young GC / Full GC OutOfMemoryError: Java heap space
虚拟机栈(Stack) 私有 局部变量表、操作数栈、动态链接、方法出口 无(自动回收) StackOverflowError / OutOfMemoryError
方法区(Metaspace) 共享 类元数据、方法字节码、常量池、静态变量、JIT 代码 Full GC(Metaspace GC) OutOfMemoryError: Metaspace
升级关注点: 数据库驱动升级、ORM 框架版本变更、连接池类加载隔离,都会导致 Metaspace 中类元数据激增。堆内存充足但 Metaspace 频繁 GC 是升级后停顿的典型特征。

2.2 堆(Heap)—— 对象生命周期的主战场

堆是 JVM 管理的最大一块内存,几乎所有对象实例和数组都在这里分配。堆在物理上不连续,逻辑上连续,且大小可通过 -Xms 和 -Xmx 控制。

堆内部按分代设计(除 G1 等分区收集器外):

  • 新生代(Young Generation):Eden + Survivor0 + Survivor1。对象优先在 Eden 分配,Minor GC 后存活对象在 Survivor 区交换。
  • 老年代(Old Generation):长期存活的对象(默认 15 次 Minor GC 后晋升),以及大对象直接进入老年代。
  • 元空间(Metaspace):JDK 8 起替代永久代,存放类元数据,属于堆外本地内存。
关键指标: 升级后若老年代占用率持续高于 70% 且 Metaspace 使用率同步攀升,说明类加载泄漏或元数据膨胀正在触发 Full GC。

2.3 虚拟机栈(Stack)—— 线程执行的基石

每个线程创建时都会分配一个独立的虚拟机栈,生命周期与线程一致。栈由一个个栈帧(Stack Frame)组成,每个方法调用对应一个栈帧入栈,方法返回则栈帧出栈。

栈帧内部结构:

  • 局部变量表:存放方法参数和局部变量,以 Slot 为单位。
  • 操作数栈:字节码指令的运算工作区。
  • 动态链接:指向运行时常量池中该方法的符号引用。
  • 方法出口:正常或异常返回时恢复调用者状态。

栈深度由 -Xss 控制。数据库迁移升级后,若连接池线程数增加或递归调用深度异常,可能触发 StackOverflowError。

2.4 方法区(Metaspace)—— 类元数据的归宿

方法区在 JDK 8 之前由永久代(PermGen)实现,JDK 8 之后被 Metaspace 取代。Metaspace 使用本地内存(Native Memory),默认上限仅受操作系统限制,但可通过 -XX:MaxMetaspaceSize 设置上限。

Metaspace 存储内容:

  • 类的结构信息(字段、方法、接口、注解等)
  • 运行时常量池(Class 文件常量池的运行时表示)
  • 静态变量(JDK 7 起从方法区移至堆,但仍在逻辑上属于方法区)
  • JIT 编译后的代码缓存(CodeCache)
  • 类加载器及其关联的元数据
升级陷阱: 数据库驱动升级后,若新驱动使用了大量匿名类、Lambda 表达式或动态代理,每个连接都会生成新的类加载器实例。这些类加载器无法被 GC 回收时,Metaspace 使用率只增不减,最终触发 Metaspace GC 甚至 OOM。

2.5 三区域关系与数据流

堆、栈、方法区并非孤立运行,它们通过引用和指针紧密协作:

  • 栈帧中的局部变量引用堆中的对象实例。
  • 堆中的对象实例通过 klass 指针指向 Metaspace 中的类元数据。
  • Metaspace 中的运行时常量池保存了字段和方法的符号引用,动态链接时解析为直接引用。

下面这张 SVG 图展示了三区域的核心交互关系:

JVM 内存模型核心区域关系图 虚拟机栈(Stack) 线程私有 栈帧 1(当前方法) 栈帧 2(调用者) 栈帧 3(...) 堆(Heap) 线程共享 · GC 主区域 Eden 区(对象分配) Survivor 区(From/To) 老年代(长期存活对象) 方法区(Metaspace) 堆外本地内存 类元数据(Klass) 运行时常量池 JIT 代码缓存 引用 klass 指针 动态链接(符号引用 → 直接引用) 核心交互说明: 1. 栈帧中的局部变量通过引用指向堆中的对象实例。 2. 堆中的每个对象头部都包含一个 klass 指针,指向 Metaspace 中的类元数据。 3. 栈帧中的动态链接在运行时解析常量池中的符号引用为直接引用。 4. 升级后类加载器泄漏 → Metaspace 无法卸载 → 触发 Full GC → 应用停顿。 5. 调优方向:监控 Metaspace 使用率、类加载器数量、Full GC 频率。

2.6 升级场景下的内存模型变化

数据库迁移版本升级后,JVM 内存模型中的三个区域都会受到不同程度的影响:

区域 升级影响 典型表现 调优建议
堆 新驱动/ORM 可能产生更多临时对象,老年代晋升加速 Young GC 频繁,老年代占用率快速上升 增大堆大小,调整新生代与老年代比例
栈 连接池线程数增加,栈深度可能变化 线程栈内存占用上升,偶发 StackOverflowError 适当增大 -Xss,检查递归调用
Metaspace 类加载器数量激增,类元数据膨胀 Metaspace GC 频繁,Full GC 停顿时间长 设置 -XX:MaxMetaspaceSize,监控类加载器泄漏