0%

内存分配

JVM 内存分配:运行时数据区的结构与分工

JVM 运行时数据区是 Java 程序运行的内存基础,其结构划分直接影响程序的性能、内存管理和垃圾回收机制。理解内存区域的划分及各区域的作用,是排查内存泄漏、优化程序性能的关键。本文将详细解析 JVM 运行时数据区的结构、各区域的特点及内存分配规则。

JVM 运行时数据区的整体结构

JVM 运行时数据区根据线程共享性生命周期分为两类:线程共享区(随 JVM 启动 / 退出而创建 / 销毁)和线程私有区(随线程创建 / 销毁而创建 / 销毁)。

线程共享区(所有线程可访问)

  • 堆(Heap):存储对象实例和数组,是 JVM 中最大的内存区域,也是垃圾回收(GC)的主要场所。
  • 方法区(Method Area):存储类元数据(类信息、字段、方法、常量池等),JDK 8 后由元空间(Metaspace) 实现(使用本地内存)。
  • 代码缓存区(Code Cache):存储 JIT 编译器编译后的机器码(热点代码),提高执行效率。

线程私有区(每个线程独立拥有)

  • 程序计数器(Program Counter Register):记录当前线程执行的字节码指令地址,是 JVM 中唯一不会发生 OutOfMemoryError 的区域。
  • 虚拟机栈(VM Stack):存储方法调用的栈帧(局部变量表、操作数栈、方法返回地址等),栈深度过大可能导致 StackOverflowError
  • 本地方法栈(Native Method Stack):为 Native 方法(非 Java 实现的本地方法)提供栈空间,功能与虚拟机栈类似。

各内存区域的详细解析

堆(Heap):对象的 “主战场”

堆是 JVM 中最大的内存区域,被所有线程共享,几乎所有对象实例和数组都在堆上分配

(1)堆的特点
  • 动态分配:对象内存按需分配,无需手动释放(由 GC 自动回收)。
  • 内存划分:为优化 GC 效率,堆通常分为以下区域(以 HotSpot 为例):
    • 年轻代(Young Generation):存储新创建的对象,分为 Eden 区(伊甸园)和两个 Survivor 区(From、To),比例通常为 8:1:1。
    • 老年代(Old Generation/Tenured):存储存活时间较长的对象(多次 GC 后仍存活的对象)。
    • 永久代(Permanent Generation):JDK 7 及以前用于存储类元数据,JDK 8 后被元空间取代(移至本地内存)。
(2)堆的内存分配规则
  • 对象优先在 Eden 区分配:新对象先进入 Eden 区,当 Eden 区满时触发 Minor GC(年轻代 GC)。
  • 大对象直接进入老年代:超过指定大小的对象(如大数组)直接分配到老年代(避免年轻代频繁 GC),可通过 -XX:PretenureSizeThreshold 配置阈值。
  • 长期存活对象进入老年代:对象每经历一次 Minor GC 存活下来,年龄加 1,达到阈值(默认 15)后进入老年代,可通过 -XX:MaxTenuringThreshold 配置。

方法区(Method Area):类元数据的 “仓库”

方法区存储类的元数据信息,包括:

  • 类的结构信息(类名、父类、接口、访问修饰符等);
  • 字段和方法信息(名称、类型、参数、返回值等);
  • 常量池(字符串常量、数字常量、符号引用等);
  • 静态变量(JDK 8 后随 Class 对象移至堆中)。
(1)方法区的实现演变
  • JDK 7 及以前:通过永久代(Permanent Generation) 实现,受 JVM 堆内存限制(可通过 -XX:PermSize-XX:MaxPermSize 配置大小)。
  • JDK 8 及以后:改用元空间(Metaspace) 实现,使用本地内存(不受 JVM 堆大小限制,默认仅受系统内存限制),可通过 -XX:MetaspaceSize-XX:MaxMetaspaceSize 配置。
(2)常见问题
  • OutOfMemoryError: PermGen space:JDK 7 及以前,永久代内存不足(如大量动态生成类)。
  • OutOfMemoryError: Metaspace:JDK 8 及以后,元空间内存不足(需调整 -XX:MaxMetaspaceSize)。

程序计数器(Program Counter Register):线程执行的 “导航仪”

程序计数器是线程私有的小型内存区域,作用是记录当前线程执行的字节码指令地址

特点
  • 线程隔离:每个线程有独立的程序计数器,确保线程切换后能恢复执行位置。
  • 无 OOM:是 JVM 中唯一不会抛出 OutOfMemoryError 的区域。
  • Native 方法特殊处理:当线程执行 Native 方法时,程序计数器值为 undefined(因 Native 方法由本地语言实现,无需字节码指令)。

