HotSpot 虚拟机 Java 堆中对象的分配、布局和访问
0. 前提
- new 关键字,排除复制、反序列化
- 普通 Java 对象,排除数组和 Class 对象
1. 分配过程
1. 检查 new 指令的参数是否能在常量池中找到一个类的符号引用,并且检查是否已被加载、解析和初始化过
- 否则执行类加载过程
2. 为新生对象分配内存
-
如何分配
-
根据所用的垃圾回收器是否有空间压缩整理功能决定
-
方式
-
指针碰撞
Bump The Pointer- 内存空间是规整的,一边正在使用的,一边空闲的,边界处有一个指针,分配内存只是把指针往空闲的那部分移动与对象大小相等的距离
-
空闲列表
Free List- 不是规整的,分配的时候在空闲列表上找到一块足够大的空间,更新列表记录
-
-
-
考虑高并发
-
方案一
-
分配内存进行同步处理:CAS+ 失败重试保证更新操作的原子性
- Atomic::cmpxchg_ptr()
goto retry
- Atomic::cmpxchg_ptr()
-
-
方案二
- 每个线程在 Java 堆中预先分配一小块内存,称为本地线程分配缓冲区(Thread Local Allocation Buffer,TLAB),TLAB 用完后才会同步锁分配内存
- 是否启用 TLAB,可以通过-XX:+/-UseTLAB 指定
-
3. 零值初始化
- 不包括对象头
- 如果使用 TLAB 可以提前到 TLAB 分配时顺便进行
- 保证对象实例字段在 Java 代码中不赋初始值就能直接使用,使程序能访问到字段得数据类型对应的零值
4. 对象头 Object Header 初始化
-
根据虚拟机当前运行状态的不同,会有不同的设置方式
-
对象头内容
- 对象是哪个类的实例
- 如何找到类的元数据信息
- 对象的哈希码
- 对象的 GC 分代年龄等信息
- ......
5. 执行构造函数
-
一般来说会执行 Class 文件中的
()方法 -
父类子类执行顺序
- 父类静态代码块
- 子类静态代码块
- 父类代码块
- 父类构造函数
- 子类代码块
- 子类构造函数
2. 对象内存布局
对象头 Header
-
Mark Word
-
类型指针 Class Pointer
- 指向类型元数据的指针
- 虚拟机通常用这个来确定对象是哪个类的实例
-
数组长度 length
- 如果是数组的话才需要
- 普通 Java 对象可以直接确定大小,数组长度不固定的话,没法只通过元数据推断出数组大小
实例数据 Instance Data
-
保存对象的所有字段,不管是父类还是子类
- 默认顺序:
long/double, int, short, char, byte/boolean, oop (Ordinary Object Pointers)
- 默认顺序:
-
存储顺序一般是长度一样的存一起,与代码中定义的顺序也有关系
-
-XX:FieldsAllocationStyle
-
+XX:CompactFileds,默认为 true
- 允许子类中比较窄的变量插入到父类变量的空隙之中,节省一点点空间
-
对齐填充 Padding
- HotSpot 规定任何对象大小都必须是 8 字节的整数倍,对象头已经设计为 8 字节的整数倍,如果实例数据部分没有对齐的话需要通过对齐填充补全
3. 对象访问
通过 Java 栈(本地变量表)上的 reference 数据来操作对上的具体对象,由虚拟机实现具体方式
主流方式
-
句柄
- reference 存储的是对象实例数据和对象类型数据
- Java 堆中单独开辟一块区域存放句柄,访问对象实例数据需要两次跳转
-
直接指针
- reference 存储的直接就是是对象地址,可以直接访问到对象示例数据,访问对象类型数据才需要两次跳转
- 因为通常只是访问对象,而不是访问对象的类型数据
补充
查看字节码的插件
- jclasslib
对象类型数据
- 对象类型数据就是被虚拟机加载的类信息(即 Class 信息,见 2.2.5 方法区)
对象实例数据
- 对象实例数据就是被 new 出来的对象信息。