Java对象内存布局

1. 对象内存布局

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

图3

1.1 对象头

对象头包括两部分信息:Mark Word类型指针

Mark Word
  • Mark Word 存储对象自身的运行时数据,包括哈希码GC分代年龄锁状态标志偏向线程ID等;
  • 对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构,以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存款空间;

Mark Word 在32位和64位虚拟机中的存储结构如下图:

  • 32位虚拟机中的存储结构
图4
  • 64位虚拟机中的存储结构
图5
类型指针
  • 对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;
  • 类元数据存储在方法区中;

1.2 实例数据

  • 实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容;
  • 无论是从父类继承下来的,还是在子类中定义的,都需要记录起来;
  • 字段的存储顺序会受虚拟机分配策略参数和字段在Java源码中定义顺序的影响;

1.3 对齐填充

  • 对齐填充不是必然存在的,仅仅起着占位符的作用;
  • 由于HotSpot要求对象起始地址必须是8字节的整数倍,即对象的大小必须是8字节的整数倍;而对象头部分刚好是8字节的倍数,因此,当对象实例数据部分没有对齐时,需要通过对齐填充来补全;

2. 对象的创建

虚拟机遇到一条new指令时,会依次执行类加载检查内存分配修改指针空间初始化零值对象设置,然后交给Java程序进行对象初始化

2.1 类加载检查

类加载检查会检查这个指令的参数(类名)是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过,如果没有则必须先执行相应的类加载过程

2.2 内存分配

在类加载检查通过后,虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定;

为对象分配空间的任务,等同于把一块确定大小的内存从Java堆中划分出来,根据内存是否规整,分配方法有两种:指针碰撞空闲列表

指针碰撞

假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器;那么分配内存时,就仅仅将这个指针向空闲空间挪到一段与对象大小相等的距离即可;

压缩整理功能的垃圾收集器可以使用这种分配方式,如SerialParNew(垃圾收集器会在下一篇文章详解)

空闲列表

如果已使用的内存空间和空闲的内存空间是互相交错的,那么虚拟机就必须维护一个列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录;

在使用CMS这种基于标记-清除算法的收集器时,通常采用空闲列表分配;

2.3 修改指针

内存分配成功后,需要修改对象的指针指向分配的位置,在并发的情况下不是线程安全的,可能出现正在给A对象分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况;

解决方案有两种:

  1. 同步处理:虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;
  2. 本地线程分配缓冲:把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定;
  • 虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定;

2.4 空间初始化零值

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就可以直接使用,程序能访问到这些字段的数据类型所对应的零值;

如果使用TLAB,这一工作也可以提前至TLAB分配时进行;

2.5 对象设置

接下来,虚拟机对对象进行必要的设置,即设置对象头的值;

例如,这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息;

2.6 对象初始化

对象设置完成后,对虚拟机来说,对象已经产生,可以交给Java程序,对Java程序来说,对象创建才刚刚开始,方法还没执行,所有的字段都还为零,所以执行完new指令之后会接着执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生

3. 对象的访问定位

建立对象是为了使用对象,Java程序需要通过上的reference数据来操作上的具体对象。

reference类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中的对象的具体位置,所以对象访问方式也是取决于虚拟机实现的,目前主流的访问方式有两种:使用句柄直接指针

3.1 使用句柄访问

如果使用句柄访问的话,Java堆中会划分出一块内存来存放句柄池,reference中存储的就是对象的句柄地址,而句柄中包括了对象实例数据类型数据各自具体的地址

优势:

  • reference中存储的是稳定的句柄地址,在对象被移动时(垃圾收集时移动对象是非常普遍的行为)只会改变句柄中的实例数据指针,而reference本身不需要修改;
图10

3.2 使用直接指针访问

如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址;

HotSpot虚拟机中就是使用直接指针访问,访问对象类型数据的相关信息放在对象头里,由类型指针指向方法区中的对象类型数据(见第一小节)

优势:

  • 速度快,节省了一次指针定位的时间开销
图11

参考文献

  • 《深入理解Java虚拟机》第2版,周志明 著;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值