运行时内存布局

2020/12/11 JVM

# 运行时内存布局

# Java 运行时内存详解

# 1. 概述

Java 虚拟机 (JVM) 在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁的时间,有些区域随着虚拟机进程的启动而存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。

JVM 运行时内存区域主要包括以下几个部分:

  1. 程序计数器(Program Counter Register)
  2. Java 虚拟机栈(Java Virtual Machine Stacks)
  3. 本地方法栈(Native Method Stack)
  4. Java 堆(Java Heap)
  5. 方法区(Method Area)

其中,程序计数器、Java 虚拟机栈和本地方法栈是线程私有的,Java 堆和方法区是线程共享的。

# 2. 各个内存区域详解

# 2.1 程序计数器(Program Counter Register)

  • 作用: 记录当前线程执行的字节码指令的地址。
  • 特点:
    • 线程私有,每个线程都有一个独立的程序计数器。
    • 生命周期与线程相同。
    • 是 JVM 规范中唯一没有规定任何 OutOfMemoryError 情况的区域。
    • 如果线程正在执行 Java 方法,则计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地方法,则计数器值为空(Undefined)。
  • 用途:
    • 多线程环境下,线程切换后能恢复到正确的执行位置。

# 2.2 Java 虚拟机栈(Java Virtual Machine Stacks)

  • 作用: 描述 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
  • 特点:
    • 线程私有,每个线程都有一个独立的 Java 虚拟机栈。
    • 生命周期与线程相同。
    • 会出现两种异常:StackOverflowError (栈深度超过虚拟机允许的最大深度) 和 OutOfMemoryError (如果 JVM 允许动态扩展栈的大小,当扩展时无法申请到足够的内存)。
  • 栈帧 (Stack Frame):
    • 局部变量表: 存储方法参数和方法内部定义的局部变量。 局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
    • 操作数栈: 用于存放方法执行过程中产生的中间结果。 类似于 CPU 的寄存器,用于执行算术运算、方法调用等操作。
    • 动态链接: 指向运行时常量池中该栈帧所属方法的引用,目的是支持动态链接。
    • 方法出口: 记录方法正常退出或异常退出时的返回地址。

# 2.3 本地方法栈(Native Method Stack)

  • 作用: 与 Java 虚拟机栈的作用相似,区别在于 Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。
  • 特点:
    • 线程私有,每个线程都有一个独立的本地方法栈。
    • 生命周期与线程相同。
    • 具体实现由不同的 JVM 实现决定。 Sun HotSpot 虚拟机直接将本地方法栈和 Java 虚拟机栈合二为一。
    • 也会出现 StackOverflowErrorOutOfMemoryError 异常。
  • Native 方法: 用 native 关键字修饰的方法,通常由 C/C++ 编写,用于访问操作系统底层资源。

# 2.4 Java 堆(Java Heap)

  • 作用: 用于存放对象实例。 几乎所有的对象实例都在堆中分配内存,是 JVM 管理的内存中最大的一块。
  • 特点:
    • 线程共享,所有线程都可以访问堆中的对象。
    • 是垃圾收集器管理的主要区域,也称为“GC 堆”(Garbage Collected Heap)。
    • 可以通过 -Xms (初始堆大小) 和 -Xmx (最大堆大小) 参数设置堆的大小。
    • 会出现 OutOfMemoryError 异常。
  • 堆的结构 (HotSpot 虚拟机):
    • 新生代 (Young Generation): 存放新创建的对象。 新生代又分为 Eden 区和两个 Survivor 区 (From Survivor 和 To Survivor)。
    • 老年代 (Old Generation): 存放经过多次垃圾回收仍然存活的对象。
    • (JDK8 以后)元空间 (Metaspace): 存放类的元数据信息、常量池等。 元空间并不在堆中,而是直接使用本地内存。
  • 对象分配:
    • 新对象通常分配在 Eden 区。
    • 当 Eden 区满时,会触发 Minor GC (也称为 Young GC),将 Eden 区和 From Survivor 区中存活的对象复制到 To Survivor 区,并清空 Eden 区和 From Survivor 区。
    • 当 To Survivor 区也满时,会将部分对象晋升到老年代。
    • 当老年代也满时,会触发 Full GC (也称为 Major GC),清理整个堆空间。

# 2.5 方法区(Method Area)

  • 作用: 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
  • 特点:
    • 线程共享,所有线程都可以访问方法区中的数据。
    • 可以选择不实现垃圾收集,垃圾收集行为在这个区域出现的频率相对较低。
    • 可以通过 -XX:MetaspaceSize (初始元空间大小) 和 -XX:MaxMetaspaceSize (最大元空间大小) 参数设置方法区的大小。
    • 会出现 OutOfMemoryError 异常。
  • 重要组成部分:
    • 运行时常量池 (Runtime Constant Pool): 是类加载后常量池表在 JVM 内存中的表示形式。 运行时常量池相对于 Class 文件常量池具备更动态的特性,Java 语言并不要求常量一定只有编译期才能产生,也就是并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是 String 类的 intern() 方法。
  • JDK 8 永久代 (Permanent Generation) 的变化:
    • 在 JDK 7 及之前,HotSpot 虚拟机使用永久代来实现方法区。 永久代位于堆中,受堆内存大小的限制,并且容易发生内存溢出。
    • 在 JDK 8 中,HotSpot 虚拟机使用元空间(Metaspace)来代替永久代。 元空间并不在堆中,而是直接使用本地内存,从而避免了永久代的问题。
    • 字符串常量池从永久代移到堆中。
    • 类的元数据信息、常量池等从永久代移到元空间。

# 3. 总结

理解 JVM 运行时内存区域对于理解 JVM 的运行机制、进行性能调优至关重要。 掌握各个区域的作用、特点以及可能出现的异常,有助于我们更好地编写高效、稳定的 Java 程序。

Last Updated: 2025/6/20 17:20:46