目录

深入理解-Java-虚拟机内存区域

深入理解 Java 虚拟机内存区域

Java 虚拟机(JVM)是 Java 程序运行的核心环境,它通过内存管理为程序提供高效的执行支持。JVM 在运行时将内存划分为多个区域,每个区域都有特定的作用和生命周期。本文将详细介绍 JVM 的运行时数据区域及其功能,并探讨与内存相关的常见问题,如内存溢出(OOM)和栈溢出(SOF)。

https://i-blog.csdnimg.cn/direct/a57883089faa4c4b99d5790bd1b21b4e.png#pic_center


JVM 运行时数据区域

JVM 将其管理的内存划分为若干区域,包括线程私有和线程共享两类。以下是主要区域的概览:

1. 程序计数器(Program Counter Register)

  • 作用 :线程私有的小型内存区域,记录当前线程执行的字节码指令地址,用于控制程序流程(如分支、循环、跳转等)。
  • 特点
    • 若执行 Java 方法,则记录字节码地址;若执行 Native 方法,则为空(Undefined)。
    • 是唯一不会抛出 OutOfMemoryError 的区域。
  • 生命周期 :随线程创建而生,随线程结束而灭。

2. Java 虚拟机栈(Java Virtual Machine Stacks)

  • 作用 :线程私有,管理 Java 方法的执行。每个方法调用对应一个栈帧(Stack Frame),包含局部变量表、操作数栈、动态连接和方法返回地址。
  • 栈帧结构
    • 局部变量表 :存储方法参数和局部变量。
    • 操作数栈 :存放中间计算结果和临时变量。
    • 动态连接 :支持方法调用时的符号引用解析。
    • 方法返回地址 :记录方法返回位置。
  • 异常
    • 栈深度超限: StackOverflowError
    • 内存不足: OutOfMemoryError
  • 调整 :通过 -Xss 参数设置栈大小。

3. 本地方法栈(Native Method Stack)

  • 作用 :线程私有,为 Native 方法服务,功能与虚拟机栈类似。
  • 异常 :同样可能抛出 StackOverflowErrorOutOfMemoryError

4. Java 堆(Java Heap)

  • 作用 :线程共享,存储对象实例,是垃圾收集器(GC)的主要管理区域。
  • 分代结构
    • 新生代 :包括 Eden 和 Survivor(S0、S1),存放新对象。
    • 老年代 :存放长期存活的对象。
    • 元空间 (JDK 8 后取代永久代):使用本地内存。
  • 年龄限制 :对象年龄记录在对象头,最大值为 15(4 位二进制),由 -XX:MaxTenuringThreshold 设置。
  • 异常 :内存不足时抛出 OutOfMemoryError
  • 调整 :通过 -Xms (初始值)和 -Xmx (最大值)配置堆大小。

5. 方法区(Method Area)

  • 作用 :线程共享,存储类信息、常量、静态变量和编译后的代码。
  • 演变
    • JDK 7 前:称为永久代(PermGen),通过 -XX:MaxPermSize 设置。
    • JDK 8 后:改为元空间(Metaspace),通过 -XX:MaxMetaspaceSize 设置,使用本地内存。
  • 异常 :扩展失败抛出 OutOfMemoryError

6. 运行时常量池(Runtime Constant Pool)

  • 作用 :方法区的一部分,存储 Class 文件中的字面量(如字符串常量)和符号引用。
  • 特点 :具备动态性,运行时可通过 String.intern() 添加常量。
  • 异常 :内存不足抛出 OutOfMemoryError

7. 直接内存(Direct Memory)

  • 作用 :非 JVM 规范定义区域,通过 NIO(如 DirectByteBuffer )直接分配堆外内存。
  • 调整 :通过 -XX:MaxDirectMemorySize 设置,默认与 -Xmx 一致。
  • 异常 :分配失败抛出 OutOfMemoryError

内存区域对比

区域作用范围可能异常
程序计数器线程私有
虚拟机栈线程私有StackOverflowError , OutOfMemoryError
本地方法栈线程私有StackOverflowError , OutOfMemoryError
Java 堆线程共享OutOfMemoryError
方法区线程共享OutOfMemoryError
运行时常量池线程共享OutOfMemoryError
直接内存非运行时区OutOfMemoryError

对象在 JVM 中的生命周期

1. 对象创建

  • 步骤
    1. 遇到 new 指令,检查常量池中类的符号引用并加载类。
    2. 在堆中分配内存(指针碰撞或空闲列表)。
    3. 初始化对象(调用 <init>() 方法)。
  • 内存分配方式
    • 指针碰撞 :堆内存规整时使用。
    • 空闲列表 :堆内存不规整时使用,依赖垃圾收集器是否带压缩功能。
  • 并发安全 :通过 CAS 或 TLAB(线程本地分配缓冲)确保分配安全。

2. 对象内存布局

  • 对象头
    • Mark Word :存储运行时数据(如哈希码、GC 年龄、锁状态)。
    • 类型指针 :指向类的元数据。
    • 数组长度 (仅数组对象):记录数组大小。
  • 实例数据 :存储字段内容。
  • 对齐填充 :确保内存对齐。

3. 对象访问

  • 句柄访问reference 指向句柄池,稳定但间接。
  • 直接指针 (HotSpot 默认): reference 直接指向对象地址,速度更快。

内存溢出与栈溢出

1. OutOfMemoryError (OOM)

除程序计数器外,其他区域都可能因内存不足抛出 OOM。常见场景包括:

  • 堆溢出 :对象过多, -Xmx 不足。
  • GC 开销超限 :GC 时间超 98% 且回收不足 2%。
  • 元空间不足 :类加载过多。
  • 直接内存溢出 :NIO 分配超限。
  • 线程创建失败 :系统内存不足以支持新线程。

2. StackOverflowError (SOF)

  • 原因
    • 递归过深。
    • 大量循环或死循环。
  • 参数 :通过 -Xss 调整栈大小。

示例分析

以下是一个简单程序的内存分配过程:

public class JVMCase {
    public final static String MAN_SEX_TYPE = "man"; // 常量池
    public static String WOMAN_SEX_TYPE = "woman";   // 方法区

    public static void main(String[] args) {
        Student stu = new Student(); // 堆中创建对象,栈中存引用
        stu.setName("nick");
        JVMCase jvmcase = new JVMCase();
        print(stu); // 静态方法入栈
        jvmcase.sayHello(stu); // 非静态方法入栈
    }
}
  • 类加载时,静态变量和常量分配在方法区。
  • main 执行时,对象在堆中创建,引用存于栈中,方法调用依次入栈。

结语

理解 JVM 内存区域是掌握 Java 性能调优的基础。从线程私有的程序计数器到共享的 Java 堆和方法区,每个区域各司其职。通过合理配置参数(如 -Xmx-Xss-XX:MaxMetaspaceSize ),并结合工具(如 jmap、MAT)分析内存问题,可以有效避免 OOM 和 SOF,提升程序稳定性。