HotSpot虚拟机在堆中对对象分配、布局和访问的全过程

          虚拟机遇到一条new指令时,首先检查这个指令能否在常量池中定位到一个类 的符号引用,并检查这个符号引用代表类是否已被加载解析和初始化过。如果没有则必须先执行相应的类加载过程。类加载通过后虚拟机将为这新对象分配内存。对象所需内存大小在类加载完成后已完全确定。内存分配有“指针碰撞”和“空间列表”两种方法。若Java堆中内存是绝对规整的(用过的内存在一边,没用过的内存在另一边,中间用指针作为分界点的指示器),内存分配时只需将指针向未使用内存区域移动与对象大小相等的距离即可;若Java堆中内存不是绝对规整的,使用过的和空闲的内存交错在一起时,虚拟机通过维护一个记录了可用内存快的列表来分配内存。在分配时在表中找出足够大的空间分配给对象实例,同时更新列表上的记录。对于Java堆是否规整,这由所采用的垃圾收集器是否带有压缩整理功能决定的。Serial采用指针碰撞,CMS采用空闲列表。要知道对象在内存中创建是非常频繁的,所以在知道了内存分配方法后我们还需要考虑并发情况下的线程安全问题。如果正在对对象A分配内存,指针还没来得及修改,对象B又使用原来的指针分配内存,这并非我们想看到的。要解决这个问题也有两种方法。①对分配内存空间的动作进行同步处理,采用CAS配上失败重试的方式保证更新操作的原子性②把内存分配的动作按照线程划分在不同的空间之中进行即在堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB)哪个线程需要分配内存就在哪个线程的TLAB上分配。只有TLAB用完并分配新的TLAB时才用同步锁锁定。内存分配完后,虚拟机将分配到的内存空间都初始化为零值(不包括对象头)。接下来虚拟机对对象做必要的设置,如如何才能找到对象的元数据信息,字节的哈希码等。此时在虚拟机看来对象已经产生了,但在Java程序的角度来看<init>方法还没开始,所有字段均为0,所以执行new指令后会接着执行<init>方法按意愿进行初始化,这样一个对象才算完全产生出来。

         对象在内存中存储的布局分为3个区域:对象头,实例数据和对齐填充。对象头分为两部分:第一部分用于存储对象自身的运行时数据,如哈希码,GC分代年龄等。对象头的另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。如果对象是个数组,对象头中还必须包括一块记录数组长度的数据。因为虚拟机可以通过普通Java对象的元数据知道Java对象的大小,但是从数组的元数据却无法确定数组的大小。实例数据部分是对象真正存储的有效信息,即程序代码中所定义的各种类型的字段内容。对于对齐填充,这并不时必须的,也没有特别含义,仅起着占位符的作用。

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

             句柄访问:Java堆中划分出一小块内存作为句柄池,reference中存储是是对象的句柄地址。句柄中包含了对象实例数据与类型数据各自的具体地址信息。好处是reference存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针HotSpot虚拟机在堆中对对象分配、布局和访问的全过程


             直接指针:reference直接存储对象地址。好处是速度更快,节省了一次指针定位的时间开销

HotSpot虚拟机在堆中对对象分配、布局和访问的全过程