Java中对象的布局

前言

本文首发于spheign的博客网站,欢迎转载。

1 概述

先说结论,Java对象保存在内存中时,由对象头实例数据对对齐填充字节组成。
我们可以借助openjdk的jol-core包很方便的输出对象布局。


    org.openjdk.jol
    jol-core
    0.9

static class L{
    private final String str = "hello world";
}

public static void main(String[] args) {
    L l = new L();  //new 一个对象
    System.out.println(ClassLayout.parseInstance(l).toPrintable());//输出 l对象 的布局
}
 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)                           94 ef 00 f8 (10010100 11101111 00000000 11111000) (-134156396)
     12     4   java.lang.String L.str                                     (object)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
  • OFFSET:偏移地址,单位字节;
  • SIZE:占用的内存大小,单位为字节;
  • TYPE DESCRIPTION:类型描述,其中object header为对象头;
  • VALUE:对应内存中当前存储的值;
    可以看出,对象头所占用的内存大小为12 * 8bit = 96bit,我是用的jdk版本是1.8,默认开启了指针压缩。可以通过vm参数-XX:-UseCompressedOops进行关闭,关闭后对象头所占用的内存大小为16 * 8bit = 128bit
    关闭后输出
 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)                           f0 fe 70 09 (11110000 11111110 01110000 00001001) (158400240)
     12     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
     16     8   java.lang.String L.str                                     (object)
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

些许不同的是我们还要注意一下数组对象,将示例中的L替换成一个对象数组后:

static class L{
    private final String str = "hello world";
}

public static void main(String[] args) {
    L[] l = new L[7];  //new 一个对象
    System.out.println(ClassLayout.parseInstance(l).toPrintable());//输出 l对象 的布局
}
  1. 关闭指针压缩
 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)                           e8 60 57 07 (11101000 01100000 01010111 00000111) (123166952)
     12     4                           (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
     16     4                           (object header)                           07 00 00 00 (00000111 00000000 00000000 00000000) (7)
     20     4                           (alignment/padding gap)                  
     24    56   com.spheign.szjx.Test$L Test$L;.                        N/A
Instance size: 80 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
  1. 开启指针压缩
 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)                           d3 ef 00 f8 (11010011 11101111 00000000 11111000) (-134156333)
     12     4                           (object header)                           07 00 00 00 (00000111 00000000 00000000 00000000) (7)
     16    28   com.spheign.szjx.Test$L Test$L;.                        N/A
     44     4                           (loss due to the next object alignment)
Instance size: 48 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

我们发现,对象头中多出来一行12 4 (object header) 07 00 00 00 (00000111 00000000 00000000 00000000) (7),这行代表的就是数组的长度,本例为7

我们随便拿一条输出来说明一下每一行代表什么意思。

 OFFSET  SIZE                      TYPE DESCRIPTION                                           VALUE
      0     4                           (object header)    // Mark Word                      01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                           (object header)    // Mark Word                      00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                           (object header)    // Klass Pointer 类元数据的指针     d3 ef 00 f8 (11010011 11101111 00000000 11111000) (-134156333)
     12     4                           (object header)    // 数组长度                        07 00 00 00 (00000111 00000000 00000000 00000000) (7)
     16    28   com.spheign.szjx.Test$L Test$L;. // Instance Data 对象实际的数据     N/A
     44     4                           (loss due to the next object alignment)  //Padding 对齐填充数据
Instance size: 48 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
object

2 对象头

通过上面的学习,我们能够很容易的说出来对象头的组成,Mark Word类元数据的指针(Klass Pointer)数组长度(不一定有)
而对象头的重点内容都在Mark Word上,它主要用来存储对象自身的运行时数据,mark word的位长度为JVM的一个Word大小,也就是说32位JVM的Mark word为32位,64位JVM为64位。

下表是64位的情况

