JVM 内存结构
线程私有区域
这些区域的生命周期与线程相同,随线程的创建而创建,随线程的销毁而销毁。
程序计数器 (Program Counter Register)
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。
- 作用:记录当前线程正在执行的字节码指令的地址。执行引擎通过读取它的值来确定下一条要执行的指令。
- 特性:
- 线程私有,确保线程切换后能恢复到正确的执行位置。
- 是唯一一个在Java虚拟机规范中没有规定任何
OutOfMemoryError情况的区域。 - 如果线程正在执行的是一个Java方法,计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则为空(Undefined)。
Java虚拟机栈 (Java Virtual Machine Stack)
每个方法在执行的同时都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
- 特点:
- 线程私有,生命周期与线程一致。
- 是Java方法执行的内存模型。
- 可能出现两种异常:
StackOverflowError:如果线程请求的栈深度大于虚拟机所允许的深度。OutOfMemoryError:如果虚拟机栈可以动态扩展,但在扩展时无法申请到足够的内存。
栈帧的内部结构
-
局部变量表 (Local Variable Table)
- 存放方法参数和方法内部定义的局部变量。
- 基本存储单位是槽(Slot)。
long和double类型的变量会占用2个槽,其他基本数据类型和引用类型占用1个槽。 - 对于实例方法,第0个槽(
slot[0])默认用于存放this引用。
-
操作数栈 (Operand Stack)
- 一个后进先出(LIFO)的栈,用于保存计算过程的中间结果,并作为计算过程中变量临时的存储空间。
- Java虚拟机的解释执行引擎是基于栈的执行引擎,此处的“栈”就是指操作数栈。
-
动态链接 (Dynamic Linking)
- 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用。
- 作用是将符号引用(代码中的方法名、变量名)转换为直接引用(内存地址)。
-
方法返回地址 (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()方法就可以在运行时将新的常量放入池中。