对象在内存中的布局
以HotSpot为例,对象在内存中存储可以分为三部分:对象头、实例数据和对齐填充。
对象头
对象头分为两部分:自身的运行时数据和类型指针。
自身的运行时数据
包括哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。在32位和64位的虚拟机中分别表示为32bit和64bit。
类型指针
对象指向它的类元类型的指针,虚拟机通过这个指针来判断这个对象是哪个类的实例。
并不是所有的虚拟机都保留了该部分
实例数据
实例数据就是对象真正存储的有效信息,也就是在代码中定义的各种类型的字段。
这部分的存储顺序跟虚拟机的分配策略参数和字段在代码中的定义顺序有关(例如HotSpot的分配顺序就为long/double,int,short/char,byte/boolean,oops)。
对齐填充
对齐填充并不是必然存在的。它仅仅起着占位的作用。
由于有些虚拟机要求对象的起始地址必须是8字节的整数倍,也就可以理解为所有对象的大小都必须为8字节的整数倍。所以当对象的大小并不是8字节的整数倍时,就需要对其填充来对对象进行填充。
对象的访问定位
java程序通过栈上的引用来访问堆上的对象实例,但java虚拟机规范并没有规定通过何种方式去定位堆上的对象。
目前主流的定位方式有句柄和直接指针两种。
句柄访问
java堆中会划分一段内存来作为句柄池,栈中的引用存放的是句柄的地址,而由句柄再重新定位到对象实例数据和类型数据。
这种方式的好处是栈中存放的引用地址是稳定的,永远指向句柄,伴随着垃圾收集对象的地址可能会被移动,这个时候只需要改变句柄中的地址而不需要修改栈中的引用地址。
直接访问
在这种方式下,栈中的引用存放的就是对象的地址,在这种情况下,就要考虑如何存储类型数据的问题,一种解决方法就是我们上边提到的类型指针。
这种方式的好处是速度快,它节省了一次指针定位的开销,对象的访问在java中是很频繁的,这类开销积少成多后节省的资源也很可观。
对象的创建
类加载
虚拟机遇到一条new指令时,会先去判断这个类是否已经被加载过,如果没有,会先执行加载过程。
分配内存
当内存规整时
在这种情况下,内存的分配是很理想的,即所有已使用的内存都排列在一起,未使用的内存分配在一起,有一个指针作为分界点,这样在为对象分配空间时只需要将指针向未使用的方向移动该对象需要的内存大小即可。这种方式称为“指针碰撞”。
内存不规整时
这种情况下,虚拟机就需要维护一个列表(类似于操作系统中的相关结构),用于记录哪些内存是可用的,在分配时从列表中找一块足够大的空间分配给对象,并且同时更新表中记录信息。这种方式称为“空闲列表”。
java堆是否规整取决于采用的垃圾收集器是否带有压缩整理功能。
初始化
内存分配完成后,虚拟机会将分配到的内存空间都初始化为0(不包括对象头)
进行必要的设置
即根据当前的信息完成对对象头的填写。
完成了以上步骤从虚拟机的角度来看一个对象的创建就完成了。