HotSpot虚拟机对象机制分析
以HotSpot JVM和常用的java heap为例,讨论对象分配、布局、访问的全过程.
对象的创建
- 虚拟机遇到new时,检这个指令的参数能否在常量池中定位到一个类的符号引用,并且检查这个类是否被加载、解析、初始化过,若没有,则加载这个类。
- 类加载检查通过后,确定所需内存大小,将这块内存从JAVA堆中划分出来。这里有两种划分方法:指针碰撞(Bump the Pointer) 、空闲列表(Free List)。选择哪种分配方式是根据java堆是否规整来确定的。而是否规整又决定了所采用的GC是否带有压缩整理功能。(这部分的内容就像是操作系统的内存管理,固定大小或动态分配,动态分配的话就设计到内存碎片的压缩。)
- 指针碰撞 若java堆中的内存是绝对规整的(内存块大小都相同),那么把指针移动与对象大小相同的距离即可实现内存分配。
- 空闲列表 java堆中的内存不是规整的,虚拟机要自己维护一个列表来记录哪块内存是可用的,分配时需要找到可用大小的内存块并更新列表。
- 对象创建的频繁性也是需要考虑的内容,即便是修改指针指向位置,在并行情况下也不是线程安全的,可能出现正在个对象A分配内存,指针还没来得及修改,对象B又使用了原来的指针来分配内存的情况。解决方案有两种:
- CAS配上失败重试的方法保证更新的原子性
- 把内存分配的动作按照线程划分在不同的空间中进行。即每个线程预先分配一小块内存,称为TLAB,哪个线程分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并重新分配新的TLAB时,才需要同步锁定。
- 分配完成后需要初始化内存空间为零值。
- 再接下来,JVM对对象进行必要设置,例如对象是哪个类的实例、如何找到元数据信息、对象哈希码、GC分代年龄等信息,这些信息存在对象头(Object header)中。
- 以上步骤后,虚拟机层面对象已经创建,但是JAVA层面方法还没执行,所有字段还都是0。接下来执行方法
对象的内存布局
在HotSpotJVM中,存储对象的布局可以分为三块区域:对象头(Header),对象示例数据(Instance Data)和对象填充(Padding)。
- 对象头
对象头分两部分:- 一部分存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态、线程持有的锁、偏向线程ID、偏向时间戳。这部分只占32bit或者64bit。
- 另一部分是类型指针,即对象指向它类元数据的指针。虚拟机通过这个指针确定这个类是哪个类的实例。(但并不是所有的虚拟机实现都要在对象数据上保存类型指针,查找对象元数据信息不一定经过对象本身),如果对象是数组,那么对象还需要记录数组长度。
- 实例数据
实例数据就是各种字段内容。 - 对齐填充(无特殊作用,相当于占位符)
对象的访问定位
JAVA程序通过栈啥上的reference数据来操作堆上的具体对象。主要有两种访问方式:
-
使用句柄(handle)访问
(PS:句柄是什么:https://blog.csdn.net/zwz2011
303359/article/details/69943503)
JAVA堆中划分出一块内存作为句柄池。 -
使用直接指针访问
两种方式的对比
- 句柄访问的优势是reference中存储的是稳定的句柄地址,对象被移动时,只改变句柄中的实例地址,reference不需要修改。
- 直接访问速度快,节省了一次指针定位的开销,由于对象访问非常频繁,直接访问能明显降低执行成本。就Sun公司的JVM来说,它是以第二种方式来访问的,但就业界而言,使用句柄访问的情况也很常见。