摘要: JVM 内存的划分, 导致内存溢出异常的可能区域
1. JVM 运行时内存区域
JVM 在执行 Java 程序的过程中会把它所管理的内存划分为以下几个区域:
1.1 程序计数器
程序计数器是一块较小的内存空间, 在 Java 虚拟机规范中是唯一一个未规定 OutOfMemoryError 的内存区域
程序计数器可看作当前线程所执行字节码的行号指示器字节码解释器工作时通过改变计数器的值来选取下一条待执行的字节码指令, 分支循环跳转异常处理线程恢复等基础功能都需要依赖计数器来完成
每个线程均有一个独立的程序计数器若线程执行的是一个 Java 方法, 计数器记录的值是正在执行虚拟机字节码地址; 若执行的是 Native 方法, 计数器值为空(Undefined)
1.2 Java 虚拟机栈
Java 虚拟机栈也就是我们经常说的 "栈"(其实这个说法不够严谨<^_^>), 它是线程私有的, 生命周期与线程相同
虚拟机栈描述的是 Java 方法执行的内存模型: 每个方法执行时都会创建一个栈帧, 用于存储局部变量栈操作数栈动态链接方法出口等信息每个方法从调用到执行完成的过程, 都对应者一个栈帧在虚拟机栈中入栈到出栈的过程
局部变量表存放了编译期可知的 8 种基本数据类型对象引用(reference 类型, 指向对象起始地址的引用指针或代表对象的句柄或其他与此对象相关的位置)returnAddress 类型(指向一条字节码指令的地址)
Java 虚拟机规范对这块区域规定了两种异常情况:
StackOverflowError
线程请求的栈深度大于虚拟机所允许的深度, 将抛出此异常;
OutOfMemoryError
若虚拟机栈可以动态扩展, 但在扩展时无法申请到足够的内存, 将抛出此异常;
1.3 本地方法栈
与虚拟机栈类似, 但它执行的 Native 方法服务
1.4 Java 堆
"GC" 堆, 虚拟机启动时创建, 为所有线程共享的一块区域在 Java 虚拟机规范描述中, 所有对象实例和数组都要在堆上分配(随着 JIT 编译器的发展和逃逸技术的逐渐成熟, 所有对象在堆上分配也变得不是这么绝对)
如果堆中剩余内存不足以完成实例的分配, 且无法再扩展, 将抛出 OutOfMemoryError
1.5 方法区
"Non-Heap", 存储已被虚拟机加载的类信息常量静态变量, 即时编译器编译后的代码等数据
根据 Java 虚拟机规范的规定, 当方法区无法满足内存分配需求时, 将抛出 OutOfMemoryError
1.6 运行时常量池
方法区的一部分, 在类加载后进入方法区时, 存放编译期生成的各种字面量和符号引用
在运行期间, 也可能将新的常量放入池中, 如 String 常用的 intern()方法
当无法满足内存分配需求时, 将抛出 OutOfMemoryError
1.7 直接内存
这部分区域并不是虚拟机运行时数据区的一部分, 也非 Java 虚拟机规范中定义的内存区域, 但也被频繁使用, 亦可能抛出 OutOfMemoryError
直接内存主要为 NIO(JDK1.4 加入的类)使用, 通过存储在 Java 堆中的 DirectByteBuffer 对象对这块内存的引用进行操作在某些场合能显著提高性能, 避免了 Java 堆和 Native 堆中来回复制数据
2. HotSpot 虚拟机对象
讨论 Java 堆中普通对象分配布局和访问的过程
2.1 对象的创建
虚拟机遇到一条 new 命令时, 首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用, 并检查这个符号引用代表的类是否已被加载解析和初始化过, 若没有, 则先执行相应的类加载过程;
在类加载检查通过后, 虚拟机将为新生对象分配内存:
指针碰撞
若 Java 堆中的内存是绝对规整的, 所有用过的和空闲的内存都各放一边, 中间放着一个指针作为分界点的指示器, 那分配内存仅仅是将指针向空闲空间方向移动一段与对象大小相等的距离;
空闲列表
Java 堆中的内存并不规整, 此时虚拟机必须维护一个列表, 记录哪些内存块是可用的, 在分配的时候从列表上找到一块足够大的空间划分给对象实例, 并更新列表上的记录
并发情况下, 在堆上分配内存, 可能出现内存分配的同步问题, 解决方案有两个, 一个就是同步内存 (CAS) 分配动作; 另一个就是采用 TLAB(本地线程分配缓冲), 即在 Java 堆中针对每个线程先预先分配一小块内存这样当线程需要分配内存时就在自己的 TLAB 上进行, 从而避免同步的开销但是当 TLAB 分配满重新分配 TLAB 时仍需要同步在内存分配完成后, 虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)
虚拟机将要对对象进行必要的设置, 如这个对象是哪个类的实例, 如何找到类的元数据信息对象的 hashCode 和 GC 分代年龄等信息这些信息存放在对象的对象头中
最后执行 init 方法, 从而产生真正可用的对象
2.2 对象的内存布局
在 HotSpot 虚拟机中, 对象在内存的存储的布局可分为 3 块区域: 对象头实例数据对齐填充
对象头
Mark Word
存储对象自身的运行时数据, 如 hashCodeGC 分代年龄锁状态标志线程持有的锁偏向线程 ID 偏向时间戳等;
类型指针
对象指向它的类元数据的指针, 虚拟机通过这个指针确定这个对象是哪个类的实例
实例数据
程序代码中所定义的各种类型的字段内容, 包括从父类继承下来的
对齐填充
并不是必然存在, 仅仅起着占位符作用, 以满足 HotSpot VM 自动内存管理系统对对象起始地址必须是 8 字节的整数倍要求
2.3 对象的访问定位
Java 程序通过栈上的 reference 数据来操作堆上的具体对象主流的访问方式:
* 句柄
Java 堆中将划分一块内存用作句柄池, reference 中存储的就是对象的句柄地址, 而句柄包含了对象实例数据与类型数据各自的具体地址信息
优点: 当对象移动时, 只需要改变句柄的指针即可, 而相应的 reference 则不需要做变动;
直接指针
reference 存储的是对象地址, 通过 reference 就能访问数据
优点: 访问速度快, 相对句柄的方式而言少了一次指针定位的开销 HotSpot VM 使用的是此种方式
参考文献
深入理解 Java 虚拟机: JVM 高级特性与最佳实践(第 2 版)
来源: https://www.cnblogs.com/oomblog/p/8472063.html