《深入理解java虚拟机》笔记(2)HotSpot虚拟机对象探秘


一、对象的创建

1、类加载: 虚拟机在遇到一条new指令时候,检查类是否已被加载、解析、初始化过,如果没有,则执行类加载过程。

 

2、分配内存:类加载完成后,则为新对象从java堆上分配内存,分配内存有两种方式:指针碰撞和空闲列表

  • 指针碰撞

  Java堆中内存是绝对完整的,用过和空闲的内存分别放在一边,中间用一个指针作为分界点的指示器。分配内存时就是将指针向空闲内存区域挪动一段与对象同样大小的距离。

  • 空闲列表

  Java对中内存不是规整的,用过的和空闲的内存相互交错,这种情况,虚拟机就必须维护一个列表,记录哪些内存是可用的。分配时候,从列表中找到一块足够大的内存划分给对象。

  选用哪种分配方式由Java堆是否规整决定,是否规整又由垃圾收集器是否带有压缩整理功能决定。

 

3、分配内存时,在并发情况下也并不是线程安全的,可能出现正在给A分配内存,指针还没来得及修改,对象B又同时使用原来的指针来分配内存情况。

  两种解决方案:

  • 对分配动作做同步处理,采用CAS和失败重试的方式保证更新操作的原子性
  • 把分配动作按照线程划分在不同的空间中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB)。虚拟机是否使用TLAB,可通过-XX:+/-UseTLAB参数设定。

 

4、必要设置:内存分配完成后,会把内存空间都初始化为零值,同时虚拟机会对对象做一些必要的设置

   如:这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希值、对象的GC分代年龄等信息。这些信息存放在对象头(Object Header)中。

 

5、init方法执行:完成以上流程,虚拟机已经完成了一个对象的创建,但从Java程序来看,对象创建才刚开始,所有字段都还为零。

     所以紧接着会执行init方法(由字节码中是否跟随invokespecial指令决定),把对象按照程序员的意愿进行初始化。

 

二、对象的内存布局

在HotSpot虚拟机中,对象在内存中储存的布局可分为3块区域:对象头(Object Header)、实例数据(Instance Data)、对象填充(Object Padding)。

 

1、对象头包括两部分数据

  • 存储对象自身的运行时数据,如下图

      

  • 类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
  • 若对象是一个数组,在头信息还会记录一块数组长度的数据。

 

2、实例数据

  存放对象真正有效信息,程序代码中所定义的各种类型的字段内容。无论是父类继承的还是子类定义的,都会记录起来。

 

3、对齐填充

  不是必然存在的,仅仅起着占位符作用,因为HotSpot要求对象大小必须是8字节的整数倍。因此对象数据没有对齐时,就需要通过对其填充来补全。

 

三、对象的访问定位

       对象访问取决于虚拟机,目前主流两种方式有句柄和直接指针两种。

 

  • 句柄:Java堆会划分一块内存作为句柄池,reference中存储的是对象句柄的地址,句柄包含了对象的实例数据与类型数据的具体地址信息,如下图

       

 

  • 指针:reference中存放的直接就是对象地址,如下图

       

 

  两种方式各有优势

  • 句柄方式就是稳定,在对象移动(垃圾回收时对象移动是很普遍的行为)时候,只会改变句柄中实例数据指针,reference不会修改。
  • 指针就是速度快,节省了一次指针定位的开销,对象访问在Java中非常频繁,积少成多也是一笔非常可观的执行成本。HotSpot就采用的这种方式。