# 四、内存分配
# 1. JVM 简化架构
# 程序计数器
- 每个线程都拥有一个 PC 寄存器,是线程私有的。用来存储指向下一条指令的地址。
- 在创建线程的时候,同时会创建相应的 PC 寄存器。
- 执行本地方法(JNI)时,PC 寄存器的值为 undefined。
- PC 寄存器是一块较小的内存空间,是唯一一个在 JVM 规范中没有规定 OutOfMemoryError 的内存区域。
# 虚拟机栈
- 栈由一系列帧(Frame)组成,是线程私有的。
- 帧用来保存一个方法的局部变量、操作数栈(Java 没有寄存器,所有参数传递使用操作数栈)、常量池指针、动态链接、方法返回值等。
- 每一次方法调用创建一个帧,并压栈。退出方法的时候,修改栈顶指针就可以把栈帧中的内容销毁。
- 局部变量表存放了编译期可知的各种基本数据类型和引用类型,每个 slot 存放 32 位数据,long、double 占 2 个槽位。
- 栈的优点:存取速度比堆快,仅次于寄存器。
- 栈的缺点:存在栈中的数据大小、生存期是在编译期决定的,缺乏灵活性。
# 堆
- 用来存放应用系统创建的对象和数组,所有线程共享 Java 堆。
- GC 主要就管理堆空间,对分代 CG 来说,堆也是分代的。
- 堆的优点:运行期动态分配内存大小,自动进行垃圾回收。
- 堆的缺点:效率相对较慢。
# 方法区
- 方法区是线程共享的,通常用来保存装载的类的结构信息。
- 常量池
- 字段、方法
- 特殊方法
- static 静态变量
- 通常和元空间关联在一起,但具体的跟 JVM 实现和版本有关。
- 🔺 String 在 JDK7 之前是放在方法区的,但是 JDK7 以后就移到堆了。
- JVM 规范把方法区描述为堆的一个逻辑部分,但它有一个别名称为 Non-heap(非堆),主要是为了与 Java 堆区分开。
# 运行时常量池
- 是 Class 文件中每个类或接口的常量池表,在运行期间的表示形式。通常包括:
- 类的版本
- 字段
- 方法
- 接口
- ...
- JDK6 在方法区中分配,JDK7 以后在堆中分配。
- 通常在加载类和接口到 JVM 后,就创建相应的运行时常量池。
# 本地方法栈
- 在 JVM 中用来支持 native 方法执行的栈就是本地方法栈。
# Class 对象是放在方法区还是堆中?
Class对象是存放在堆区的,不是方法区,这点很多人容易犯错。类的元数据(元数据并不是类的Class对象!Class对象是加载的最终产品,类的方法代码,变量名,方法名,访问权限,返回值等等都是在方法区的)才是存在方法区的。
# static 成员变量放在哪里?
方法区。如果是一个对象的话,那么方法区存引用,对象数据放在堆中。
# 方法区、永久代、元空间区别?
方法区:(逻辑上)
逻辑上的东西,是JVM 的规范,所有虚拟机必须遵守的。
是JVM 所有线程共享的、用于存储类的信息、常量池、方法数据、方法代码等。
永久代:(方法区的实现、JDK7 及之前、主要是和元空间对比)
PermGen , 就是 PermGen space ,全称是 Permanent Generation space ,是指内存的永久保存区域。
PermGen space 则是 HotSpot 虚拟机基于 JVM 规范对方法区的一个落地实现,并且只有 HotSpot 才有 PermGen space。
而如 JRockit(Oracle)、J9(IBM) 虚拟机有方法区 ,但是就没有 PermGen space。
PermGen space 是JDK7及之前, HotSpot 虚拟机对方法区的一个落地实现。在 JDK8 被移除。
Metaspace(元空间、JDK8 及之后)
元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
从前的永久代是用来存放 Class、Method 等元信息的区域, 从 JDK8 开始就去掉了。取而代之的是元空间(MetaSpace),元空间并不在虚拟机里面,而是直接使用本地内存。
# 2. 栈、堆、方法区交互关系
# 3. 堆
# 3.1 概述
- Java 堆用来存放应用系统创建的对象和数组,所有线程共享 Java 堆。
- Java 堆内存在逻辑上需要是连续的。
- Java 堆是在运行期动态分配内存大小,自动进行垃圾回收。
# 3.2 结构
- 整个堆大小 = 新生代 + 老年代
- 新生代 = Eden + 存活区
- 新生代用来放新分配的对象。新生代中经过垃圾回收,没有回收掉的对象,会被复制到老年代。
- 老年代存储对象一般比新生代存储对象的年龄大得多。
- 老年代存储一些大对象。
# 3.3 对象的内存布局
以 HotSpot 虚拟机为例,对象在内存中的布局分为以下 3 个部分:
- 对象头:
- Mark Word:存储对象自身的运行数据。如:Hashcode、GC 分代年龄、锁状态标志等。
- 类型指针:对象指向它的类元数据的指针。
- 实例数据:
- 真正存放对象实例数据的地方。
- 对齐填充:
- 这部分不一定存在,也没有什么特别含义,仅仅是占位符。因为 HotSpot 要求对象起始地址都是 8 字节的整数倍,如果不是,就对齐。
# 3.4 对象的访问定位
# ① 使用句柄
Java 堆中会划分一块内存来作为句柄池,reference 中存放句柄的地址,句柄中存储对象的实例数据和类元数据的地址。
- 间接指针,效率慢
- 修改对象引用的时候只需要修改到对象实例数据的指针即可。
# ② 使用指针
Java 堆中会存放访问类元数据的地址,reference 存储的就直接是对象的地址。
HotStop 用的是使用指针。
- reference 直接指向对象,效率快。