本篇介绍HotSpot虚拟机在Java堆中对象分配、布局和访问的全过程。
一、对象的创建
1.1 为新生对象分配内存
为新生对象分配内存有两种方式:指针碰撞(Bump The Pointer)和空闲列表(Free List)。
1.1.1 指针碰撞
垃圾收集器带空间压缩整理能力时,Java堆内存是规整的,所有被使用过的内存都被放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离。
1.1.2空闲列表
已使用的内存和空闲内存相互交错在一起,那就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
1.2 并发下分配内存
并发下分配内存不是线程安全的,解决方案:
1.2.1 聚合同步
对分配内存空间的动作进行同步处理–实际上虚拟机是采用CAS配上失败重试的方式保证操作的原子性。CAS(Compare and Swap)即比较再交换,CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
1.2.2 本地缓冲
另外一种是每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),只有本地缓冲区用完了,分配新的缓冲区时才需要同步锁定。是否使用TLAB,可以通过-XX: +/-UseTLAB参数来设定。
1.3 分配对象过程
分配对象内存 --> 内存空间初始化为零值 --> 设置对象信息到对象头 --> init方法执行
二、对象的内存布局
对象在堆内存的布局:对象头(Object Header)、实例数据(Instance Data)和对齐填充(Padding)。
2.1 对象头:
-
第一类是对象自身运行时的数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,官方称为“Mark Word”。这部分数据与对象自身定义无关,考虑空间效率,这部分空间是复用的,不同状态下存储的数据如下表。
存储内容 标志位 状态 对象哈希码、对象分代年龄 01 未锁定 指向锁记录的指针 00 轻量级锁定 指向重量级锁的指针 10 膨胀(重量级锁定) 空,不需要记录信息 11 GC 标记 偏向线程ID、偏向时间戳、对象分代年龄 01 可偏向 -
另一类是类型指针,即对象指向它的类型元数据的指针,用来确定对象是哪个类的实例。如果是数组,还需另外存数组的长度。
2.2 实例数据
程序中定义的各种类型的字段内容,包括从父类继承下来的。
2.3 对齐填充
占位符,保持对象起始地址是8字节的整数倍。
三、对象的访问定位
通过栈上的reference数据来操作堆上的具体对象。 访问方式有:
3.1 使用句柄
Java堆中可能会划出一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例与类型数据各自具体的地址信息。
使用句柄来访问的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(GC时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。
3.2 直接指针
reference中存储的直接就是对象地址。
使用指针访问最大好处就是速度更快,节省了一次指针定位的时间开销。由于对象访问非常频繁,所以比使用句柄节省了很可观的执行成本。HotSpot主要是这一种方式。