HotSpot在Java堆中对象分配、布局和访问的过程

Java是一门面向对象语言。程序中对象的创建(eg. 克隆,反序列化)非常常见。在语言层面上创建对象通常仅仅通过一个new关键字而已,而在虚拟机中,对象创建又是怎样的呢?

主要过程有三个部分:

  • 对象的创建
  • 对象的内存布局
  • 对象的访问定位

对象的创建

(1)执行类的加载过程
虚拟机遇到一条new指令时,首先检查指令参数是否在常量池中,如果有一个类的符号引用则代表类被加载、解析过。如果没有,则先执行类的加载过程。具体的类加载过程可以查看虚拟机类加载机制
(2)为新生对象分配内存
对象所需内存大小在类加载完成后可完全确定。
假设Java堆中内存绝对规整,那么分配内存就仅仅把用于分隔可用内存和已用内存指针挪动一段距离(Bump the Pointer);
如果Java内存并不规整,虚拟机必须维护一个列表记录哪些内存块可用(Free List)。
Note:Java堆是否规整由所采用的垃圾回收器是否带有压缩整理功能决定。
分配内存时,需要考虑到修改指针的线程安全性.两种方案:

  • 在分配内存空间代码处进行同步处理
  • 把内存分配划分在各自线程的空间中,即每个线程预先在堆中分配一小块内存,成为本地线程分配缓冲(TLAB)

(3)初始化内存空间

  • 将除对象头的内存空间初始化为零值
  • 将类的元数据,对象hashcode,对象的gc分代年龄等信息放在对象头中(Object Header)

上述工作完成后,便会执行方法


对象的内存布局

对象内存中存储的布局可以分为3块区域:对象头(Object Header),实例数据(Instance Data),对齐填充(Padding)

  • 对象头包括两部分信息。第一部分是存储对象自身运行时数据,如HashCode, GC分代年龄,偏向线程ID等。这部分成为“Mark Word”。第二部分是类型指针,即对象指向它的类元数据指针。如果是一个Java数组,还需要有块记录长度的数据。
  • 实例数据存储顺序会受到虚拟机分配策略和字段定义顺序影响。虚拟机分配策略为:longs/doubles, ints, shorts/chars, bytes/booleans, oops(Ordinary Object Pointers). 满足这个前提下,父类定义变量灰出现在子类之前。
  • HotSpot VM要求对象起始地址必须是8字节整数倍。

对象的访问定位

目前主流的访问方式有使用句柄和直接指针两种

  • 使用句柄,Java会划出一块内存来作为句柄池。reference存储的就是对象的句柄地址。句柄中包含对象的实例数据和类型数据各自的地址信息。
  • 直接指针访问,reference存储的直接就是对象地址

这两种访问各有优势。使用句柄最大好处是对象移动(垃圾回收时对象移动非常普遍)时只会改变句柄中的实例数据指针,而reference不需要修改。使用直接指针访问方式的最大好处就是速度更快。由于对象访问在Java中非常频繁,因此这类开销积少成多也是一项非常可观的执行成本。


参考文献
深入理解Java虚拟机-周志明