1 程序计数器
- 字节码的行号指示器
- 寄存器,寄存了字节码解释器(两种执行引擎的一种)下一行要执行的字节码指令
- 线程私有
- 原因:jvm的多线程需要线程切换
- 当执行的是本地方法时,计数器为空
- 无OutOfMemoryError
2 Java虚拟机栈
-
线程私有
-
由栈帧组成
- 每个方法执行时,JVM会创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息。
- 栈帧的入栈和出栈过程对应着方法的调用和执行完成。
-
栈帧中的局部变量表
-
存放基本数据类型、对象引用和returnAddress类型(指令地址)。
-
数据存储以变量槽(Slot)表示,其中long和double占用两个变量槽,其他类型占用一个。
-
局部变量表所需的空间在编译期分配,运行期间大小不变,大小指的是变量槽的数量。
-
-
内存异常:
- StackOverflowError:栈深度溢出
- OutOfMemoryError:栈扩展失败
3 本地方法栈
- 与虚拟机栈类似,区别是为本地(Native)方法服务
- 线程私有
- 内存异常:
- StackOverflowError:栈深度溢出
- OutOfMemoryError:栈扩展失败
4 Java堆
- 线程共享
- 在虚拟机启动时创建
- JVM管理的最大的一块内存区域
- 用于存放对象实例:
- 几乎所有对象实例和数组都在堆上分配。
- 随着Java语言的发展,例如即时编译和逃逸分析技术,一部分对象可能在栈上分配,但主要是堆分配。
- 由垃圾收集器管理:
- Java堆又被称为GC堆(Garbage Collected Heap)。
- 基于分代收集理论,堆通常分为新生代、老年代等区域,但这些划分并非Java虚拟机实际的内存布局,而是部分垃圾收集器共同的设计风格。
- TLAB(Thread Local Allocation Buffer):
- Java堆中可以有多个线程私有的TLAB,用于提高对象分配效率。
- 物理和逻辑性质:
- 物理上可以不连续,但逻辑上被视为连续。
- 对大对象(如数组),多数JVM实现可能要求连续的内存空间。
- 具体实现可以是固定大小也可以是可扩展的,但主流为可扩展。
- 内存异常:
- OutOfMemoryError:如果堆无法分配更多实例且无法扩展。
5 方法区
- 线程共享
- 用于存储虚拟机加载的类型信息、常量、静态变量、即时编译后的代码等
- 虽然《Java虚拟机规范》将方法区描述为堆的逻辑部分,但它通常被视为“非堆”(Non-Heap)以与Java堆区分。
- 永久代与方法区的关系:
- 在JDK 8以前,HotSpot虚拟机中的方法区常被称为“永久代”(Permanent Generation),但这两者并不等价。
- 永久代是旧版本JDK中HotSpot特有的实现,用于便于垃圾收集器管理这部分内存。
- 到了JDK 8,已经完全废弃了永久代的概念,改用本地内存中的元空间(Metaspace)来实现方法区,与JRockit和J9虚拟机一致。
- 《Java虚拟机规范》对方法区的规定相对宽松,可以不连续,可以固定大小或可扩展,可以不实现垃圾收集。
- 内存异常:
- OutOfMemoryError:当方法区无法满足内存分配需求时
6 运行时常量池
- 运行时常量池是方法区的一部分
- 类文件中的常量池表(包含编译期生成的字面量和符号引用)在类加载后被存放到方法区的运行时常量池中
- 运行时常量池除了保存Class文件中的符号引用,还会存储这些引用翻译出来的直接引用。
- Java虚拟机规范对于运行时常量池的具体实现细节并没有明确要求。
- 运行时常量池具备动态性,即在运行期间也可以将新的常量加入池中,例如
String
类的intern()
方法的使用。 - 内存异常:
- 由于运行时常量池是方法区的一部分,它受到方法区内存的限制。
- OutOfMemoryError:当运行时常量池无法再申请到足够的内存时
7 字符串常量池
- 仅在概念上是运行时常量池的一部分
- 在Java 6及更早版本中,字符串常量池物理上位于永久代,即在物理上也是方法区的一部分
- 但从Java 7开始,字符串常量池就被移动到Java堆内存中。并且,当java 8中永久代彻底被元空间取缔时,字符串常量池依旧位于Java堆内存中。