对象的内存布局
对象的创建
- 加载 -> 验证
当Java虚拟机遇到字节码new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过;如果没有,那么必须先执行相应的类加载过程
- 准备 -> 解析
在类加载检查通过之后,Java虚拟机将为新生对象分配内存,对象所需的内存大小在类加载完成后便可完全确定,为对象分配空间的任务实际上便等同于把一块确定大小的内存块从Java堆中划分出来
内存分配完成之后,虚拟机必须将分配到的内存空间(但不包括对象头)都初始化为零值,如果使用TLAB的话,这一项工作也可以提前至TLAB分配时顺便进行
接着,Java虚拟机还要对对象进行必要的设置(对象头的初始化):对象的实例、元数据信息、GC分代年龄、锁信息、哈希码(实际对象的哈希码会延后到真正调用Object::hashCode()方法是才计算)等
- 初始化
最后,执行Class文件中的<init>()方法(即构造函数),按照程序员的意愿对对象进行初始化
分配内存的方式
Java堆依据内存是否规整有两种内存分配方式:指针碰撞和空闲列表;而Java堆内存的规整又取决于垃圾收集器是否带有空间压缩整理的能力;
当使用Serial、ParNew等带压缩整理过程的收集器时,系统采用的分配算法是指针碰撞,而当选择CMS这种基于清除算法的收集器时,理论上就只能使用空闲列表来分配内存(CMS的实现里,为了能在多数情况下分配的更快,设计了Linear Allocation Buffer的分配缓冲区,通过空闲列表拿到缓冲区之后,再在缓冲区中通过指针碰撞的方式进行分配)
- 指针碰撞
适用于堆内存是绝对规整的,通过分界点指示器指针划分为已用区域和空闲区域;为新生对象分配内存,就是将指针向空闲区偏移新生对象所需大小的距离
- 空闲列表
适用于不规整内存,已使用区域和空闲区域交织在一起;这种情况下,Java虚拟机就有必要维护一份记录了哪些位置内存可用的空闲列表,为新生对象分配内存后,需要更新空闲列表
分配内存时的并发处理
- CAS 失败重试
使用CAS 失败重试对分配内存空间的动作做同步处理
- 本地线程分配缓冲(TLAB)
每个线程在Java堆中预先分配一小块内存,称为TLAB,哪个线程需要分配内存,就在各自的分配缓冲区中分配;虚拟机是否使用TLAB,通过-XX: /-UseTLAB参数来设定
对象的内存布局
在HotSpot虚拟机里,对象在堆内存中的存储布局被划分为三个部分:对象头、实例数据、对齐填充
- 对象头
HotSpot虚拟机对象的对象头部分包括两类信息:Mark Word、类型指针
Mark Word,对象自身的运行时数据,一个有着动态定义的数据结构,以便在极小的空间存储尽量多的数据;比如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,
类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例;如果对象是一个Java数组,对象头中还必须有一块用于记录数组长度的数据,以便通过Java对象的元数据信息和长度推断出数组的大小
- 实例数据
对象真正存储的有效信息,也即是程序员意愿的数据;无论是从父类继承下来的,还是子类中定义的字段都必须记录起来;HotSpot虚拟机默认的分配顺序为longs/doubles、ints、short/chars、bytes/booleans、oops
- 对齐填充
并不是必然存在的,仅仅起着占位符的作用;HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,任何对象的大小都必须是8字节的整数倍,因此某些情况下就需要对齐填充来补全
对象的访问定位
主流的访问方式主要有:句柄访问、直接指针访问;HotSpot虚拟机采用的是第二种方式进行的对象访问
- 句柄访问
使用句柄访问,Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息
使用句柄访问的最大好处就是:reference中存储的是稳定句柄地址,在对象被移动(GC)时只会改变句柄中的实例数据指针,而reference本身不需要改动
- 直接指针访问
使用直接指针访问,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销
使用直接指针访问的最大好处是:速度更快,节省了一次指针定位的时间开销