Chapter 2 - Java 内存区域与内存溢出异常
Created by : Mr Dk.
2020 / 01 / 24 20:26 🧨🧧
Ningbo, Zhejiang, China
2.1 概述
在 JVM 的自动内存管理机制的帮助下,不再需要为每一个 new
操作分配对应的 delete/free
代码。
2.2 运行时数据区域
JVM 管理的内存被划分为不同区域。
2.2.1 程序计数器 (Program Counter Register)
当前线程所执行的字节码的行号指示器。各条线程之间的计数器互不影响,独立存储,是线程私有的内存。
2.2.2 Java 虚拟机栈 (Java Virtual Machine Stack)
由线程私有,生命周期与线程相同,描述的是 Java 函数执行的线程内存模型。
- 当一个 Java 函数被调用时,JVM 会创建栈帧存储局部变量表等信息
- 调用完毕,栈帧出栈
局部变量表中存放编译期可知的数据:
- JVM 基本数据类型
- 对象引用 (指向对象起始地址的引用指针)
- returnAddress 类型
这些数据在局部变量表中以 局部变量槽 (slot) 来表示,局部变量槽在编译期间完成分配 (栈帧中需要的局部变量大小是可以提前确定的)。
2.2.3 本地方法栈 (Native Method Stacks)
与虚拟机栈发挥的作用类似。
- 虚拟机栈为执行 Java 函数 (字节码) 服务
- 本地方法栈为虚拟机用到的本地函数服务 (意思是非 Java 函数)
2.2.4 Java 堆 (Java Heap)
Java Heap 是 JVM 所管理的内存中最大的一块,是 所有线程共享 的一块内存区域,作用是存放所有的对象实例。Java Heap 是垃圾收集器管理的内存区域,因此也被称为 GC Heap。所有线程共享的 Java Heap 上可以划分出各线程私有的 分配缓冲区 (Thread Local Allocation Buffer, TLAB):
- 提升对象分配时的效率
- 将 Java Heap 细分的目的只是为了更好地回收内存,或更快地分配内存
Java Heap 在物理上可以是不连续的,但在逻辑上是连续的。Java Heap 可以被实现为是固定的,也可以是可扩展的。
2.2.5 方法区 (Method Area)
也是各个线程共享的内存区域,存储 JVM 加载的类型信息、常量、静态变量、JIT 编译器编译后的代码缓存。在实现上,不需要连续内存,大小可扩展,甚至还可以不实现垃圾收集。
2.2.6 运行时常量池 (Runtime Constant Pool)
是 方法区 的一部分。JVM 严格规定了 Class 文件每一部分的格式,其中。常量池表 (Constant Pool Table) 用于存放编译期生成的常量与符号引用。
2.2.7 直接内存
绕过 JVM 分配的内存,不位于 Java Heap 中,不属于运行时数据区。
2.3 HotSpot 虚拟机对象探秘
2.3.1 对象的创建
JVM 遇到 new
指令时,首先去检查指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析、初始化。如果没有,则需要执行类加载。接下来,为新生对象分配内存:
- 将一块固定大小的内存从堆上划分出来
- 类加载后,对象所需要的内存大小可以确定
另外,考虑到分配内存时的线程安全问题,解决方案有:
- 对分配内存空间进行同步:JVM 使用 CAS + 失败重试保证更新的原子性
- 为每个线程在 JVM 中划分私有区域 (TLAB):只有本地缓冲用完,分配新的内存时才需要同步锁定
内存分配完毕后,JVM 将分配的内存空间初始化为 0,保证了对象的字段在 Java 代码中不需要初始化就能直接使用。JVM 对对象进行设置,将信息存放在对象的 Object Header 中,从 JVM 的视角来看,新的对象已经分配完成。但从 Java 代码的视角看,还需要执行对象的构造函数,这样才构造出了一个真正可用的对象。
2.3.2 对象的内存布局
对象在堆内存中的存储布局可以划分为三部分:
- 对象头 (Header)
- 实例数据 (Instant Data)
- 对齐补充 (Padding)
对象头包含两部分信息:
- 存储对象自身的运行时数据 (Mark Word, 32-bit 或 64-bit)
- HashCode
- GC 年龄
- 锁状态
- 持有的锁等
- 类型指针,指向对象类型 metadata 的指针
- JVM 通过这个类来确定该对象是哪个类的实例
- 如果是数组,还额外需要记录数组的长度
实例数据部分存储了对象真正存储的有效信息,包括 Java 代码中定义的各种字段。HotSpot 虚拟机会有一个默认的字段分配顺序,使相同宽度的字段被存放到一起;满足这一条件的前提下,父类中定义的变量会出现在子类之前。由于 HotSpot 虚拟机的自动内存管理系统要求对象起始地址对齐 8B,由于对象头已被设计为固定长度,并对齐 8B,若实例数据不足 8B,将会被填充以对齐 8B。
2.3.3 对象的访问定位
Java 程序通过栈上的对象引用来操作堆上的具体对象。JVM 规范没有定义对象的具体访问方式,由 JVM 的实现决定:
- 句柄访问
- Java Heap 中划出一块内存作为句柄池
- 对象引用存储的是句柄地址
- 句柄中包含对象实例数据与类型数据各自的地址
- 直接指针访问
- 对象引用直接存储对象的地址
句柄访问的优势:在对象被移动 (GC 时很普遍) 时只会改变句柄中的实例数据地址,对象引用不会被修改。
直接指针访问的优势:
- 速度更快,节省了一次指针定位的开销
- HotSpot 使用这种方式进行对象访问