Java对象创建过程及内存布局和访问方式

126 阅读3分钟

对象创建

Java对象:数组,class对象,普通对象

普通对象的创建过程:

==> 收到new指令。

==> 检查这个指令的参数是否能再常量池中定位到一个类的引用。

==> 检查这个符号引用代表的类是否已被加载、解析、初始化过。(如果没有,则进行类加载)

==> 虚拟机为新生对象分配内存。(对象所需内存大小在类加载完成后可完全确定)

如何分配内存?

  • 如果内存是规整的,指针碰撞,移动指针就可以了。(Serial,ParNew等带Compact过程的收集器)

  • 如果内存不规整的,空闲列表,维护一个列表,记录哪些内存块可用。(CMS这种基于Mark-Sweep的)

如何处理并发?

  • 进行同步处理,CAS+失败重试的方式保证更新操作的原子性

  • 按线程划分 TLAB(本地内存缓冲区),只需要在TLAB用完并分配新的TLAB时需要同步锁定

==> 虚拟机将分配的内存空间全部初始化为零值(不包括对象头)

==> 虚拟机对对象头进行设置。

这个对象是哪个类的实例、如何找到类的元数据信息、对象的hash码、对象的gc分代年龄。

是否启用偏向锁等

==> 执行<init>方法,通过构造函数按程序逻辑进行初始化。

==> 对象创建完成。

内存布局

  1. 对象头

    • MarkWord,用于存储对象自身的运行时数据

      • 哈希码
      • GC分代年龄
      • 锁状态标志
      • 线程持有的锁
      • 偏向线程id
      • 偏向时间戳

      未锁定的状态下:

      对象哈希码 GC分代年龄 锁标志位 固定为0
      25bit 4bit 2bit 1bit

      其他状态下:(轻量级锁定、重量级锁定、GC标记、可偏向)

      存储内容 标志位 状态
      对象哈希码、对象分代年龄 01 未锁定
      指向锁记录的指针 00 轻量级锁定
      指向重量级锁的指针 10 膨胀
      空,不需要记录信息 11 GC标记
      偏向线程id、偏向时间戳、对象分代年龄 01 可偏向
    • 类型指针,即指向它的类元数据的指针(通过元数据可以确定对象的大小)

    • 数组长度,如果对象是数组(通过元数据无法确定数组大小,所以需要记录数组长度)

  2. 实例数据

    对象真正存储的有效信息,包括父类继承的和子类定义的

    存储顺序受虚拟机分配策略参数和源码中定义顺序的影响

    默认分配策略:(相同宽度的字段总是分配到一起)

    • longs/doubles
    • ints
    • shorts/chars
    • bytes/booleans
    • oops(Ordinary Object Pointers)

    在父类中定义的变量会出现在子类之前,如果CompactFields参数值为true(默认为true),那么子类之中较窄的变量也可能会插到父类变量的空隙之中

  3. 对齐填充

    HotSpot自动内存管理系统要求对象起始地址必须是8字节的整数倍,即对象的大小必须是8的整数倍。因此如果对象的实例数据部分没有对齐时,就需要通过对齐填充来补全

访问定位

  1. 句柄访问(存储对象实例数据的指针和到对象类型数据的指针)

    好处:稳定的reference地址,对象被移动时只会改变句柄中的对象实例数据指针

  2. 直接指针(需要对象实例数据存储到对象类型数据的指针,HotSpot VM)

    好处:速度更快,节省了一次指针定位的时间开销