HotSpot虚拟机中的对象
本文所涉及的内容都是基于HostSpot虚拟机而言的。
对象的创建
一个对象的创建(这里不包括数组和Class对象),在Java中仅仅是一个new关键字,当虚拟机遇到字节码new指令时,它会先检查指令中的参数能否在常量池(①)中定位到一个符号引用(②),并且检查这个符号引用代表的类是否已经被加载、解析和初始化过,如果没有,那必须先执行类加载。
类加载检验通过后,虚拟机会在堆中为它划分一块区域,区域的大小在类加载完成后可以确定。这里划分区域根据不同虚拟机的设计,会有不同的方案。
如果Java堆内存是绝对规整的,一半放用过的,一半放空闲的,中间有一个指针用作分界。那么内存分配只需要将那个指针向空闲的方向移动与对象大小的位置即可,这种分配方式称为指针碰撞。
如果内存是不规整的,即使用的和未使用的内存交错在一起,虚拟机就需要维护一个列表,记录哪些内存是可以使用的,在分配时就需要从虚拟机中找到足够大小的空间划分给对象,并更新列表记录,这种称为空闲列表。
采用那种分配方案取决于堆是否规整,而是否规整又取决于垃圾收集器是否带有空间压缩整理的能力。
由于内存分配是特别频繁的一件事,指针移动这一操作并不是线程安全的,虚拟机采用的解决方案是CAS(③)配上失败重试的方式保证更新操作的原子性。
内存分配完成后,虚拟机会把分配到的内存空间都初始化为零值,这一步保证了对象实例字段在Java代码中可以不赋初始值就可以使用。
接下来,虚拟机就要设置对象的基本属性,比如这个对象是哪个类的实例,如何找到类的元数据,对象的哈希码,对象的GC分代年龄等信息。这些信息被放在对象头中。
对象的内存布局
对象在堆中的存储布局可以划分为三个部分,对象头,实例数据,对齐填充。
对象头包括两部分信息,第一部分是存储自身运行时数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID等,这部分被称为Mark Word。这里面的有部分信息会在偏向锁,轻量级锁的实现中用到。
第二部分是类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过该指针确定对象是哪个类的实例。这个指针指向每个对象生成的Class对象,位于方法区中。
数据实例部分是对象真正存储的有效信息,即我们在代码中定义的各种字段。
对齐填充并不是必定存在的,也没有特别含义。
对象的访问定位
对象创建完后,会通过栈上的reference数据来操作堆上的对象,主流访问方式有以下两种:
使用句柄
如果使用句柄的话,堆中会划分出一块内存来作为句柄池,reference存储的就是对象的句柄地址,句柄中包含了对象实例数据与类型数据各自具体的信息。具体结构如下:
直接指针
如果使用直接指针访问的话,Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销。直接指针的最大好处是省略了一次指针定位的开销。
具体结构如下:
名词解释
①常量池:常量池位于方法区中,用于存放编译器生成的各种字面量与符号引用
②符号引用:在Java虚拟机中,当一个类被加载时,它的类信息会被存储在运行时常量池中,包括类的名称、方法的名称和参数类型等信息,这些信息构成了符号引用。
③CAS操作:类似于版本控制,不过是在字节码层面的。它涉及到3个值,原值A,以及A的副本,要修改的值C。在修改时,它会验证A的值是否等于副本A,如果等,再将C的值写入。
参考
《深入理解Java虚拟机》