在介绍完Java虚拟机运行时数据区域后,大致知道了虚拟机各部分存储什么内容,本部分内容将介绍虚拟机中对象的创建过程、对象内存布局以及如何访问。本部分内容讨论的都以HotSpot虚拟机中的Java堆为例,对象仅限于Java普通对象,不包括数组、Class对象等。
对象的创建
在编写Java程序时,我们创建对象是通常用new关键字,而在虚拟机中的创建过程大致分为四个步骤;
类加载检查
虚拟机遇到一条new指令是,会先去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否被加载、解析和初始化过。如果没有则先执行类加载过程;
为新生对象分配内存
在类加载过程通过后,虚拟机会为新生对象分配内存,对象所需内存大小可在类加载完成后确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来;
两种分配方式:使用哪种方式是由Java对是否规整决定,而Java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定。
指针碰撞
假设Java堆中内存是绝对规整的,所有用过的内存在一边,空闲的内存在另一边,中间放着一个指针作为分界点的指示器,那所分配的内存就仅仅是把该指针向空闲空间那边挪动一段与对象大小相等的距离,这种方式成为指针碰撞。空闲列表
如如果Java堆中内存是不规整的,已使用的内存和空闲内存相互交错,则虚拟机就必须维护一个列表,记录可使用的内存,在分配的时候在列表中找到足够大的内存空间分配给对象,并更新列表上的记录,这种分配方式称为空闲列表。
内存分配的线程安全问题:
对象创建非常频繁,即使仅仅修改指针指向的位置,也可能是非线程安全的,可能在给对象A分配内存时,指针还没来得及修改,对象B又使用了原来的指针来分配内存。
解决档案:
- 对分配内存空间动作进行同步处理-实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;
- 本地线程分配缓冲(TLAB)-把内存分配的动作按照线程换分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存。当那个线程需要分配时,就在哪个线程的TLAB上分配,当TLAB用完分配新的TLAB时会使用同步锁定。虚拟机是否使用TLAB通过-XX:+/-UseTLAB参数来设定。
将分配到的内存空间都初始化为零值
保证对象实例字段在Java代码中可以不赋初始值就可以使用,程序能直接访问这些字段的数据类型所对应的零值。
若使用TLAB,本过程也可以提前至TLAB分配时进行。
对象设置
在对象的对象头中保存对象是哪个类的实例、如何能找到类的元数据信息、对象的哈希吗、对象的GC分代年龄等信息。
注意:在上面四个步骤完成后,再虚拟机看来,对象已经创建完成,但从Java程序来看,对象创建才刚开始-
方法还没有执行,所有字段都还为零。所以一般来说,执行new指令后会执行 方法。
对象内存布局
HotSpot虚拟机中,对象在内存中存储的布局可以分为3快区域:对象头、实例数据和对齐填充。
对象头
对象头信息主要包括两部分:
- 用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标识、线程持有的锁、偏向线程ID、偏向时间戳,数据的长度根据32位还是64位的虚拟机决定,分别为32bit和64bit,官方成为Mark Word,Mark Word被设计成一个非固定大小的数据结构以便在极小的空间存储尽量多的信息,它会根据对象的状态复用自己的存储空间。
- 另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针确定他是哪个类的实例,但并不是所有虚拟机实现都必须在对象数据上保留类型指针,也就是说,查找对象的元数据信息并不一定要通过对象本身。
如果对象是一个数组,在对象头中还必须有一块来记录数组的长度,因为虚拟机无法从数组的元数据中确定数组的大小。
实例数据
实例数据部分存储程序代码中所定义的各种类型的字段内容。无论是继承父类的还是子类中定义的,都要记录。这部分存储顺序受到虚拟机分配策略参数和字段在Java源码中定义顺序的影响。HotSpot虚拟机默认分配顺序为longs/doubles、ints、shorts/chars、bytes/booleans、oops,可以看出相同宽度的字段会被分配到一起。此前提下,父类中定义的变量会出现在子类之前。如果CompactFields参数值为true(默认true),子类中较窄的变量可能会插入到父类变量的空隙中。
对齐填充
对其填充并不是必然存在的,具有占位符的作用。因为HotSpot VM自动内存管理系统要求对象起始地址必须是8字节的整数倍,也就是说,对象的大小必须是8字节的整数倍。而对象头部分刚好是8字节的倍数(1倍或2倍),因此,当实例数据部分没有对齐时,就需要通过对其填充来补全。
对象的访问定位
对象的访问是根据虚拟机的实现而定的,主流的访问方式主要有句柄访问和直接指针两种方式。
句柄访问
使用句柄访问的方式,Java堆中会划分出一块内存作为句柄池,reference中存储的对象就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体信息,如图所示:
直接指针
使用直接指针访问,Java堆对象的布局当中就必须考虑如何放置访问类型数据信息,而reference中存储的直接就是对象地址,如图所示:
比较:
使用句柄访问的优势是reference中存储的是稳定的句柄地址,在对象被移动(如垃圾收集时)时只会改变句柄中实例数据指针,而reference本身不需要修改;
使用直接指针的优势在于速度更快,节省一次指针的定位开销。HotSpot VM则使用这种方式进行对象访问。