HotSpot虚拟机对象的创建

107 阅读5分钟

1.对象的创建

Java中每时每刻都有对象被创建出来。语言层面上通常仅仅是new关键字(例外:复制,反序列化)。下面介绍普通对象(除数组,Class对象等)的创建。

Jvm在执行到字节码new指令时。首先检查该指令的参数能否在常量池定位到一个类的符号引用,并检查这个类时候被加载,解析和初始化过。若没有,先执行相应的类加载

内存分配

对象所需要的内存大小在类加载完后是可以确定的。所谓的为对象分配空间实际就是在堆中把这一确定大小的内存块划分出来。划分方法可以分为:

指针碰撞(Bump The Pointer) :这里要求堆中的内存是规整的,即用过的在一边,没用过的在另一边,中间放着一个指针作为分界点的指示器,分配内存后仅把指针向空闲区挪动相应大小即可。其需要采用的垃圾收集器带有空间压缩整理(Compact)的能力。

空闲列表(Free List): 用于被使用和未被使用的内存交错在一起的情况,Jvm通过维护一张记录哪些内存可用的列表,分配时从表上查找划分即可。

保证分配内存时线程安全的方法:

  1. 对分配内存空间的动作进行同步处理(Jvm采用CAS加上失败重试保证更新操作的原子性)。
  2. 每个线程预先在堆(伊甸园区)中分配一小块内存,称为本地线程分配缓冲(TLAB)。线程要分配内存时在本地缓冲区中分配,只有在本地缓冲区用完分配新的缓存区时才需要同步。

分配完内存后,将分配到的内存中除了对象头的都初始化为零值。采用TLAB的话也可以提前至TLAB分配时顺便进行。这样Java代码中实例字段不赋初始值就可以使用。

接下来会在对象头(Object Header)中设置信息。如对象是哪个类的实例,如何找到类的元数据信息,对象的GC分代年龄等信息。

此时在JVM的视角对象已经产生,但从程序来看,对象的创建才刚刚开始。构造函数,即class文件中 () 方法还没执行。字段都是零值,一般来说new指令后就会接着执行()方法,按照程序员的意愿将对象初始化。(对于new关键字编译器会在其指令后生成invokespecial指令,让其接着执行(),但直接通过其他方式产生则不一定这样)

init方法会按照代码顺序,进行显示赋值,代码块。和构造器赋值(最后)

2.对象的内存布局

对象在堆中的存储分布可以分为:对象头(Header),实例数据(Instance Data)和对齐填充(Padding)。

对象头(32 or 64比特)包含两类信息:

  1. 存储自身的运行时数据(Mark Word):哈希码,GC分代年龄,锁状态标志,线程持有的锁等。虽然对象要存储的运行时数据很多已经超过了Bitmap结构所能记录的最大限度。但这些信息是与对象自身定义的数据无关的额外成本。因此Mark Word设计为有着动态定义的数据结构,即可根据对象的状态复用储存空间。
  2. 类型指针:对象指向它的类型元数据(方法区)的指针,JVM通过这个指针确定该对象是哪个类的实例。(并不是所有jvm都保留类型指针)

如果对象是个数组,对象头里会有记录数组长度的数据。(普通对象可根据元数据信息确定大小)

实例数据:

存储代码里定义的各种类型的字段内容,包括父类继承的和子类定义的。其存储顺序受到JVM分配策略参数和字段在源码定义顺序的影响。

HotSpot默认顺序:longs/doubles,ints,shorts/chars,bytes/booleans,oops。

可以发现相同宽度的字段放到一起,在该前提下,父类定义的变量会在子类前。在HotSpot的+XX:CompactFields参数设置为true(默认为true),子类较窄的变量可以插入父类变量的空隙之中,以节约一点空间。

对齐填充:对象的的大小被要求为8字节的整数倍,因此不是时需要填充。

3.对象的定位访问

Java程序通过栈上的reference数据操作堆上的对象,目前主流访问方式有句柄和直接指针

句柄:

在堆中划分内存作为句柄池, reference中存储对象的句柄地址,句柄中包括对象的实例数据和类型数据的地址信息。

直接指针:

reference中存储对象地址,在堆中的对象的内存布局中存放如何访问类型数据的相关信息。

两种方法,句柄可以在对象移动时仅改变句柄中的实例数据指针。直接指针访问速度快,少了一次指针定位的开销。