对象内存的分配与布局
- 创建(new、clone、反射、反序列化)
- new -> 常量池定位符号引用,并检查符号引用代表的类是否被加载、解析、初始化。若没有,则执行类加载过程
- 对象所需内存在类加载完成后便可完全确定。
- 对象内存分配方式,由垃圾收集器是否有压缩整理功能决定
- 指针碰撞:堆内存绝对规整,使用的内存与空闲内存通过指针作为分界点。分配内存只需要相应移动指针即可。serial、parNew等带Compact过程的收集器。
- 空闲列表:记录空闲内存地址。分配内存时同时更新列表即可。cms基于Mark-Swap算法的收集器。
- 对象内存分配的线程安全问题
- 分配内存时同步--虚拟机采用CAS加失败重试的方法
- 本地线程分配缓冲(Thread Local Allocate Buffer)
- 堆预先分配给线程一定的内存空间,分配内存时首先使用该部分。
- 只有当TLAB使用完,重新分配时才同步。
- 是否使用,通过 -XX:+/-UserTLAB参数决定
- 内存布局
- 对象头
- MarkWord
- 存放对象自身的运行时数据,比如hashcode、GC分代年龄、锁状态便知、线程持有的锁、偏向线程ID、偏向时间戳
- 在32位、64位的虚拟机中(未开启压缩指针)分别为32bit和64bit
- 非固定的数据结构,根据对象状态复用存储空间
- Klass指针
- 对象指向它的类元数据的指针,通过该指针确定对象是哪个类的实例
- 并不完全通过对象本身,句柄池的句柄同时保存到实例数据和类型数据的指针
- 若对象是Java数组,则需要一块记录数组长度的空间,因为无法通过普通Java对象的元数据信息确定
- MarkWord
- 实例数据
- 对象真正存储的有效信息,即定义的各种类型字段内容
- 存储顺序受虚拟机分配策略和字段在源码中的定义顺序影响
- HotSpot默认分配(相同宽度的字段一起分配):longs/doubles、ints、shorts、chars、bytes/booleans、oops(ordinary object pointers)
- 父类定义的字段在子类之前
- 若CompactFields参数值为true,则 子类较窄的变量会被插到父类变量的空隙间
- 对齐填充
- 占位符作用
- Hotspot规定对象的起始地址必须是8字节的整数倍,即对象必须是8字节的整数倍
- 对象头是8字节的倍数(1倍或2倍),因此该部分是在需要时补齐实例数据的空缺
- 对象头
- 访问定位
- JVM通过栈上的reference数据操作堆上的对象,其本质只是指向对象的引用
- 方式由虚拟机决定,目前主流的两种
- 句柄访问
- 堆中划分内存作为句柄池,reference存放对象的句柄地址
- 句柄包括对象实例数据与类型数据的地址信息
- 优点是稳定,当对象被移动时,只会更改句柄的实例数据地址
- 直接指针访问(Hotspot)
- reference存放的对象地址
- 而相应的堆对象必须存储类型数据的相关信息(Klass指针)
- 优点是高效,节省了一次指针定位的时间
- 句柄访问