一、创建对象
检查->分配内存->初始化->设置
当虚拟机遇到一条new指令去创建一个对象时,首先检查指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号的引用代表的类是否已被加载、解析、和初始化过。如果没有则先执行相应类的加载过程。
在类加载检查通过后,会为新对象分配内存。对象所需的内存在类加载后便可以完全确定。内存的分配方式有两种,一种称为指针碰撞(Bump the Pointer),一种称为空闲列表(Free List)。指针碰撞是指内存在堆中是绝对规整的,而空闲列表则是已使用的内存和空闲的内存是相互交错的,虚拟机维护了一个列表记录了哪些空闲的内存可用,分配的时候从列表中找寻一块合适大小的空间进行划分给对象实例。虚拟机采用哪种分配方式是由java堆是否规整决定的,而java堆是否规整又是由垃圾收集器是否带有压缩整理功能决定。
采用指针碰撞带Compact功能的收集器如:Serial、ParNew;
采用空闲列表的收集器如:CMS。
在分配内存的时候除了考虑到可用空间划分的问题外,还涉及到一个需要考虑的问题,即对象创建时的并发问题。解决方案包括两种:1.对分配内存空间的操作进行同步处理;2.把内存分配的动作按照线程划分到不同的空间进行,即每个线程在java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB)。
内存分配完成后,需要将分配到的内存空间都初始化为零值,该操作保证了对象的实例字段在Java代码中可以不赋值就直接使用。
再之后,虚拟机会对对象进行一些必要的设置(包括对象是哪个类的实例、如何找到类的元数据信息、对象的哈希码等)。
至此,虚拟机认为对象已经产生了。但Java程序后续还会对对象进行init方法。
二、对象的内存布局
对象在内存中的布局可以分为3个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
对象头包括两部分信息。一部分用于存储对象自身的运行时数据(如哈希码、锁状态标志、线程持有的锁等等);另一部分是类型指针(即对象指向他的类元数据的指针,虚拟机通过这个指针确定这个对象是哪个类的实例)。
实例数据存放的是对象真正存储的有效信息(包括父类继承的和子类定义的)。各字段的存储顺序会受到虚拟机分配策略参数和字段在java源码中定义顺序的影响。
对齐填充并非是必然存在的。其意义在于因为HotSpot虚拟机的自动填充要求对象的起始地址必须是8字节的整数倍(即对象的大小必须是8字节的整数倍),故需要通过对齐填充来补充。
三、对象的访问定位
Java程序通过栈上的reference数据来操作堆上的对象实例。而reference类型只规定一个指向对象的引用,并没定义该引用以何种方式定位访问对象的具体位置,所以目前主流的对象访问方式包括使用句柄访问和直接指针两种。
1.使用句柄访问:Java堆中会划分一块内存作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象实例数据和类型数据各自的具体地址信息,如下:
2.使用指针访问:reference中存储的直接就是对象地址,而在堆对象中就要考虑如何放置访问类型数据的信息,如下:
两种方式的比较各有优势。句柄的好处在于reference中存储的是稳定的句柄地址信息,在对象被移动时(当垃圾收集时,对象移动是非常普遍的行为)只会改变句柄中的实例数据指针,而reference本身无需修改;而直接指针的方式的好处也可以从图中看出,相较于句柄而言,他节省了一次指针定位的时间开销,速度更快。