JAVA内存区域与内存溢出异常
1. 运行时数据区域
注意: 区域大小并不代表实际虚拟机中区域大小
多线程是通过线程轮流切换, 分配处理器时间片的方式来实现的.
1.1 程序计数器
描述: 当前线程所执行的字节码的行号指示器. 工作是通过改变这个计数器的值来选取吓一跳需要执行的字节码指令, 是程序控制的指示器. 通俗的说就是程序计数器指向(行号)指向哪条指令, 计算机就执行哪条指令
特点:
- 线程私有, 生命周期与线程相同
- 线程执行
Java
方法时, 该线程的计数器记录的是正在执行的虚拟机字节码指令的地址 - 线程执行本地方法时, 该线程的计数器的值为空(
Undefined
) - 程序计数器区域时唯一一个没有规定任何
OutOfMemoryError
情况的区域
1.2 Java
虚拟机栈
描述: 是Java
方法执行的线程内存模型: 每个方法被执行的时候, Java
虚拟机都会同步创建一个栈帧用于存储局部变量表, 操作数栈, 动态连接, 方法出口等信息. 每个方法被调用直至执行完毕的过程, 就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程(栈结构特点: 先进后出)
局部变量表存放了编译期间可知的各种虚拟机基本数据类型, 对象引用(regerence
类型, 并不是对象本身, 可能是指针或者句柄),returnAddress
类型(指向了一条字节码指令的地址)
上述数据类型自局部变量表中的存储空间以局部变量槽(Slot
)表示, 64位的long
和double
类型数据会占用两个变量槽, 其余数据类型只占一个. 局部变量表所需的内存空间在编译期间完成分配, 当进入方法时, 这个方法需要在栈帧中分配多大的局部变量空间是完全确定的, 在方法运行期间不会改变局部变量表的大小(变量槽的数量)
特点:
- 线程私有, 生命周期与线程相同
- 线程请求的栈深度大于虚拟机所允许的深度, 建抛出
StackOverFlowError
异常 Java
虚拟机栈容量可以动态扩展(注意HorSpot
虚拟机栈容量不可以动态扩展), 当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError
异常
1.3 本地方法栈
描述: 作用于虚拟机栈相似, 区别是虚拟机栈为虚拟机执行Java
方法服务, 本地方法栈为虚拟机用到的本地方法服务
特点:
- 栈深度溢出时抛出
StackOverFlowError
- 栈扩展失败时抛出
OutOfMemoryError
1.4 Java
堆
描述: Java
堆的唯一目的时存放对象实例. "几乎"所有的对象实例都在这里分配内存
特点:
- 是被所有线程共享的一块内存区域, 在虚拟机启动时创建
- 是垃圾收集器管理的内存区域
Java
堆可以处于物理上不连续的内存空间, 但在逻辑上应该被视为连续(链表的特点)Java
堆既可以被实现成固定的大小, 也可以是可扩展的. 如果在Java
堆中没有内存完成实例分配, 并且堆也无法再扩展时, 并且堆也无法再扩展时,Java
虚拟机将会抛出OutOfMemoryError
异常
1.5 方法区
描述: 用于存储已被虚拟机加载的类型信息, 常量, 静态变量, 即时编译器编译后的代码缓存等数据
特点: 如果方法区无法满足新的内存分配需求时, 将抛出OutOfMemoryError
异常
1.6 运行时常量池
描述: 是方法区的一部分. 用于存放编译期生成的各种字面量与符号引用
特点: 受方法区内存限制
1.7 直接内存
描述: 直接内存不是虚拟机运行时数据区一部分, 也不是 虚拟机规范中 定义的内存区域
NIO
中的通道和缓冲区I/O
方式通过Native
函数库直接分配堆外内存
特点: 受本机内存以及处理器寻址空间限制, 会抛出
OutOfMemoryError
2. HotSpot
虚拟机对象
2.1 对象的创建
- 类加载检查
Java
虚拟机遇到一条字节码new
指令时, 首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用, 并检查这个符号引用代表的类是否已被加载, 解析和初始化过程, 如果没有, 则必须执行相应的类加载过程 - 分配内存
对象所需的内存大小在类加载完成后即可完全确定, 为对象分配空间的任务实际上便等同于把一块确定大小的内存块从Java
堆中划分出来
分配方式:
指针碰撞: 假设Java
堆中内存是绝对规整的, 所有被使用过的内存都被放到一边, 空闲的内存放在另一边, 中间放着一个指针作为分界点的指示器, 那所分配内存就仅仅是吧指针向空闲方向挪动一段与对象大小相等距离.
空闲列表: 如果Java
堆中的内存并不是规整的, 已经被使用的内存和空闲的内存相互交错在一起, 虚拟机就必须维护一个列表, 记录哪些内存块是可用的, 分配时从列表中找到一块足够到的空间划分给对象实例, 并更新列表上的记录
分配方式由Java
堆内存是否规整决定,Java
堆是否规整由采用的垃圾收集器是否带有空间压缩整理的能力决定 - 初始化零值
保证对象可以不付出是指就直接使用 - 对对象进行必要设置(初始化对象头)
- 对象初始化内容
new
指令之后会接着执行Class
文件中的<init>()
方法, 按照意愿进行对象初始化
2.2 对象的内存布局
对象在堆内存中的存储布局可以划分为三个部分: 对象头, 实例数据, 对齐填充
- 对象头: 包括两类信息
第一类是用于存储对象自身的运行时数据(Mark Word
). 这部分数据的长度在32位和64位的虚拟机中分配32个比特和64个比特. 包括: 哈希码,GC
分代年龄, 锁状态标志, 线程持有的锁, 偏向线程ID
, 偏向时间戳等
第二类是类型指针, 即对象指向他的类型元数据的指针,Java
虚拟机通过这个指针来确定该对象是哪个类的实例, 如果是数组还包含数组长度信息 - 实例数据: 对象真正存储的有效数据
HotSpot
虚拟机默认的分配顺序(宽度相同的总是被分配到一起):longs/doubles
,ints
,shorts/chars
,bytes/booleans
,oops
(普通对象指正)
在满足上述条件下, 在父类中定义的变量会出现在子类之前 - 对齐填充: 起占位符作用
HotSpot
虚拟机的自动内存管理系统要求对象其实地址必须是8字节的整数倍(任何对象的大小都必须是8字节的整数倍)
2.3 对象的访问定位
通过栈上的reference
数据来操作堆上的具体对象.
访问方式:
- 句柄访问:
Java
堆中划分出一块内存作为句柄池,reference
中存储的就是对象的句柄地址, 而句柄中包含了对象实例数据与类型数据各自具体的地址信息. 优点是reference
中存储的是稳定句柄地址, 在对象被移动时只会改变句柄的实例数据指针,reference
本身不需要修改
- 直接指针访问:
Java
堆中对象的内存布局就必须考虑如何放置访问类型的相关信息,reference
中存储的直接对象就是对象地址, 如果只是访问对象本身的话, 就不需要多一次简介访问的开销. 优点是防卫速度更快, 节省了一次指针定位的时间开销
2.4 OutOfMemoryError
- 区分内存溢出和内存泄漏
内存溢出: 申请的数据空间比实际对象需要的数据空间小, 导致数据向后覆盖
内存泄漏: 使用后未释放内存