Bootstrap

Java并发编程系列插曲——对象的内存结构

这一篇从介绍一个工具类入手,这个工具类可以用来查看Java中类和对象的内存结构,知道如何查看内存结构,以后遇到一些相关的问题,或看到相关的文章,就可以动手输出一下,更有利于理解和解决问题。

先说下这个工具类,org.openjdk.jol.info.ClassLayout,如何引用这个工具类?现在的项目基本是基于各种构建工具创建,就以maven为例,要引入其jar包,在pom中加入如下声明即可:



    org.openjdk.jol
    jol-core
    0.9

虽然看包名出处是openjdk,但在OpenJDK和Oracle JDK上都可以使用,文中所使用的是Orcale JDK13。

使用这个工具类演示之前,我们可以考虑个问题,我们定义一个类,实例化一个对象,它们到底占用多少内存?内存中的结构是怎么样的?通过简单几个例子可以说明一下(嗯,我也只懂简单的)。

比如我们定义一个类,输出一下它的内存结构看看,如下代码所示:

public class ShowClassLayout {
    public static class Lock {
    }

    public static void main(String[] args) {
        System.out.println(ClassLayout.parseClass(Lock.class).toPrintable());
        Lock lock = new Lock();
        System.out.println(ClassLayout.parseInstance(lock).toPrintable());
    }
}

代码中定义了一个类Lock,在程序中,首先输出了类的内存结构,然后输出实例化后的内存结构,结果分别如下:

ShowClassLayout$Lock object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0    12        (object header)                           N/A
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

ShowClassLayout$Lock object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           4a 9b 16 00 (01001010 10011011 00010110 00000000) (1481546)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以看到,类因为没有实例化,所以没有具体分配内存,所以只有其内存占用字节数的信息,而对象经过实例化,其内存信息有了具体的值。其中一个信息可以注意下,其内存中有4个字节是用来做补齐的,并没有实际内容,所以从这点上也可以提醒我们一下,如果要评估某个功能需要产生大量的对象实例或传输大量的对象实例,计算其大小时就不能简单的只是把其内部的变量的内存大小相加,否则可能会产生成倍的差距,造成误判。

为什么会产生补齐的问题?简单说就是为了效率。而补齐也是有规则的,比如我们用的64位虚拟机,64位就是一个寻址单元,那么如果类或者说对象占用内存的大小不是64的整数倍,那就会产生额外的补齐。我们可以在对象中加字段来举例说明一下。如下代码所示:

public class ShowClassLayout {
    public static class Lock {
        byte b;
    }

    public static void main(String[] args) {
        System.out.println(ClassLayout.parseClass(Lock.class).toPrintable());
        Lock lock = new Lock();
        System.out.println(ClassLayout.parseInstance(lock).toPrintable());
    }
}

代码中第三行我们增加一个byte类型的字段,再看下其内存结构,这次只看类的情况,如下:

ShowClassLayout$Lock object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0    12        (object header)                           N/A
     12     1   byte Lock.b                                    N/A
     13     3        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total

可以看出,因为byte占用1个字节,所以这次用于补齐的就成了3个字节,相应的最后给出的内存损失(也就是补齐所用的内存)由4字节变为3字节,但可以看到总的字节数还是16,仍然是64位的倍数。也可以加入其他的类型试试,比如byte换成int,int占用4字节,这样就不需要补齐,会发现没有额外的补齐了,就不在代码上列出了。

那再考虑一个问题,就是如果引用的不是原生类型,而是对象呢?那我们来看一下,如下代码所示:

public class ShowClassLayout {
    public static class Lock {
        byte b;
        Ref ref;
    }

    public static class Ref {
    }

    public static void main(String[] args) {
        System.out.println(ClassLayout.parseClass(Lock.class).toPrintable());
        Lock lock = new Lock();
        System.out.println(ClassLayout.parseInstance(lock).toPrintable());
    }
}

其内存结构,如下所示:

