JAVA 虚拟机管理的内存会包括以下几个内存区域:
- 程序计数器
- JAVA虚拟机栈
- 本地方法栈
- JAVA 堆
- 方法区
- 运行时常量池
- 直接内存
内存结构图:
程序计数器(Program Counter Register)
程序计数器是一块比较小的内存空间, 可以被看做是当前线程执行的字节码的行号指示器。
字节码解释器是工作时是通过改变这个计数器的值来选取下一条需要执行的字节码指令, 分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来执行
为了线程切换后能够恢复到正确的执行位置, 每个线程都需要有一个独立的计数器, 每条线程之间的计数器需要互不影响, 独立存储, 所以我们称程序计数器是**"线程私有"**的内存区域。
如果线程正在执行的是一个JAVA方法, 那么程序计数器记录的就是正在执行的字节码指令地址。
如果线程正在执行的是Native方法, 这个计数器值则为空(Undefined)
程序计数器是唯一不会出现OOM异常情况的区域
JAVA虚拟机栈(Java Virtual Machine Stacks)
JAVA虚拟机栈描述的是JAVA方法执行的内存模型:
每个方法在执行的时候都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口信息等。
局部变量表存放了编译期可知的8种基本数量类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型, 不等同于对象本身)和returnAddress(指向了一条字节码指令的地址)类型。
局部变量表所需内存空间在编译器就完成了分配。方法运行期不会改变局部变量表的大小。
64位长度的long 和 duoble 会占用2个局部变量空间(Slot), 其余的数据类型都只占用一个
每一个方法从调用直到执行完成的过程, 就对应着一个栈帧在虚拟机栈中入栈到出栈的过程
与程序计数器一样, JAVA虚拟机栈也是**"线程私有"**的内存区域。它的生命周期与线程一致。
JAVA虚拟机栈可能会出现两种异常:
-
StackOverFlowError异常
线程请求的栈深度大于虚拟机允许的深度, 会抛出这个异常
-
OutOfMemoryError异常
虚拟机栈动态扩展时无法申请到足够的内存时, 会抛出这个异常
本地方法栈(Natice Method Stack)
本地方法栈与虚拟机栈的作用非常的相似,区别在于虚拟机栈为虚拟机执行JAVA 方法(字节码)服务, 而本地方法栈则为虚拟机使用到的Native 方法服务。
本地方法栈是"线程私有"的内存区域
与虚拟机栈一样, 本地方法栈也会抛出StackOverFlowError 和 OutOfMemoryError 异常
JAVA 堆(Java Heap)
JAVA 堆内存区域的唯一目的就是存放JAVA 对象实例, 几乎所有的对象实例以及数组都会在这里分配内存。
💡随着JIT编译器的发展与逃逸分析技术, 栈上分配、标量替换等优化技术让所有对象都在堆上分配不再那么绝对了
从内存回收的角度看, JAVA 堆可以分为新生代和老年代; 再细致一点有Eden 区、From Survivor区、To Survivor 区等, 堆分代结构图:
JAVA堆是"线程共享"的内存区域
如果JAVA堆中没有空间完成实例内存分配, 并且堆无法再扩展时, 会抛出OutOfMemoryError 异常
方法区(Method Area)
方法区主要用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
它还有一个别名Non-Heap(非堆), 目的就是与JAVA 堆进行区分开来。
方法区和堆一样也是"线程共享"的内存区域
会抛出OutOfMemoryError 异常
运行时常量池(Runtime Constant Pool)
运行时常量池是方法区的一部分, 用于存放编译器生成的各种字面量和符号引用
运行期间也可能将新的常量放入到常量池中
会抛出OutOfMemoryError 异常
直接内存(Direct Memory)
直接内存并不是虚拟机运行时数据区的一部分, 也不是虚拟机规范定义的内存区域
在JDK1.4中新加入了NIO(new Input/Output) 类, 引入了一种基于通道(Channel) 与 缓冲区(buffer) 的I/O方式, 它可以使用Native函数库直接分配堆外内存, 然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作
这样操作能避免来回在Java堆和Native堆复制数据, 从而显著提高性能。
这部分内存也会被频繁的使用, 也会抛出OutOfMemoryError 异常
总结
- 程序计数器
- 保存的是线程正在执行的字节码指令地址
- 线程私有的区域
- 唯一不会抛出OutOfMemoryError 异常的区域
- JAVA虚拟机栈
- 用于存储局部变量表、操作数栈、动态连接、方法出口信息等。
- 线程私有的区域
- 会抛出StackOverFlowError 和 OutOfMemoryError 异常
- 本地方法栈
- 功能与虚拟机栈相似, 主要是为虚拟机使用到的Native 方法服务
- 线程私有的区域
- 会抛出StackOverFlowError 和 OutOfMemoryError 异常
- JAVA 堆
- 存放JAVA 对象实例和数组实例
- 线程共享的区域
- 会抛出OutOfMemoryError 异常
- 方法区
- 主要存放类的结构信息, 常量信息, 字段信息,静态变量,方法数据,构造函数, 普通方法的字节码信息、即时编译器编译后的代码等信息
- 线程共享的区域
- 会抛出OutOfMemoryError 异常
- 运行时常量池
- 用于存放编译器生成的各种字面量和符号引用
- 是方法区的一部分, 是线程共享的区域
- 会抛出OutOfMemoryError 异常
- 直接内存
- 不是虚拟机规范定义的内存区域, 使用Native函数库直接分配堆外内存
- 会抛出OutOfMemoryError 异常