HotSpot 对象

普通对象的创建(不包括数组和class对象)

  当虚拟机遇到new指令时,会在常量池中检查是否包含这个类的符号引用(全限定名),通过这个确定是否经过类加载的过程,如果true,为该对象分配内存,对象大小在类加载过程就已经确定。如果false,需要进行类加载。

1、分配内存

分配内存的方式

  指针碰撞:如果内存是绝对规整的,使用过的在一边,未使用过的在另一边,中间有个指针作为分界点的指示器,为对象分配内存的时候,只需要向未使用过的一边移动与对象大小相同的距离完成内存分配

  空闲列表:内存非规整的情况下,jvm通过维护一个列表记录哪些内存可用,在分配内存的时候,从列表里面找到一个大于对象大小的内存进行分配,并且更新列表记录

使用哪种内存分配方式取决于使用哪种垃圾收集器:

  Serial、ParNew等带有Compact过程,采用指针碰撞

  类似CMS这种基于Mark-Sweep算法的收集器,采用空闲列表

并发环境下分配内存

  如果给对象A分配内存的时候,指针还没来及修改,对象B使用原来的指针来分配内存(类似脏读)

解决方案

  ①.对分配内存的动作进行同步处理--通过CAS+失败重试的方式保证操作的原子性(了解CAS机制可以查看Atomic源码)

  ②.使用本地线程缓冲区TLAB,就是为每个线程在堆中预先分配一小块内存,各个线程分配内存相互不影响(类似ThreadLocal变量概念),只有在TLAB用完重新分配新的TLAB时,才需要进行同步处理。通过-XX:+/-UseTLAB参数配置是否使用TLAB

  上述分配内存动作完成。

2、初始化内存

  此时,jvm将分配的内存空间初始化为零值(不包含对象头),如果使用TLAB,在分配TLAB时提前完成这一步

作用:

  为了保证用户没有赋初值的情况下也是可以使用

  然后设置对象头保存的一些信息,例如:这个对象是哪个类的实例,如何找到类的元数据信息、对象的哈希码、GC分代年龄等。
  都完成之后,VM角度来看,一个对象已经产生,但程序的角度,对象创建才刚开始——<init>方法还没执行,所有字段都是零值,初始化过程,一个真正的对象才完成

3、对象的内存布局(HotSpot)

对象头Header

  ①存储对象本身的运行时数据,如hashcode,GC分代年龄、锁状态标志、线程持有的锁等

  ②类型指针:对象指向本身类元数据的指针,虚拟机就是通过类型指针确定对象是哪个类的实例。但不是所有的对象都保存类型指针,所以查找类的元数据也不一定通过对象本身

  针对数组,Header还要一个记录数组长度的数据,因此无法通过数组的元数据确定数组大小,但是普通java对象是可以的

实例数据Instance Data

  对象真正存储的有效数据,也就是代码定义的各种类型字段,无论是从父类继承的,还是子类本身定义的。相同宽度的字段被分配到一起。满足这个条件前提下,父类定义到子类前,但是如果CompactFields=true,子类较小的变量也可能插入到父类变量的空隙

对齐填充Padding

  不是必然存在的,没有特别的意义,起着占位符的作用,对象起始地址要是8字节的整数倍,就是对象的大小必须是8字节的整数倍,对象头是8字节的1倍或2倍,当对象实例数据部分没有对齐时,需要对齐填充来补全。

4、对象的访问定位

  java程序通过栈中的reference操作堆中的具体数据。

定位方式:

  ①.句柄访问(间接):堆中维护一个句柄池,本地变量表中reference存储的就是对象的句柄地址,句柄池包含了对象实例数据(堆)和类型数据(方法区)各自的地址信息

  ②.直接指针访问:reference中存储的就是对象实例数据,实例数据有指向方法区中的对象类型数据的指针

 

对比

  句柄访问的优点:reference中保存的只是句柄地址,如果对象GC被移动只需要改变实例数据指针,reference本身不需要改变

  指针访问的优点:速度更快。节省一次指针定位的时间开销,因为对象访问在java很频繁,所以节省的时间开销是很客观的。HotSpot采用指针访问

猜你喜欢

转载自www.cnblogs.com/huigelaile/p/10830403.html