二、对象(实例)的内存布局
1.对象的创建
- 虚拟机遇到一个new指令
- 检查这个指令的参数是否能在常量池中定位到一个类的符号引用
- 检查这个符号引用代表的类是否已经被加载、解析、初始化过。
- 如果没有,则执行类加载过程。(如果有,直接为新对象分配内存)
- 类加载检查通过后,vm为新生对象分配内存。(对象所需要的内存大小,在类加载完成后就能完全确定)
- 根据不同情况选取,内存分配策略:1)指针碰撞。2)空闲列表。(采取哪种分配方式由内存是否规整决定,内存是否规整由GC策略决定)
- 指针碰撞:堆中的内存是足够规整的,占用中内存和空闲内存以一个指针为分割,各占内存的一边。此时分配内存,只需要移动指针到对象大小的距离。这种分配方式叫做指针碰撞。
- 空闲列表:堆中的内存不规整,无法通过一个指针去标识,vm必须维护一个空闲内存列表,在分配时找出一块足够大的内存去分配,并更新列表记录,这种分配方式叫做空闲列表。
- 在TLAB中分配空间。(TLAB本地线程分配缓存,用于解决并发问题,线程1给A分配内存,刚分配完,指针还没修改或是列表还没更新,线程2又使用了刚刚的指针分配给B了内存,为了保证内存分配的安全,每个线程都预分配了一个独立的内存缓冲区,每个线程分配内存时,都在自己的缓冲区内存里分配,等缓冲区内的内存用完了,再使用同步锁定保证原子性。)
- 内存分配完成后(指针移位,列表同步),已分配的内存空间都初始化为0。如果使用TLAB,初始化可提前到TLAB分配时进行。(这一步骤保证了java实例中的成员变量【基础类型的字段】,可以不赋值就使用,我们可以访问这些数据类型所对应的零值)
- 这之后,虚拟机对对象会进行一些”初始“的设置:这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、GC分代年龄(这些信息存在对象头)。根据vm运行状态,是否启用偏向锁等等。。
- 此时对于vm来说,一个对象已经”创建成功“了。而对于java程序来说,创建才刚开始—方法还没执行,所有的字段属性还是0。接下来根据我们所构造的参数,来对实例进行初始化。这样一个对象那个才真正的创建成功了。
2.对象的内存布局
- 在HotSpot VM中,对象在内存中的存储可以分为三块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。
- 对象头(Header)包括两部分信息:1)对象自身运行时数据(哈希码、GC分代年龄、锁状态标志、线程锁、偏向线程ID、偏向时间戳),官方称这部分数据为Mark Word。2)类型指针(对象指向它的类元数据的指针,虚拟机通过这个指针来确定对象是哪个类的实例) 根据数组的元数据,无法确定数组实例的长度,所以需要在对象投中增加一个数组的长度
- 实例数据(Instance Data)存储的是对象真正存储的有效信息,对象内各个成员字段的值。
- 对齐填充不是必然存在的,仅仅起着占位符的作用。(对象大小必须是8字节的整数倍,对象头部分刚好是8字节的倍数)
3.对象的访问定位
java中,在栈中定义一个reference类型对象,在堆中定义对象实体,通过栈中的reference对象指向堆中实体来控制对象。jvm只规定了指向对象的引用,并没有定义使用何种方式去定位堆中对象的具体位置。
目前主流方式:1)句柄。2)直接指针。
- 句柄:堆中划分一块内存作为句柄池,reference(栈中)中存储对象的句柄地址,句柄中包含对象实例数据(堆中实例池)与类型数据各自的地址信息(方法区)。
- 直接指针:reference(栈中)直接存储,对象实例数据(堆中)的地址,对象实例数据中存放类型数据地址(指向方法区)。
各自优势:
- 句柄:reference中存放的是稳定的句柄地址,对象被移动时,只需要改变句柄中的对象地址(实例对象指针),不需要改变reference。
- 直接指针:速度更快,节省一次指针定位的开销。HotSpot VM使用【直接指针】定位对象。
来源:CSDN
作者:我也想coding啊!
链接:https://blog.csdn.net/weixin_42998510/article/details/104653106