Skip to content

JVM 内存结构

线程私有区域

这些区域的生命周期与线程相同,随线程的创建而创建,随线程的销毁而销毁。

程序计数器 (Program Counter Register)

程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。

  • 作用:记录当前线程正在执行的字节码指令的地址。执行引擎通过读取它的值来确定下一条要执行的指令。
  • 特性:
    • 线程私有,确保线程切换后能恢复到正确的执行位置。
    • 是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
    • 如果线程正在执行的是一个Java方法,计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则为空(Undefined)。

Java虚拟机栈 (Java Virtual Machine Stack)

每个方法在执行的同时都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

  • 特点:
    • 线程私有,生命周期与线程一致。
    • 是Java方法执行的内存模型。
    • 可能出现两种异常:
      • StackOverflowError:如果线程请求的栈深度大于虚拟机所允许的深度。
      • OutOfMemoryError:如果虚拟机栈可以动态扩展,但在扩展时无法申请到足够的内存。

栈帧的内部结构

  1. 局部变量表 (Local Variable Table)

    • 存放方法参数和方法内部定义的局部变量。
    • 基本存储单位是槽(Slot)。longdouble类型的变量会占用2个槽,其他基本数据类型和引用类型占用1个槽。
    • 对于实例方法,第0个槽(slot[0])默认用于存放this引用。
  2. 操作数栈 (Operand Stack)

    • 一个后进先出(LIFO)的栈,用于保存计算过程的中间结果,并作为计算过程中变量临时的存储空间。
    • Java虚拟机的解释执行引擎是基于栈的执行引擎,此处的“栈”就是指操作数栈。
  3. 动态链接 (Dynamic Linking)

    • 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用。
    • 作用是将符号引用(代码中的方法名、变量名)转换为直接引用(内存地址)。
  4. 方法返回地址 (Return Address)

    • 当一个方法执行完毕后,需要返回到调用它的地方。方法返回地址就存放了调用该方法的指令的下一条指令的地址。

本地方法栈 (Native Method Stack)

本地方法栈与虚拟机栈所发挥的作用非常相似,其区别是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。在HotSpot虚拟机中,直接将本地方法栈和虚拟机栈合二为一。


线程共享区域

所有线程共享这些数据区域,在虚拟机启动时创建。

堆 (Heap)

堆是Java虚拟机所管理的内存中最大的一块,是所有线程共享的区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都要在堆上分配。

内存划分(分代模型)

为了高效地进行垃圾回收,HotSpot虚拟机将堆在逻辑上划分为:

  • 新生代 (Young Generation)
    • 新创建的对象首先被分配到这里。
    • 分为一个伊甸园区(Eden Space)和两个幸存者区(Survivor Space S0/S1)。绝大多数对象在Eden区生成,经过Minor GC后存活的对象会在两个幸存者区之间移动。
  • 老年代 (Old Generation)
    • 存放生命周期较长的对象,通常是经过多次新生代GC后仍然存活的对象。

对象分配与TLAB

  • 对象的创建非常频繁,在并发环境下直接在堆上分配内存需要加锁,影响效率。
  • TLAB (Thread Local Allocation Buffer,线程本地分配缓冲区) 是一个优化措施。虚拟机为每个线程在新生代的Eden区预先分配一小块内存,作为该线程的私有分配缓冲区。
  • 当需要分配对象时,线程首先在自己的TLAB中分配,这样可以避免多线程间的锁竞争,提升分配效率。TLAB用完后,再通过加锁的方式去Eden区申请。

逃逸分析与栈上分配

理论上所有对象都应在堆上分配,但通过即时编译器(JIT)的逃逸分析技术,情况有所改变。

  • 逃逸分析:分析一个对象的动态作用域,判断其是否可能“逃逸”出创建它的方法。
  • 如果一个对象没有发生逃逸,编译器可以进行优化:
    • 栈上分配:直接在栈上为对象分配内存,方法执行结束后,对象随栈帧一起销毁,无需GC介入,极大提升性能。
    • 标量替换:将一个聚合量(对象)拆散成多个标量(基本数据类型)来存储,不创建这个对象,而是将它的成员变量直接在栈上分配。
    • 同步消除(锁消除):如果一个对象不会被其他线程访问,那么对这个对象的所有同步操作都可以安全地消除掉。

方法区 (Method Area)

方法区用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器(JIT)编译后的代码缓存等数据。

方法区的实现演进

方法区是一个逻辑概念,不同的虚拟机有不同的实现。在HotSpot虚拟机中,它的实现经历了演变:

JDK 版本 实现方式 存储内容
JDK 1.6 及之前 永久代 (PermGen) 类型信息、常量、静态变量、字符串常量池都存放在永久代。永久代在JVM堆中。
JDK 1.7 永久代 (PermGen) 字符串常量池和静态变量被移出永久代,放到了Java堆中。
JDK 1.8 及之后 元空间 (Metaspace) 永久代被彻底移除,由元空间取代。元空间使用本地内存(Native Memory),不再占用JVM堆内存。类型信息、方法信息等存放在元空间,但字符串常量池和静态变量仍在Java堆中。

移除永久代的原因

  • 永久代大小难以设定,容易导致OutOfMemoryError
  • 切换到元空间使用本地内存,其大小只受限于物理内存,降低了OOM的风险,也简化了Full GC的过程。

运行时常量池 (Runtime Constant Pool)

  • 它是方法区的一部分,是每个类或接口的常量池在运行时的表现形式。
  • 用于存放编译期生成的各种字面量和符号引用。
  • 相对于Class文件常量池,运行时常量池具有动态性,例如String.intern()方法就可以在运行时将新的常量放入池中。