14、JVM 实战 - JVM之运行时数据区 - 对象分配过程

一、对象的访问定位

堆中存对象,栈上存引用,JVM中有两种对象访问定位方式:

1、通过直接引用访问

 如图,直接引用方式是局部变量存储是对象实例的地址,同时,对象所属的类也可以通过对象实例获取。
 

2、通过句柄池访问

如图,句柄访问方式是在堆中开辟了一块句柄池,句柄池存储了对象的引用和所属类的引用,而局部变量表存储了句柄池的地址。
 

  • 对比:

句柄方式,当对象位置发改变时(如GC时,对象复制),不需要改变局部变量的引用。访问对象的类型数据时,可以直接获取,而不用获取对象实例之后再访问。
直接引用,可以直接通过引用获取到对象实例,速度更快,HotSpot虚拟机采用该方式。

二、对象的内存布局

 

对象在堆中的内存布局主要分为三个部分:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。

1、对象头

  • Mark Word: 存储对象自身运行时的数据,主要内容如下表
  • 类型指针:指向对象所属类的类型数据。
  • 数组长度:档案实例是一个Java数组时,对象头中用于记录数组长度的区域。

 

2、实例数据

对象存储的真正有效信息,各种字段内容,包括从父类继承的。

3、对象填充

没有实际意义,相当于占位符,如果对象实例所占空间不是8字节的整数倍,就通过对齐填充补充到8的整数倍(HotSpot虚拟机的自动内存管理系统要求对象的起始地址必须是8的整数倍)

三、堆中的线程私有区域

 

堆中的区域并不是都是线程共享,为提高对象分配的效率,线程在堆中划分出了线程私有的分配缓冲区(Thread Local Allocation Buffer,简称TLAB)。默认情况下,TLAB仅占Eden区的1%,可以通过-XX:+UseTLAB启用TLAB,也可以通过-XX:TLABSize调节TLAB的大小。

  • TLAB的优势:
  • 提高分配效率,堆区作为共享区域,Eden区对象创建频繁,给对象分配内存时,为保证线程安全,避免多线程操作同一地址,必然需要一系统加锁机制,进而影响分配速度。TLAB是对象分配的首选区域,在TLAB中给对象分配内存失败时,才会在Eden区分配。

四、对象创建过程

 

对象创建过程:

1、当JVM执行到一条new指令时(如new #2 <heap/HeapPartDemo>),就会去根据符号引用(#2)检测运行时常量池中这个符号引用指向的类是否已经完成加载、解析和初始化。如果没有就会进行该类的类加载过程。

2、 当检测到类已加载,就会开始分配内存,对象的大小在类加载阶段就已确定,在Java堆中为对象分配内存有指针碰撞和空闲列表两种方式,对象根据条件不一定分配在Eden区,这个栈上分配时再写;

  • 指针碰撞(Bump The Pointer):如果Java堆中内存是规整的,已用内存区域和空闲内存区域用一个指针作为分界线,在分配内存给对象时,只要依据对象所需内存(对象大小在类加载时就会确定),将指针向空闲内存区域移动一段与对象大小相等距离即可,如Eden区内存区域是[0x00000000fd580000,0x00000000ff600000],,0x00000000fd969620是指针位置,分配时在,0x00000000fd969620基础上加上对象大小即可,0x00000000fd969620就是对象的起始地址,依据对象填充,这个地址换算过来应该是8的整数倍。
  • 空间列表(Free List):如果Java堆中内存不规整,已用内存和空间内存交织在一起,就需要有一个表记录那些内存可用,这个表就被称为空闲列表,给对象分配内存就需要从空闲列表中找到一块足够的区域分配内存,同时更新表信息。

同样,对象分配内存也存在并发问题,对象分配内存线程问题的解决方式:

  • 采用CAS配上失败重试的方式使得线程同步。
  • 线程私有的分配缓冲区TLAB。

3、 完成内存分配之后,就会进行对象实例数据属性初始化,这一步保证了即使对象字段属性即使没有赋值,也能被使用,访问到对应的零值;

4、 实例数据初始化完成后,需要设置对象头,包括MarkWord、类型指针和数组长度;

5、 执行Class文件里()方法,完成Java程序意义上的对象构造函数;