HotSpot虚拟机对象探秘
一、对象的创建过程
1)当虚拟机遇到new指令时,会首先检查这个指令的参数是否在常量池中存在,并检查这个指令代表的类是否被加载、解析和初始化过。若没有,就必须执行相应的类加载过程。
2)当类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可以完全确定。为对象分配空间的方式有两种:
- 指针碰撞(Bump the Pointer): 当内存是绝对规整的,所用过的内存都放一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配的内存仅仅是将那个指针向空间的那边挪动一段与对象大小相等的距离。
- 空闲列表(Free List): 当内存不是规整的时,虚拟机维护了一个列表,记录了哪些内存是可用的,在分配内存时从列表中选取一块足够大的空间划分给对象实例,并更新列表上的记录。
在使用Serial、ParNew等带Compact过程的收集器时,会采用指针碰撞;在使用CMS这种基于Mark-Sweep算法收集器时,采用的是空闲列表。
为考虑并发情况下内存分配的线程安全问题,可以采取两种方案:
- 采用CAS配上失败重试的方法来保证更新操作的原子性。
- 把内存分配的动作划分在不同的空间中进行,即每个线程在Java堆中预先分配一小块内存。称为本地分配缓冲。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAb时,才需要同步锁定。
3)内存分配完成后,虚拟机需要将分配的内存空间初始化为零值(不包括对象头)。接下来,要对对象进行必要的设置.例如,这个对象是哪个类的实例,如何才能找到类的元数据信息。对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头中。
4)以上工作完成后,从虚拟机角度来说,一个新的对象已经产生了,但在Java程序的角度来看,对象创建才刚刚开始。执行完new指令后完new指令后会接着执行<init>方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出。
二、对象的内存布局
在HotSpot中,对象可以划分为3块区域:对象头、实例数据、对齐填充。
1)对象头包含两个部分:
- 第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志等。这部分数据的长度在32为和64位的虚拟机中分别位32bit和64bit,官方称他为“Mark Word”。它被设置位一个非固定的数据结构以便在极小的空间存储尽量多的信息,它会根据对象的状态复用自己的存储空间。
- 另一部分位类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
2)实例数据是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录下来。
3)对齐填充并不是必然存在的,没有什么特别的含义,仅仅是起着占位符的作用。HotSpot的自动配置管理系统要求对象的大小必须是8字节的整数倍。当不满足时,就需要通过对齐填充来补齐。
三、对象的访问定位
Java程序需要通过栈上的reference数据来操作堆上的具体对象。目前主流的访问方式有两种:句柄访问和直接指针。
1. 句柄访问
使用句柄访问时,在Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自对的具体地址信息。它的优点是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。
2. 直接访问
使用直接指针访问,Java堆对象的布局中就必须放置访问类型数据的相关信息,而reference中存储的直接就是对象地址。它的优势是速度更快,节省了一次指针定位的时间开销。HotSpot就使用了这种方式。