关于java:JAVA对象布局对象头Object-Header

42次阅读

共计 8288 个字符,预计需要花费 21 分钟才能阅读完成。

因为 Java 面向对象的思维,在 JVM 中须要大量存储对象,存储时为了实现一些额定的性能,须要在对象中增加一些标记字段用于加强对象性能。在学习并发编程常识 synchronized 时,咱们总是难以了解其实现原理,因为偏差锁、轻量级锁、重量级锁都波及到对象头,所以理解 java 对象头是咱们深刻理解 synchronized 的前提条件, 以下咱们应用64 位 JDK 示例

1. 对象布局的总体构造

2. 获取一个对象布局实例

1. 首先在 maven 我的项目中 引入查看对象布局的神器

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

2. 调用 ClassLayout.parseInstance().toPrintable()

public class Main{public static void main(String[] args) throws InterruptedException {L l = new L();  //new 一个对象 
        System.out.println(ClassLayout.parseInstance(l).toPrintable());// 输入 l 对象 的布局
    }
}
// 对象类
class L{private boolean myboolean = true;}

运行后输入:

 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 e4 2c 11 (11110000 11100100 00101100 00010001) (288154864)
     12     4           (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
     16     1   boolean L.myboolean                               true
     17     7           (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total

对象头所占用的内存大小为 16*8bit=128bit。如果大家本人入手去打印输出,可能失去的后果是96bit,这是因为我敞开了指针压缩。jdk8 版本是默认开启指针压缩的,能够通过配置 vm 参数敞开指针压缩。对于更多压缩指针拜访 JAVA 文档: 官网

敞开指针压缩        -XX:-UseCompressedOops 

开启指针压缩之后,再看对象的内存布局:

 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)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     1   boolean L.myboolean                               true
     13     3           (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total
  • OFFSET:偏移地址,单位字节;
  • SIZE:占用的内存大小,单位为字节;
  • TYPE DESCRIPTION:类型形容,其中 object header 为对象头;
  • VALUE:对应内存中以后存储的值;

开启指针压缩能够缩小对象的内存应用。因而,开启指针压缩,实践上来讲,大概能节俭百分之五十的内存。jdk8及当前版本曾经默认开启指针压缩,无需配置。

一般的对象获取到的对象头构造为:

|--------------------------------------------------------------|
|                     Object Header (128 bits)                 |
|------------------------------------|-------------------------|
|        Mark Word (64 bits)         | Klass pointer (64 bits) |
|------------------------------------|-------------------------|

一般对象 压缩后 获取构造:

|--------------------------------------------------------------|
|                     Object Header (96 bits)                  |
|------------------------------------|-------------------------|
|        Mark Word (64 bits)         | Klass pointer (32 bits) |
|------------------------------------|-------------------------|

数组对象获取到的对象头构造为:

|---------------------------------------------------------------------------------|
|                                 Object Header (128 bits)                        |
|--------------------------------|-----------------------|------------------------|
|        Mark Word(64bits)       | Klass pointer(32bits) |  array length(32bits)  |
|--------------------------------|-----------------------|------------------------|
 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)                           6d 01 00 f8 (01101101 00000001 00000000 11111000) (-134217363)
     12     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
     16    20    int [I.<elements>                             N/A
     36     4        (loss due to the next object alignment)
Instance size: 40 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

3. 对象头的组成

咱们先理解一下,一个 JAVA 对象的存储构造。在 Hotspot 虚拟机中,对象在内存中的存储布局分为 3 块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
在咱们刚刚打印的后果中能够这样归类:

 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)    //markword             01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4           (object header)    //markword             00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)   //klass pointer 类元数据 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     1   boolean L.myboolean                               true    // Instance Data 对象理论的数据
     13     3           (loss due to the next object alignment)            //Padding 对齐填充数据

1.Mark Word

这部分次要用来存储对象本身的运行时数据,如 hashcode、gc 分代年龄等。mark word的位长度为 JVM 的一个 Word 大小,也就是说 32 位 JVM 的 Mark word 为 32 位,64 位 JVM 为 64 位。
为了让一个字大小存储更多的信息,JVM 将字的最低两个位设置为标记位,不同标记位下的 Mark Word 示意如下:

其中各局部的含意如下:
lock:2 位的锁状态标记位,因为心愿用尽可能少的二进制位示意尽可能多的信息,所以设置了 lock 标记。该标记的值不同,整个 mark word 示意的含意不同。
通过倒数三位数 咱们能够判断出锁的类型

enum {  locked_value                 = 0, // 0 00 轻量级锁
         unlocked_value           = 1,// 0 01 无锁
         monitor_value            = 2,// 0 10 重量级锁
         marked_value             = 3,// 0 11 gc 标记
         biased_lock_pattern      = 5 // 1 01 偏差锁
  };
通过内存信息剖析锁状态

写一个 synchronized 加锁的 demo 剖析锁状态
接着,咱们再看一下,应用 synchronized 加锁状况下对象的内存信息,通过对象头剖析锁状态。

代码:

public class Main{public static void main(String[] args) throws InterruptedException {L l = new L();
        Runnable RUNNABLE = () -> {while (!Thread.interrupted()) {synchronized (l) {
                    String SPLITE_STR = "===========================================";
                    System.out.println(SPLITE_STR);
                    System.out.println(ClassLayout.parseInstance(l).toPrintable());
                    System.out.println(SPLITE_STR);
                }
                try {Thread.sleep(1000);
                } catch (InterruptedException e) {e.printStackTrace();
                }
            }
        };
        for (int i = 0; i < 3; i++) {new Thread(RUNNABLE).start();}
    }
}

class L{private boolean myboolean = true;}

输入:

===========================================
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           5a 97 02 c1 (01011010 10010111 00000010 11000001) (-1056794790)
      4     4           (object header)                           d7 7f 00 00 (11010111 01111111 00000000 00000000) (32727)
      8     4           (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     1   boolean L.myboolean                               true
     13     3           (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total

===========================================

Mark Word为 0X00007FD7C102975A 对应的 2 进制为: 0xb00000000 00000000 01111111 11010111 11000001 00000010 10010111 01011010
咱们能够看到在第一行 object header 中 value=5a 对应的 2 进制为 01011010 倒数第三位 为 0 示意不是偏量锁, 后两位为 10 示意为 分量锁

enum {  locked_value                 = 0, // 0 00 轻量级锁
         unlocked_value           = 1,// 0 01 无锁
         monitor_value            = 2,// 0 10 重量级锁
         marked_value             = 3,// 0 11 gc 标记
         biased_lock_pattern      = 5 // 1 01 偏差锁
  };

例子 2:

public class Main{public static void main(String[] args) throws InterruptedException {L l = new L();
        synchronized (l) {Thread.sleep(1000);
            System.out.println(ClassLayout.parseInstance(l).toPrintable());
            Thread.sleep(1000);
        }     // 轻量锁
    }
}

class L{private boolean myboolean = true;}

输入:

 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           f0 18 58 00 (11110000 00011000 01011000 00000000) (5773552)
      4     4           (object header)                           00 70 00 00 (00000000 01110000 00000000 00000000) (28672)
      8     4           (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     1   boolean L.myboolean                               true
     13     3           (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total

对应的 mark word 为 0x00007000005818f0 对应的 2 进制为 0xb00000000 00000000 01110000 00000000 00000000 01011000 00011000 11110000
依据开端倒数第三位为 0 示意不是偏量锁 倒数后 2 位为 00 示意这是一个轻量锁

enum {  locked_value                 = 0, // 0 00 轻量级锁
         unlocked_value           = 1,// 0 01 无锁
         monitor_value            = 2,// 0 10 重量级锁
         marked_value             = 3,// 0 11 gc 标记
         biased_lock_pattern      = 5 // 1 01 偏差锁
  };

你可能会有疑难 mark word = 0x00007000005818f0 是怎么算进去的, 依据前 64 位的 value 倒序排列拼成的串就是 mark word
例子:

 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           f0 18 58 00 (11110000 00011000 01011000 00000000) (5773552)
      4     4           (object header)                           00 70 00 00 (00000000 01110000 00000000 00000000) (28672)
      8     4           (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     1   boolean L.myboolean                               true
     13     3           (loss due to the next object alignment)

Mark word 串为 前 64 位倒序排列为:00000000 00000000 01110000 00000000 00000000 01011000 00011000 11110000
转换为 16 进制为 00007000005818f0

2.Klass Pointer

即对象指向它的元数据的指针,虚拟机通过这个指针来确定是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针(通过句柄池拜访)。

简略引申一下对象的拜访形式,咱们创建对象的目标就是为了应用它。所以咱们的 Java 程序在运行时会通过虚拟机栈中本地变量表的 reference 数据来操作堆上对象。然而 reference 只是 JVM 中标准的一个指向对象的援用,那这个援用如何去定位到具体的对象呢?因而,不同的虚拟机能够实现不同的定位形式。次要有两种:句柄池和间接指针。

2.1 应用句柄拜访

会在堆中开拓一块内存作为句柄池,句柄中贮存了对象实例数据(属性值构造体)的内存地址,拜访类型数据的内存地址(类信息,办法类型信息),对象实例数据个别也在 heap 中开拓,类型数据个别贮存在办法区中。

长处 :reference 存储的是稳固的句柄地址,在对象被挪动(垃圾收集时挪动对象是十分广泛的行为)时只会扭转句柄中的实例数据指针,而 reference 自身不须要扭转。
毛病:减少了一次指针定位的工夫开销。

2.2 应用指针拜访

指针拜访形式指 reference 中间接贮存对象在 heap 中的内存地址,但对应的类型数据拜访地址须要在实例中存储。

长处 :节俭了一次指针定位的开销。
毛病:在对象被挪动时(如进行 GC 后的内存重新排列),reference 自身须要被批改。

总结:

通过句柄池拜访的话,对象的类型指针是不须要存在于对象头中的,然而目前大部分的虚拟机实现都是采纳间接指针形式拜访。此外如果对象为 JAVA 数组的话,那么在对象头中还会存在一部分数据来标识数组长度,否则 JVM 能够查看一般对象的元数据信息就能够晓得其大小,看数组对象却不行

3. 对齐填充字节

因为 JVM 要求 java 的对象占的内存大小应该是 8bit 的倍数,所以前面有几个字节用于把对象的大小补齐至 8bit 的倍数,就不特地介绍了

4.JVM 降级锁的过程

1,当没有被当成锁时,这就是一个一般的对象,Mark Word 记录对象的 HashCode,锁标记位是 01,是否偏差锁那一位是 0。

2,当对象被当做同步锁并有一个线程 A 抢到了锁时,锁标记位还是 01,但是否偏差锁那一位改成 1,前 23bit 记录抢到锁的线程 id,示意进入偏差锁状态。

3,当线程 A 再次试图来取得锁时,JVM 发现同步锁对象的标记位是 01,是否偏差锁是 1,也就是偏差状态,Mark Word 中记录的线程 id 就是线程 A 本人的 id,示意线程 A 曾经取得了这个偏差锁,能够执行同步锁的代码。

4,当线程 B 试图取得这个锁时,JVM 发现同步锁处于偏差状态,然而 Mark Word 中的线程 id 记录的不是 B,那么线程 B 会先用 CAS 操作试图取得锁,这里的取得锁操作是有可能胜利的,因为线程 A 个别不会主动开释偏差锁。如果抢锁胜利,就把 Mark Word 里的线程 id 改为线程 B 的 id,代表线程 B 取得了这个偏差锁,能够执行同步锁代码。如果抢锁失败,则继续执行步骤 5。

5,偏差锁状态抢锁失败,代表以后锁有肯定的竞争,偏差锁将降级为轻量级锁。JVM 会在以后线程的线程栈中开拓一块独自的空间,外面保留指向对象锁 Mark Word 的指针,同时在对象锁 Mark Word 中保留指向这片空间的指针。上述两个保留操作都是 CAS 操作,如果保留胜利,代表线程抢到了同步锁,就把 Mark Word 中的锁标记位改成 00,能够执行同步锁代码。如果保留失败,示意抢锁失败,竞争太强烈,继续执行步骤 6。

6,轻量级锁抢锁失败,JVM 会应用自旋锁,自旋锁不是一个锁状态,只是代表一直的重试,尝试抢锁。从 JDK1.7 开始,自旋锁默认启用,自旋次数由 JVM 决定。如果抢锁胜利则执行同步锁代码,如果失败则继续执行步骤 7。

7,自旋锁重试之后如果抢锁仍然失败,同步锁会降级至重量级锁,锁标记位改为 10。在这个状态下,未抢到锁的线程都会被阻塞。

总结: 本章节次要介绍了对象布局蕴含对象头, 对象实例数据, 和对齐数据. 并且介绍了对象头中蕴含的信息和解析办法
更多内容请继续关注公众号:java 宝典

关注公众号:java 宝典

正文完
 0