虚拟机栈(VM Stack):方法调用的 “栈帧容器”

虚拟机栈是线程私有的,模拟方法调用的执行过程,每个方法调用对应一个栈帧(Stack Frame) 的入栈,方法执行完毕后栈帧出栈。

(1)栈帧的组成
  • 局部变量表:存储方法的局部变量(基本数据类型、对象引用、返回地址等),容量在编译期确定。
  • 操作数栈:作为方法执行的临时数据存储区(如算术运算、参数传递)。
  • 动态链接:指向常量池中该方法的符号引用(支持动态绑定)。
  • 方法返回地址:方法执行完毕后返回的位置(如调用者的下一条指令地址)。
(2)常见异常
  • StackOverflowError:栈深度超过虚拟机允许的最大深度(如递归调用无终止条件)。
  • OutOfMemoryError:虚拟机栈可动态扩展时,扩展失败(内存不足)。

本地方法栈(Native Method Stack):Native 方法的 “执行栈”

本地方法栈与虚拟机栈功能类似,但专为 Native 方法(如 C/C++ 实现的方法)服务

特点
  • 实现灵活:JVM 规范未强制规定实现方式,部分虚拟机(如 HotSpot)将本地方法栈与虚拟机栈合并。
  • 异常类型:与虚拟机栈相同,可能抛出 StackOverflowErrorOutOfMemoryError

代码缓存区(Code Cache):热点代码的 “加速区”

代码缓存区用于存储 JIT(即时编译器)编译后的机器码(热点代码,如频繁执行的方法或循环),避免重复解释执行,提高效率。

特点
  • 内存限制:默认大小有限,若热点代码过多可能导致 OutOfMemoryError: CodeCache is full
  • 配置参数:可通过 -XX:InitialCodeCacheSize-XX:ReservedCodeCacheSize 调整大小。

内存分配规则:数据存放在哪里?

Java 中的数据(变量、对象等)根据类型和作用域,分配在不同的内存区域:

数据类型 存储位置 特点
基本数据类型局部变量 虚拟机栈的局部变量表 随方法执行入栈,方法结束出栈,内存自动释放。
对象引用(局部变量) 虚拟机栈的局部变量表 存储对象在堆中的内存地址,引用本身在栈上,对象实例在堆上。
对象实例 new 创建,GC 负责回收,生命周期与引用相关。
数组 数组本身是对象,存储在堆上,数组元素(基本类型 / 引用)也在堆上。
静态变量(类变量) 方法区(JDK 8 后在堆) 随类加载初始化,生命周期与类一致(JVM 退出时释放)。
实例变量 堆(对象实例中) 随对象创建在堆上分配,对象被回收时释放。
字符串常量 常量池(方法区 / 堆) 编译期确定的字符串常量存储在常量池,JDK 7 后常量池移至堆中。

示例:内存分配场景分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MemoryAllocation {
// 静态变量(类变量):存储在方法区(JDK8后在堆)
private static int staticVar = 10;

// 实例变量:随对象存储在堆中
private int instanceVar;

public void method() {
// 局部变量(基本类型):存储在虚拟机栈的局部变量表
int localVar = 20;

// 对象引用(局部变量):引用在栈,对象实例在堆
MemoryAllocation obj = new MemoryAllocation();

// 数组:数组对象在堆,元素(基本类型)也在堆
int[] array = new int[5];
}
}
  • staticVar:静态变量,类加载时分配在方法区(JDK 8 后在堆)。
  • instanceVar:实例变量,当 new MemoryAllocation() 时,随对象分配在堆中。
  • localVar:局部变量(int),存储在 method() 方法的栈帧局部变量表。
  • obj:对象引用(局部变量),存储在栈帧局部变量表,指向堆中的 MemoryAllocation 对象。
  • array:数组引用在栈,数组对象(含 5 个 int 元素)在堆中。

堆与栈的核心区别

对比维度 堆(Heap) 栈(VM Stack)
线程共享性 所有线程共享 线程私有
存储内容 对象实例、数组、静态变量(JDK8 后) 方法栈帧(局部变量、操作数栈等)
内存大小 较大(通常几 GB) 较小(通常几 MB)
分配方式 动态分配(无需连续内存) 连续内存块(栈帧入栈 / 出栈)
回收机制 由 GC 自动回收(对象实例) 随方法执行自动释放(栈帧出栈)
异常类型 OutOfMemoryError(堆溢出) StackOverflowError(栈深度溢出)

欢迎关注我的其它发布渠道

表情 | 预览
快来做第一个评论的人吧~
Powered By Valine
v1.3.10