ShowClassLayout$Lock object internals:
 OFFSET  SIZE                  TYPE DESCRIPTION               VALUE
      0    12                       (object header)           N/A
     12     1                  byte Lock.b                    N/A
     13     3                       (alignment/padding gap)                  
     16     4   ShowClassLayout.Ref Lock.ref                  N/A
     20     4                       (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

可以看到这次有两段补齐,byte与ref之间,有一个3字节的补齐,而ref后有一个4字节的补齐,这次的内存损失总计就有7字节。可以看出类或对象的内部是以4字节为单位的,不足4字节的就会补齐到4字节,才跟下一个属性,而类或对象整体上依然要满足64位的整数倍。其中属性为对象的占用4字节,也就是说这里存放的是对一个引用地址。

这里还会产生一个问题,为什么64位的虚拟机类和对象必须满足64位的整数倍要求,而内部的引用为什么是32位的?这不合常理。确实不合理,这么做的原因也是为了效率,或者说性能,所以64位虚拟机内部默认开启了“压缩指针”,即指针都使用了压缩方式的32位。我们可以把压缩指针取消,再看一下。要取消压缩指针在运行时加入参数“-XX:-UseCompressedOops”,我们再看一下这时的输出,如下所示:

ShowClassLayout$Lock object internals:
 OFFSET  SIZE                  TYPE DESCRIPTION             VALUE
      0    16                       (object header)         N/A
     16     1                  byte Lock.b                  N/A
     17     7                       (alignment/padding gap)                  
     24     8   ShowClassLayout.Ref Lock.ref                N/A
Instance size: 32 bytes
Space losses: 7 bytes internal + 0 bytes external = 7 bytes total

有几个变化,对象头共占用16个字节,而不是12了,byte占用1个,后面的补齐变成了7个字节,而引用也变成了8个字节,这要不是到了内存管理极限是不需要开启的。

对象头里是些什么内容?用一张图来说明一下(来源搜索引擎)

先回顾下刚才对象内存结构:

ShowClassLayout$Lock object internals:
 OFFSET  SIZE                  TYPE DESCRIPTION                               VALUE
      0     4                       (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4                       (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                       (object header)                           4a 9b 16 00 (01001010 10011011 00010110 00000000) (1481546)
     12     1                  byte Lock.b                                    0
     13     3                       (alignment/padding gap)                  
     16     4   ShowClassLayout.Ref Lock.ref                                  null
     20     4                       (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

然后我们调用一下这个对象的hashcode,并加个锁来看看变化,代码如下:

public class ShowClassLayout {
    public static class Lock {
        byte b;
        Ref ref;
    }

    public static class Ref {

    }

    public static void main(String[] args) {
        System.out.println(ClassLayout.parseClass(Lock.class).toPrintable());
        Lock lock = new Lock();
        lock.hashCode();
        System.out.println(ClassLayout.parseInstance(lock).toPrintable());
        synchronized (lock) {
            System.out.println(ClassLayout.parseInstance(lock).toPrintable());
        }
    }
}

然后看下内存结构,如下所示:

ShowClassLayout$Lock object internals:
 OFFSET  SIZE                  TYPE DESCRIPTION                               VALUE
      0     4                       (object header)                           58 7a 53 05 (01011000 01111010 01010011 00000101) (89356888)
      4     4                       (object header)                           00 70 00 00 (00000000 01110000 00000000 00000000) (28672)
      8     4                       (object header)                           4a 9a 16 00 (01001010 10011010 00010110 00000000) (1481290)
     12     1                  byte Lock.b                                    0
     13     3                       (alignment/padding gap)                  
     16     4   ShowClassLayout.Ref Lock.ref                                  null
     20     4                       (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

可以拖动代码块看下相应的hash值的位置和锁位置内存都发生变化。对于这部分了解下就好,万一遇到要排查问题时需要,再对照检查。

这样下来,对内存结构就会有个简单的认识了。

本系列其他文章: