对象创建
本文基于 Hotspot 虚拟机,其他 JVM 行为可能会有区别
本文中的 Eden 区泛指各 GC 中新对象的分配区域,如 G1 的 Eden Region、ZGC 的分配 Page 等
本文行为均基于默认行为,某些 JVM 参数会导致行为不同
对象创建大致可以分为如下几个步骤
元数据检查 -> 内存分配 -> 设置对象头 -> 对象内容初始化
详解
通过 new 指令方式创建类实例的流程图如下所示
内存分配
否
是
是
充足
不足
否
剩余空间小于浪费阈值
剩余空间大于浪费阈值
遇到 new 指令
类元数据已存在?
执行类加载
Loading - Linking - Init
启用 TLAB?
TLAB 余额充足?
TLAB 内分配
无锁指针碰撞
对象大小
vs TLAB剩余空间?
CAS 竞争申请新 TLAB
然后分配
CAS 竞争堆内存
直接分配
空间清零
TLAB预清零 / 分配后清零
设置对象头
MarkWord + KlassPtr
new 指令完成
对象引用入栈
继续执行字节码
invokespecial
触发构造方法
对象真正可用
元数据检查
检查当前命令能否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否被加载解析和初始化过,如果没有的话会先进行类加载过程。
在元数据中会拿到对象所需内存的大小
内存分配
堆内存格局和 gc 强相关,此处尽量不涉及具体的内存格局细节
堆不是线程独占内存,同时内部对象会频繁的被 gc 清理,因此内存分配大致来说有两个问题,一是分配到哪块内存,二是怎么保证分配的并发安全
指针碰撞
在绝大部分 gc 中,Eden 区都是一块至少局部规整的内存。换句话说有某个地址作为分界点,使用过的内存在一边,空闲的内存在另一边。那么分配内存就只是把分界地址的指针往空闲方向挪动一段与对象所需内存大小相等的距离,这种分配方式就是指针碰撞。
空闲列表
CMS 是非压缩 GC,无法保证堆局部规整,需要维护一个列表记录内存的占用情况来辅助分配对象,这种分配方式就是空闲列表(实际上还是有机制保证局部规整的,但是 CMS 都废弃了就不深入了)
CAS
简单来说就是通过硬件的 CAS 原子操作与自旋实现并发安全,下图是碰撞指针的 CAS 分配示例
CAS 原子操作
准备工作
是
否
利用最新 Top
直接重算
开始分配
读取/更新快照
snapshot = 当前 Top
计算期望新指针
new_top = snapshot + size
比较:
内存 Top == snapshot ?
匹配:
Top 更新为 new_top
不匹配:
Top 未修改
获取最新 Top
分配成功
返回 snapshot 地址
TLAB
TLAB(Thread Local Allocation Buffer),简单来说就是给线程预分配一块内存,当新对象可以直接放到 TLAB 中时就可以直接无锁分配。
TLAB 本身的内存申请也是通过 CAS 实现的。
空间清零
Java 语言规范规定的所有字段在未赋值前,必须有默认值,因此对象字段区域需要进行清零操作
这个步骤在流程图上标记在了设置对象头之前,但是实际上不同分配方式该步骤执行时机不同
- 堆上直接分配:同步清零
- TLAB 分配:TLAB 申请时批量清零整个 TLAB 块
设置对象头
设置对象的元数据
做完这个步骤后在 vm 层面对象就创建完了,只是对象内容还没有进行初始化
对象内容初始化
执行 <init>() 方法初始化对象内容
其他创建方式
除了标准构造(new 或者 反射)外,其他构造方式(克隆、反序列化、Unsafe)流程在在 vm 层面流程极其类似(clone略特殊,他分配内存时不进行空间清零步骤而是直接拷贝内存),只是对象内容初始化方式不同
| 创建方式 | 字节码/API | 是否调用 <init> |
内存初始化方式 | 内容初始化方式 | 备注 |
|---|---|---|---|---|---|
| new 关键字 | new |
是 | 清零 | 构造方法 | 最标准的创建方式 |
| 反射 | Class.newInstance Constructor.newInstance |
是 | 清零 | 构造方法 | 最终调用 new 流程,但在 JVM 层面有安全检查开销 |
| 克隆 | clone() |
否 | 内存复制 | - | 内存初始化完内容也就初始化完了 |
| 反序列化 | ObjectInputStream.readObject |
视情况 | 清零 | 序列化流 | 通常调用第一个不可序列化父类的无参构造 |
| Unsafe | Unsafe.allocateInstance |
否 | 清零 | - |