JVM 内存结构与管理机制

123 阅读8分钟

一、运行时数据区总体结构

JVM 在运行时将内存划分为若干区域,统称为运行时数据区。其中,堆、线程栈与元空间是三个核心组成部分,分别承担对象实例、执行上下文和类元数据的存储与管理职责。

这三者的划分基于对数据生命周期、访问模式与管理成本的综合权衡,体现了分治与职责分离的设计思想。


二、堆

堆是 JVM 中最大的运行时数据区,被所有线程共享,用于存储对象实例。数组、字符串常量池中的字符串对象,均位于堆中。

堆由垃圾回收器统一管理,其内存分配与回收不依赖程序员干预。对象的生命周期不确定,且可能被多个线程访问,因此堆的设计目标是:为共享的、动态生命周期的数据提供高效的存储与回收机制。

1. 分代结构

堆采用分代设计,划分为年轻代与老年代,依据对象的生命周期特征进行分区管理。

该设计基于“弱分代假说”:绝大多数对象在创建后很快失去引用,仅少数对象长期存活。通过将高频死亡对象集中于年轻代,可实现快速、低开销的回收。

年轻代

年轻代负责新对象的分配与短生命周期对象的回收。

  • 新对象优先在 Eden 区分配;
  • 当 Eden 区空间不足时,触发 Minor GC;
  • Minor GC 采用标记-复制算法,将 Eden 区和当前 Survivor 区中的存活对象复制到另一块空的 Survivor 区,随后清空原区域;
  • Eden 区与 Survivor 区的比例由 -XX:SurvivorRatio 控制,典型值为 8:1:1。

年轻代包含两个大小相等的 Survivor 区(S0 和 S1),在 Minor GC 中交替作为 From 和 To 空间,实现双缓冲机制。每次 Minor GC 后,存活对象的年龄加一。当年龄达到阈值(默认 15,由 -XX:MaxTenuringThreshold 控制),对象晋升至老年代。

对象进入老年代的路径包括:

  • 年龄达到晋升阈值;
  • 大对象直接分配(由 -XX:PretenureSizeThreshold 控制);
  • Survivor 区空间不足时的提前晋升;
  • 空间担保失败时的直接分配。

老年代

老年代存放长期存活对象和大对象。

其回收频率低于年轻代,但单次回收耗时较长。当老年代空间不足时,触发 Major GC 或 Full GC,可能导致整个应用程序暂停(Stop-The-World)。

老年代的回收效率直接影响系统整体性能,频繁的 Full GC 通常表明内存分配不合理或存在内存泄漏。

2. 参数控制

堆的大小由 -Xms(初始大小)和 -Xmx(最大大小)参数控制。年轻代大小可通过 -Xmn 直接指定,或通过 -XX:NewRatio 设置其与老年代的比例。

合理的参数配置能够降低 GC 频率与停顿时间,提升系统稳定性。


三、线程栈

线程栈是线程私有的内存区域,用于支持方法调用的执行。每个线程在创建时分配独立的栈空间,其生命周期与线程一致。

栈由栈帧(Stack Frame)构成,每个方法调用对应一个栈帧。栈帧是方法执行的数据载体,包含局部变量表、操作数栈、动态链接和返回地址四个组成部分。

1. 局部变量表

局部变量表以变量槽(Slot)为单位存储数据。基本类型中,long 和 double 占用两个 Slot,其余类型占用一个 Slot。对象引用(reference)也存储于局部变量表,其本质是堆中对象实例的指针。

局部变量必须在使用前显式初始化,未赋值的局部变量无法通过编译。

2. 操作数栈

操作数栈是执行字节码指令的工作区。字节码指令通过入栈和出栈操作完成运算。例如,加法指令将两个操作数从操作数栈弹出,执行相加后将结果压回栈顶。操作数栈的深度在编译期确定,运行时不会溢出。

3. 动态链接

动态链接指向运行时常量池中该方法的符号引用。该机制支持方法调用过程中的动态分派,是实现多态的基础。通过动态链接,虚拟机可在运行时解析方法的实际地址。