锁状态 分代年龄 (4bit) 是否偏向锁 (1bit) 锁标志位 (2bit)
无锁 unused (25bit) hashcode (31bit) unused (1bit) age (4bit) 0 01
偏向锁 thread id (54bit) epoch (2bit) unused (1bit) age (4bit) 1 01
轻量级锁 ptr_to_lock_record (62bit) 00
重量级锁 ptr_to_heavyweight_monitor (62bit) 10
GC标志 11
lock

2.1 锁标识

上图中的内存占用的顺序正好可以对应到我们前面代码例子中的输出,不过要转换一下,我们把mark word的部分拿出来

 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)

VALUE部分是0x0100000000000000,将其倒叙0x0000000000000001,再转换成二级制就正好对应起来了。所以,我们看一个对象正好处于什么类型的锁,我们只需要查看后三位就可以了。无锁的情况是001我们已经在上面的示例中展现出来了,这里我想把其他三种锁的情况也都做一个示例输出出来,很遗憾偏向锁的示例我还没有好的方案,所以这里就先展示轻量级锁和重量级锁。

static class L{
  private final Object lock = new Object();
  public void run(String name){
    synchronized (lock) {
      System.out.println(">>>>>>> thread name : " + name);
      System.out.println(ClassLayout.parseInstance(lock).toPrintable());
    }
  }
}

public static void main(String[] args) {
  L l = new L();
  ExecutorService pool = Executors.newCachedThreadPool();
  pool.execute(()->{
    String threadName = Thread.currentThread().getName();
    l.run(threadName);
  });
  pool.shutdown();
}
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           a8 d7 fa 03 (10101000 11010111 11111010 00000011) (66770856)
      4     4        (object header)                           00 70 00 00 (00000000 01110000 00000000 00000000) (28672)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

最后一位的二进制位10101000,轻量级锁。

我们再加一个线程

static class L{
  private final Object lock = new Object();
  public void run(String name){
    synchronized (lock) {
      System.out.println(">>>>>>> thread name : " + name);
      System.out.println(ClassLayout.parseInstance(lock).toPrintable());
    }
  }
}

public static void main(String[] args) {
  L l = new L();
  ExecutorService pool = Executors.newCachedThreadPool();
  pool.execute(()->{
    String threadName = Thread.currentThread().getName();
    l.run(threadName);
  });
  pool.execute(()->{
    String threadName = Thread.currentThread().getName();
    l.run(threadName);
  });
  pool.shutdown();
}
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           5a 11 03 a6 (01011010 00010001 00000011 10100110) (-1509748390)
      4     4        (object header)                           ce 7f 00 00 (11001110 01111111 00000000 00000000) (32718)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

最后一位的二进制位01011010,重量级锁。

这里有个重要的知识点叫锁升级,我画了一个图来描述锁升级的过程:

lock

有一个知识点我们必须要明确,synchronized锁的是对象而不是其包裹的代码。

  1. 对象被new出来后,没有任何线程持有这个对象的锁,这时就是无锁状态;
  2. 当且仅当只有一个线程A获取到这个对象的锁的时候,对象就会从无锁状态升级成为偏向锁,Mark Word中就会记录这个线程的标识,此时线程A持有这个对象的锁;
  3. 还是这个线程A再次获取这个对象的锁时,发现他是一个偏向锁,并且对象头中记录着自己的线程标识,那么线程A就继续使用这把锁。这里也是锁的可重入性,所以,synchronized也是可重入锁;
  4. 在线程A持有锁的时候,线程B也来争抢这把锁了,线程B发现这是一把偏向锁,并且对象头中的线程标识不是自己。那么,偏向锁就会升级为轻量级锁;
  5. 又有一些线程来争抢这个轻量级锁了,争抢的过程其实就是利用CAS自旋。为了避免长时间自旋消耗CPU资源,当自旋超过10次的时候,轻量级锁升级为重量级锁。

