Java 高并发与多线程(二)——Java 普通对象的对象头及组成部分

本贴最后更新于 1521 天前,其中的信息可能已经事过景迁

Java 中的对象除了我们可见的属性部分,其实还有另外不可见的内容,这部分就是对象头(Object Header)。本文讲探究一下对象头里具体都有哪些细节,为将来 synchronized 关键字的讨论打一个基础。

1. 工具准备

笔者本地使用 64 位的 JDK8,然后引入一个叫 JOL 的依赖包,全称 Java Object Layout。截止到本文写作时,最高版本为 0.10,这里选用了非最新的使用较多的一个版本,GAV 如下:

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>

这个工具本身功能很丰富,可以从各个角度对内存中的对象进行查看,我们这里只使用查看对象内部结构的功能,代码如下。(有兴趣的同学可以去 OpenJDK 官网参考更多样例)

public class Test {

    public static void main(String[] args) {
        Object o = new Object();

        ClassLayout classLayout = ClassLayout.parseInstance(o);

        System.out.println(classLayout.toPrintable());
    }
}

执行输出如下:

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

这里需要特别说明的是,由于存在大端小端的问题,我们看上面 bit 值的顺序应该是每 8 个 bit 一组,组内从左往右看,组间从后往前看。即,第二行第四组 bit 值为整个 Object Header 的开头,其中第四组从左开始的第一个 bit 也是整个对象头的第一个 bit;第一行第一组的 bit 值为整个对象头的末尾,其中值为 1 的那个 bit 就是整个对象头最后一个 bit。

2. 对象头的具体内容

上一节中我们用 JOL 输出了一个对象在内存中的内容,这一节我们就对这个结果进行分析。

2.1 普通对象的内存组成

普通对象的内存占用由以下四个部分组成:

  1. Mark Word 占 8 字节
  2. Class Pointer 占 4 字节
  3. 实例数据
  4. Padding 对齐

其中 Mark Word 和 Class Pointer 合起来称为 Object Header(对象头)。Padding 部分并不携带任何信息,它存在的原因是当前 64 位处理器每次读取内存都是 8 个字节为一组,因此如果一个对象的大小可以正好被 8 整除,那么每次读取的数据块都可以保证在同一个对象内,省去了判断对象结尾位置这样的操作。

2.2 Mark Word 部分

对象头里面主要的信息都存在 Mark Word 部分,具体内容可以看下面这个表格。

锁状态 56 bit 1 bit 4 bit 1 bit 2 bit
无锁 31 bit 未用 25 bit hash 未用 分代年龄 是否偏向锁 01
偏向锁 54 bit 线程ID 2 bit Epoch 未用 分代年龄 是否偏向锁 01
轻量级锁 指向栈中锁记录的指针 00
重量级锁 指向重量级锁的指针 10
GC标记 11

从这个表格中我们可以看出,Mark Word 中的内容是根据对象的锁状态来决定的,不同的锁状态 Mark Word 会存储不同的内容。涉及锁相关的内容我们会在下一篇讨论 synchronized 关键字的文章中详解,本文会讨论一些其它部分的内容,目前读者只要知道 synchronized 关键字会修改对象的 Mark Word 就好。

2.2.1 hash code

在无锁状态下,Mark Word 会用前 56 个 bit 中的后 25 个 bit 记录 hash code。我们看代码如下:

import org.openjdk.jol.info.ClassLayout;

public class Test {

    public static void main(String[] args) {
        Object o = new Object();

        o.hashCode(); 

        ClassLayout classLayout = ClassLayout.parseInstance(o);

        System.out.println(classLayout.toPrintable());
    }
}

输出是:

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 a5 e5 4a (00000001 10100101 11100101 01001010) (1256563969)
      4     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

根据本节最开始提到的阅读顺序,可以看出,有区别的部分是第一行后三组 bit 和第二行第一组的最后一个 bit,一共是 3 * 8 + 1 = 25 个 bit,这部分就是 hash code。第二行第一组的前 7 个 bit 和后三组 bit,一共 7 + 3 * 8 = 31 个 bit,就是未使用的部分。

这个 hash code 只有在对象第一次调用 hashCode()方法时生成,后面再需要 hash code 的地方就会直接从对象头里面读取。这里我们可以得到一个额外的结论:最好不要使用可变的属性来生成 hash code。原因是如果这个可变属性发生了变化,在类似 HashMap 和 HashSet 这种依赖于 hash code 的场景中,可能会有意外的问题发生。

2.2.2 分代年龄

我们知道,堆内存是划分为新生代和老年代区域的。一个对象在每经历一次 GC 后如果还存活,那么它的年龄就会 +1。这个年龄就是记录在对象头中,占用 4 个 bit,因此一个对象的年龄最大就到 15,再超过时,对象就会从新生代晋升到老年代。当然这个值可以通过--XX:MaxTenuringThreshold 参数来调整,但也不能超过 15。

我们还是来看代码:

import org.openjdk.jol.info.ClassLayout;

public class Test {

    public static void main(String[] args) {
        Object o = new Object();

        ClassLayout classLayout = ClassLayout.parseInstance(o);

        System.gc(); // 向jvm发送gc信号,但不保证一定gc

        System.out.println(classLayout.toPrintable());
    }
}

输出为:

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           09 00 00 00 (00001001 00000000 00000000 00000000) (9)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

根据表格,第一个 bit 未用,从第二到第五个 bit 为分代年龄,我们可以看到第五个 bit 值加了 1。

2.3 Class Pointer

JVM 的内存区域中,有一个叫方法区的部分,这部分主要存储的内容是关于类的元数据。我们在堆内存中的对象,如果需要该类型的元数据,就是通过这个 Class Pointer 来找到的。

有一些 C 基础的同学可能会发现,64 位机器中的指针应该占用 8 个 byte,可是这里只占用了 4 个。其实这里是 JVM 的一个优化内容,叫作指针压缩

我们可以在命令行里输入:

java -XX:+PrintCommandLineFlags -version

可以看到输出为:

-XX:InitialHeapSize=132730432 -XX:MaxHeapSize=2123686912 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
java version "1.8.0_171"
Java(TM) SE Runtime Environment (build 1.8.0_171-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.171-b11, mixed mode)

其中有两项-XX:+UseCompressedClassPointers -XX:+UseCompressedOops 就是控制指针压缩的。经笔者测试,这两个参数只要关闭其中一个,Class Pointer 就会变为 8 字节大小。

3. 下集预告

这篇博文主要是介绍了 Object Header 的大概内容,在下篇 synchronized 关键字的探究中会对 Object Header 中锁的部分进行详细介绍。

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3167 引用 • 8207 回帖 • 1 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...