对象的内存布局
对象在内存中存储的布局可以分为3块区域: 对象头(Header)、实例数据(Instance Data) 和对齐填充(Padding)
HotSpot 虚拟机的对象头包括两部分信息
用于存储对象自身的运行时数据
如 哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
这部分数据的长度在 32位和64位虚拟机(未开启压缩指针) 中分别为 32bit 和 64bit,官方称之为 “Mark Word“.
对象需要存储的运行时数据很多,其实已经超出了 32位、64位 Bitmap 结构所能记录的限度,但是对象头信息是与对象自身定义的 数据无关的额外存储成本, 考虑到虚拟机空间效率, Mark Word 被设计成一个非固定的数据结构以便在极小的空间存储尽量多的信息,它会根据对象的状态复用自己的存储空间。
例如: 在 32 位的 HotSpot 虚拟机中,如果对象处于未被锁定的状态下,那么 Mark Word 的32bit 空间中的 25bit 用于存储对象哈希码,4 bit 用于 存储对象分代年龄,2bit 用于存储锁标志位,1bit固定为0,而在其他状态(轻量级锁,重量锁,GC标记,偏向锁)下的存储内容:
对象头的另一部分是类型指针。 即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有虚拟机实现都必须在对象数据上保留类型指针。
换句话说,查找对象的元数据信息并不一定要经过对象本身。
另外,如果对象是一个 Java 数组,在对象投中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通 Java 对象的元数据信息确定 Java 对象的大小,但是从数组的元数据中却无法确定数组的大小。
对齐填充。并不是必然存在的,也没有特别的含义,仅仅是占位符的作用。
由于 HotSpot VM 的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,即 对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 or 2 倍)。因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
对象的访问定位
建立对象是为了使用对象,Java 程序需要通过栈上的 reference 数据来操作堆上的具体对象。
由于 reference 类型在 Java 虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过河中方式去定位、访问堆中对象的具体位置,所以对象的访问方式也是取决于虚拟机的实现。
目前主流的访问方式使用 句柄 和 直接指针 两种。
使用句柄访问:
Java 堆中会划分出一块内存来作为句柄池
reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据 与 类型数据各自的具体地址信息。
优势: 使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时,只会改变句柄中的实例数据指针,而 reference 本身不需要修改。
直接指针访问:
Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象地址。
优势: 最大的好处是速度更快,节省了一次指针定位的时间开销,由于对象的访问在 Java 中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。HotSpot 是使用直接指针访问的形式。