JVM探秘:Java对象

对象的创建

  1. 虚拟机遇到一条 new 指令时,首先去检查这个指令的参数是否能在方法区常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,就必须先执行相应的类加载过程。

  2. 类加载检查过后,虚拟机为新生对象分配内存。对象所需内存大小在类加载过后就完全确定,为对象分配空间就等同于,从 Java 堆中划分出一块确定大小的内存。

    此时有两种情况:

    • 如果 Java 堆中内存是规整的,那么虚拟机将会采用指针碰撞(Bump the Pointer)分配内存。也就是用过的内存放在一边,空闲的内存放另一边,中间有个指针作为分界点指示器,那么分配内存就是把指针向空闲那一边挪动了与对象相同大小的距离。

    • 如果 Java 堆中内存不规整,已使用和未使用的内存相互交错,那么虚拟机将会采用空闲列表(Free List)分配内存。也就是虚拟机会维护一个空闲列表,记录哪些内存块可用,分配内存时就从空闲列表中找到一块足够大的空间分给对象实例,并更新列表。

      选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整由垃圾收集器是否带有压缩整理功能决定。比如Serial、ParNew这种基于复制算法和标记-整理算法的收集器,就是采用指针碰撞法分配内存。而CMS这种基于标记-清除算法的收集器就是采用空闲列表。

      关于垃圾收集算法和垃圾收集器,后面会单独有文章讲到。

      还有一个问题就是要保证分配内存时的线程安全,即使是简单的指针碰撞修改一下指针的位置,在并发情况下也不是线程安全的。解决这个问题有两种方案,一种是对分配内存的动作进行同步处理,保证操作的原子性;另一种就是前面提到的本地线程分配缓冲TLAB。

  3. 在 Java 堆中内存分配完成后,虚拟机会将已分配的内存空间初始化为零值(不包括对象头)。这保证了对象的实例字段在 Java 代码中可以不用赋初始值就可以直接使用,程序能访问到这些字段的数据类型所对应的零值。如果采用了 TLAB,这一步也可以提前至 TLAB 分配时进行。

  4. 接下来对对象进行必要的设置,例如这个对象是哪个类的实例、如何找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息都存放在对象的对象头(Object Header)中。

  5. 这个时候,从虚拟机的角度看一个新的对象已经产生了,但从 Java 程序角度看对象的创建才刚开始,init 方法还没执行,所有的字段都还为零。所以这时会执行 init 的方法,把对象按照程序员的意愿初始化。

对象的内存布局

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

对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,官方称这部分数据为 Mark Word。另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针确定对象是哪个类的实例。但不是所有的虚拟机实现都会在对象数据上保留类型指针,也就是说查找对象的元数据信息不一定要经过对象本身,这点会在下面对象定位方式中讲到。另外如果对象是一个 Java 数组,对象头中还会记录数组长度,用来确定 Java 对象的大小。

HotSpot虚拟机对象头Mark Word:

存储内容 标志位 状态
对象哈希码、分代年龄 01 未锁定
指向锁记录的指针 00 轻量级锁定
指向重量级锁的指针 10 膨胀(重量级锁定)
空,不需要记录信息 11 GC标记
偏向线程ID、偏向时间戳、对象分代年龄 01 可偏向

实例数据是对象的真正有效数据,也就是代码中定义的字段内容。这部分存储数据受到虚拟机分配策略参数和字段在 Java 源码中定义顺序的影响。HotSpot 虚拟机默认分配策略为 longs/doubles、ints、shorts/chars、bytes/booleans、oops。相同宽度的字段总是被分配到一起。在这个前提下,父类中定义的变量会出现在子类之前。如果CompactFields参数为true,那么子类中较窄的变量也可能插到父类变量的空隙中。

对齐填充并不是必然存在的,也没有特殊含义,它仅仅是占位符。由于 HotSpot VM 的自动内存管理系统要求对象起始地址必须是8字节的整数倍,也就是对象大小必须是8字节的整数倍。因此当对象实例数据没有对齐时,就会通过对齐填充来补全。

对象的访问定位

建立对象是为了使用对象,Java 程序需要通过 Java 栈上的 reference 数据来操作 Java 堆上的对象。reference 类型就是一个指向对象的引用。对象的访问方式取决于虚拟机实现,目前的访问方式主要有使用句柄和直接指针两种。

  • 如果使用句柄访问,Java 堆中将会划分出一块内存作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象的实例数据与类型数据的地址,如下图:
    image

  • 如果使用直接指针访问,reference 中存储的直接就是对象地址,但是对象中还会放置访问类型数据的指针,如下图:
    image

这两种对象访问方式各有优劣,使用句柄访问的话,reference 中存储的是稳定的句柄地址,垃圾收集时移动对象的话,只会修改句柄中的实例数据指针,而 reference 本身不需要修改。

使用直接指针访问的最大好处就是速度更快,节省了一次指针定位的时间,对象访问非常频繁,这种节省极少成多的话也是非常可观。

HotSpot 虚拟机使用的是直接指针访问的方式。