高亮的杂货铺

JVM从0到1-HotSpot虚拟机对象

2020-03-15 · 4 min read
JVM Java

当我们写下一个new关键字时,发生了什么?

虚拟机遇到一个new指令时,先会去检查这个指令的参数是否能够在常量池中定位到一个类的符号引用,然后检查这个类是否被加载、解析和初始化过,如果没有,那么执行该过程。

检查通过后,虚拟机要为新生对象分配内存,内存的大小在类加载完成的时候就已经可以完全确定了,所以内存分配的任务是把等同于对象大小的一块内存从Java堆中划分出来。

此时需要考虑一个问题,由于对象创建是一个非常频繁的行为,即使仅仅是修改一个指针的位置,在并发情况下也并不是线程安全的。对于这种情况,通常由两种可选方案,一种是通过CAS配合重试保证更新操作的原子性,另一种则是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中都预先分配一小块内存,称之为本地线程分配缓冲(ThreadLocal Allocation Buffer, TLAB),哪个线程需要分配内存时,优先在TLAB中分配,只有本地缓冲区用完了,分配新的缓冲区时,才需要同步锁定,可以通过JVM参数来决定是否使用TLAB。

接下来对对象进行必要的设置,例如这个对象是哪个类的实例,如何才能找到类的元数据信息,对象的GC分代年龄等信息,这些信息存放在对象的对象头中。 根据虚拟机当前运行状态的不同,例如是否使用偏向锁等,对象头会由不同的设置方式。

此时,所有的字段都是默认的初始值。new指令之后会接着调用<init>()方法,按照程序员的意愿对对象进行初始化。

对象的内存布局

在HotSpot虚拟机中,对象代内存中的存储布局可以划分为三个部分,对象头(Header),实例数据(Instance Data)和对齐填充(Padding)。

对象头部分包括两类信息,第一类是用于存储对象自身的运行时数据,例如HashCode, GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等,这部数据的长度在32位和64位虚拟机中,分别为32个bit和64个bit,官方称之为“Mark Word”。 对象要存储的运行时数据很多,其实已经超过了32、64位Bitmap结构的空间效率,Mark Word被设计成一个有着动态定义的数据结构,以便在极小的空间内存储尽量多的数据,会根据对象的状态复用自己的存储空间。

对象头的另一个部分是类型指针,即对象指向他的类型元数据的指针,Java虚拟机通过这个指针来确定对象是哪个类的实例。 并不是所有的虚拟机实现都必须保留类型指针,例如通过句柄来实现的,就不需要。 此外,如果对象是一个Java数组,那么在对象头中还必须要有一块记录数组长度的数据。

对象头之后是对象真正存储的有效信息,即程序代码里所定义的各个类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录下来。

第三部分是对齐填充,这并不是必然存在的,也没有特别的含义,仅仅是起着占位符的作用。 HotSpot虚拟机内存管理要求对象的启始地址必须是8的整数倍,所以任何对象的大小都必须是8的整数倍。