上面描述的就是锁升级的过程,理论上锁每升级一次,效率就会变差,但事实真是如此吗?

  1. 偏向锁的效率未必高。

    偏向锁有锁撤销的操作,这个操作会消耗CPU资源,如果频繁的进行 无锁--偏向锁--无锁 的转换,还不如直接使用轻量级锁。也就是只有一个线程频繁的获取锁释放锁的过程。

  2. 轻量级锁为什么要升级为重量级锁?

    上面也说了,轻量级锁在争抢的时候会进行自旋的操作,当有许许多多的线程同时进行自旋的时候,将相当的耗费CPU资源。控制自旋次数与时间也是CAS要做的优化内容。

  3. 重量级锁为什么效率差?

    重量级锁为OS级锁,线程会进入等待队列中等待CPU的调用,因此,在进行线程切换的时候会比较耗时。

  4. synchronized和Lock (CAS)应该如何选择?

    a. synchronized:高争用、高耗时的场景,因为等待队列不消耗CPU资源;

    b. Lock (CAS):低争用、低耗时的场景,因为此时自旋次数很少就能拿到锁;

    以上是理论情况,实际开发中一定要遵循实测的结果!!!

2.2 分代年龄

我们平时遇见最多的问题就是分代年龄的最大值,显而易见,分代年龄只有4bit所以的最大值也就是15
分代年龄又叫GC分代年龄,他和垃圾回收机制有关,GC回收的又是堆(Heap)空间的内容,所以要理解分代年龄就要先搞清楚Heap空间。

2.2.1 堆Heap空间

在《深入理解Java虚拟机》一书中对以上内容讲解的非常清晰,建议大家都去读一读。
堆被分为三个区域,我画了一张图来直观的说明一下


Java8

上图的元空间并不适用与jdk1.7的版本,在jdk1.7中,元空间的位置是持久代。元空间使用的是本机物理内存,而持久代使用的是JVM的堆内存。

Java7

分区的目的就是为了优化GC的性能,避免GC运行时对整个Heap空间进行扫描。Java对象中的绝大多数都是临时对象,存活时间很短,分区后,只在很小的范围内扫描这些数据。

2.2.2 对象在堆空间中的生命周期(分代年龄的作用)

我们模拟一下对象分配空间的过程

  1. 新的对象在Eden区被new出来(大对象例外),一开始的时候两个幸存者区域和老年区都是空的;

    1
  2. 对象创建的越来越多,Eden区域逐渐被填满;

    2
  3. 此时将触发Minor GC,删除没有引用的对象,没有被删除的对象被复制到From幸存区,然后清空Eden区域;

    3
  4. 对象继续创建,Eden区域又满了,再一次触发Minor GÇ,删除没有引用的对象,留下存在引用的对象,将这些对象和之前复制到From幸存区的对象一起复制到To幸存区,然后清空Eden区和From区。这两步也叫做GC的复制算法。

    4
  5. 对象继续创建,Eden区域又满了,第三次触发Minor GÇ。与上次不同的是,To区From区将发生角色转换,然后继续执行第四步。

    5
  6. 上面的操作其实已经修改了分代年龄,Minor GC每发生一次,没有被删除的对象的分代年龄就会+1,直到达到分代年龄的阀值(默认是15,由JVM参数MaxTenuringThreshold决定),这些对象就被移动到老年区

    6
  7. 当老年区的存储快满了时,将触发Major GC,清理老年区没有被引用的对象。

3 实例数据

并不是所有的变量都存放在这里,对象的的所有成员变量以及其父类的成员变量是存放在这里的。

也就是说,静态变量和常量是不在这里面存储的,它们被存放在方法区中。

这部分存储的顺序会受到虚拟机的分配策略参数(FieldsAllocationStyle)和字段在Java源码中的定义顺序影响。

4 对齐填充

JVM要求Java对象的大小必须是8byte的倍数,所以这个的作用就是把对象的大小补齐至8byte的倍数。

注意:不是8bit(比特)的倍数,是8bytes(字节)的倍数,1byte = 8bit。

你可能感兴趣的:(Java中对象的布局)