4. 返回地址

返回地址记录方法执行完毕后应恢复的执行位置。正常返回时,调用者的程序计数器指向当前指令的下一条指令;异常返回时,由异常处理机制确定目标地址。

5. 栈的管理

栈空间大小由 -Xss 参数控制。若线程请求的栈深度大于虚拟机允许的最大深度,抛出 StackOverflowError。若栈无法动态扩展且内存不足,抛出 OutOfMemoryError

栈内存无需垃圾回收,方法执行结束时,其栈帧自动弹出,空间立即释放。


四、元空间

元空间是存储类元数据的区域,位于本地内存(Native Memory),独立于堆。其内容包括类的结构信息:类名、字段、方法、方法字节码、常量池、注解、JIT 编译后的代码缓存等。

元空间取代了 JDK 8 之前的永久代(PermGen)。永久代位于堆中,导致类信息与对象实例争抢堆内存,频繁引发 OutOfMemoryError: PermGen。元空间移至本地内存后,仅受系统可用内存限制,避免了该问题。

1. 生命周期与卸载

类元数据的生命周期与类加载器绑定。只有当类加载器被回收时,其加载的类元数据才可被卸载。类卸载是 Full GC 的附带动作,需满足三个条件:

  • 该类所有实例已被回收;
  • 该类的 Class 对象未被引用;
  • 该类的类加载器实例已被回收。

2. 内存管理

元空间大小默认无上限,受系统本地内存限制。可通过 -XX:MaxMetaspaceSize 设置上限,防止内存耗尽。当元空间耗尽时,触发 Full GC 以尝试回收无用类元数据。若仍不足,抛出 OutOfMemoryError: Metaspace

元空间的内存管理由 JVM 自身负责,使用类加载器粒度的内存块分配机制。类元数据的分配与释放不参与常规垃圾回收流程,但其回收依赖于 GC 对类加载器的可达性分析。

3. 字符串常量池的位置

字符串常量池在 JDK 7 后从永久代移至堆中,因此其生命周期与普通对象一致,由垃圾回收器管理。元空间中仅保留符号引用,实际字符串对象存储于堆。


五、垃圾回收机制

JVM 的垃圾回收机制基于可达性分析算法,以 GC Roots 为起点,标记所有可达对象,未被标记的对象视为可回收。

1. 回收算法

标记-清除

分为标记和清除两个阶段。标记阶段遍历并标记所有存活对象;清除阶段回收未被标记的对象内存。该算法实现简单,但回收后产生内存碎片,影响大对象分配效率。曾用于 CMS 收集器的老年代回收阶段。

标记-复制

将内存划分为 From 和 To 两个区域。回收时,将 From 区中的存活对象复制到 To 区,随后清空 From 区。该算法回收后内存连续,无碎片,且仅处理存活对象,效率较高。缺点是空间利用率低。主要用于年轻代的 Minor GC。

标记-整理

在标记阶段与标记-清除相同。后续阶段将所有存活对象向内存一端移动,然后清理边界外的内存。该算法避免了碎片,空间利用率高。用于 Serial Old 和 Parallel Old 等老年代收集器。

2. 默认回收器

在 JDK 8 中,默认使用 Parallel Scavenge 作为年轻代收集器,采用标记-复制算法,关注吞吐量;老年代默认使用 Parallel Old,采用标记-整理算法。

自 JDK 9 起,G1(Garbage-First)成为默认回收器。G1 将堆划分为多个固定大小的区域(Region),年轻代和老年代均以 Region 为单位进行管理。其回收过程基于并发标记与疏散机制,本质上是一种分区域的复制算法,目标是实现低延迟的垃圾回收。

3. GC 触发条件

  • Minor GC:在 Eden 区满时触发,频率高但耗时短;
  • Full GC:在老年代满、元空间满或显式调用 System.gc() 时可能触发,影响范围广,停顿时间长。

频繁的 Full GC 通常表明内存分配不合理或存在内存泄漏。