JVM系列 - 运行时数据区域
Last updated: Oct 289, 28089
概述
JVM 在运行 Java 程序时会把内存划分为若干个不同的数据区域,包括:
- 程序计数器 - Program Counter Register
- 虚拟机栈 - VM Stack
- 本地方法栈 - Native Method Stack
- Java 堆 - Heap
- 方法区 - Method Area
程序计数器
线程私有
可以看作 当前线程所执行的字节码的行号指示器
每个线程都需要有独立的程序计数器,互不影响,独立存储
Java 虚拟机栈 & 本地方法栈
线程私有
生命周期与线程相同,描述 Java 方法执行的线程内存模型
每个方法被调用至执行完毕,就对应着栈帧在 虚拟机栈 中的入栈和出栈
栈帧中包含的局部变量表存放了编译期可知的:
- 基本数据类型
- 对象引用(引用指针或对象句柄)
- returnAddress 类型(指向一条字节码指令的地址)
区别:
虚拟机栈:为虚拟机执行 Java 方法服务
本地方法栈:为虚拟机执行 本地(Native)方法服务
异常
- 线程请求的栈深度大于虚拟机所允许的深度,抛出 StackOverflowError 异常
- 如果虚拟机栈容量可以动态扩展,则当栈无法申请到足够内存,就报 OOM
Java 堆
线程共享
目的:存放 对象实例 以及 数组
从内存回收的角度
由于现代垃圾收集器大部分都是基于 分代收集理论 设计的,
所以 Java 堆中经常会出现:
- 新生代
- 老年代
- 永久代
- Eden空间
- From Survivor 空间
- To Survivor 空间
等名词,这些划分适用于 HotSpot 虚拟机,因为它是基于 经典分代 设计的
从分配内存的角度
所有线程共享的 Java 堆可以划分出多个 线程私有 的 分配缓冲区,称为:
TLAB (Thread Local Allocation Buffer),提升对象分配时的效率
异常
可以通过参数 -Xmx 和 -Xms 设定 Java 堆的最大值和初始值
如果堆大小无法扩展,无法完成实例的分配,就会抛出 OOM 异常
方法区
线程共享
存储:
- 加载类的类型信息
- 常量
- 静态变量
- 即时编译器编译后的的代码缓存
JDK 8 之前,使用 永久代 实现 方法区,但二者并不等价
JDK 8 之后,放弃 永久代,改用 本地内存 实现 元空间,主要存放 类型信息
字符串常量池 和 静态变量 等等也被移出,到哪?
内存回收
主要针对 常量池的回收 和对 类型的卸载
异常
如果方法区无法满足新的内存分配需求,将抛出 OOM 异常
运行时常量池
Runtime Constant Pool 是方法区的一部分
Class 文件中包含的 常量池表(Constant Pool Table)存放:
- 编译时生成的各种字面量
- 符号引用
这部分内容在类加载后存放到方法区的运行时常量池中
特点
相较于 Class 文件常量池,运行时常量池具备 动态性
即在运行期间,允许将新的常量放入池中,比如 String 类的 intern() 方法
异常
当常量池无法申请到内存时,会抛出 OOM
直接内存
不是 虚拟机运行时数据区的一部分,但因为被频繁使用,也会导致 OOM 异常
Java NIO 可以使用 Native 函数库 直接分配 堆外内存,
通过 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作
因为避免了数据在 Java 堆和 Native 堆之间频繁复制,某些场景可以提高性能
需要考虑 本机总内存,防止 JVM 中所有内存与直接内存总和过大造成 OOM