【巨人的肩膀】JAVA对象布局之对象头(Object Header)

由于 Java 面向对象的思想,在 JVM 中需要大量存储对象,存储时为了实现一些额外的功能,需要在对象中添加一些标记字段用于增强对象功能 。在学习并发编程知识 synchronized 时,我们总是难以理解其实现原理,因为偏向锁、轻量级锁、重量级锁都涉及到对象头,所以了解 java 对象头是我们深入了解 synchronized 的前提条件,以下我们使用 64 位 JDK 示例

对象布局的总体结构

在这里插入图片描述

获取一个对象布局实例

  1. 首先在 maven 项目中引入查看对象布局的神器
        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.14</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 参数关闭指针压缩:官网

关闭指针压缩		-XX:-UseCompressedOops 
打开指针压缩		-XX:+UseCompressedOops 
开启指针压缩可以减少对象的内存使用。因此,开启指针压缩,理论上来讲,大约能节省百分之五十的内存。jdk8 及以后版本已经默认开启指针压缩,无需配置

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

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:对应内存中当前存储的值
普通的对象获取到的对象头结构为:

|--------------------------------------------------------------|
|                     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

对象头的组成

一个 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      // Instance Data 对象实际的数据 true	
13      3         (loss due to the next object alignment)			//Padding 对齐填充数据
Mark Word

这部分主要用来存储对象自身的运行时数据,如 hashcode、gc 分代年龄等。mark word 的位长度为 JVM 的一个 Word 大小,也就是说 32 位 JVM 的 Mark word 为 32 位,64 位 JVM 为 64 位。为了让一个字大小存储更多的信息,JVM 将字节的最低两个位设置为标记位
Mark word 串为前 64 位倒序排列:00000000 00000000 00000000 00000000 00000000 00000001 转换为 16 进制为 0000000000000010
在这里插入图片描述

  • 通过内存信息分析锁状态
    写一个 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 表示为重量锁

Class Pointer

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

简单引申一下对象的访问方式,我们创建对象的目的就是为了使用它。所以我们的Java程序在运行时会通过虚拟机栈中本地变量表的 reference 数据来操作堆上对象。但是 reference 只是 JVM 中规范的一个指向对象的引用,那这个引用如何去定位到具体的对象呢?因此,不同的虚拟机可以实现不同的定位方式。主要有两种:句柄池和直接指针

  • 使用句柄访问
    会在堆中开辟一块内存作为句柄池,句柄中储存了对象实例数据(属性值结构体)的内存地址,访问类型数据的内存地址(类信息,方法类型信息),对象实例数据一般也在heap中开辟,类型数据一般储存在方法区中
    • 优点:reference存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要改变
    • 缺点:增加了一次指针定位的时间开销
      在这里插入图片描述
  • 使用指针访问
    指针访问方式指reference中直接储存对象在heap中的内存地址,但对应的类型数据访问地址需要在实例中存储
    • 优点:节省了一次指针定位的开销
    • 缺点:在对象被移动时(如进行GC后的内存重新排列),reference本身需要被修改
      在这里插入图片描述
  • 总结
    通过句柄池访问的话,对象的类型指针是不需要存在于对象头中的,但是目前大部分的虚拟机实现都是采用直接指针方式访问。此外如果对象为JAVA数组的话,那么在对象头中还会存在一部分数据来标识数组长度,否则JVM可以查看普通对象的元数据信息就可以知道其大小,看数组对象却不行
对齐填充字节

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

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。在这个状态下,未抢到锁的线程都会被阻塞

jol-core的使用

package com.just.demo.demo.layout;

import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.info.GraphLayout;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @author lbh
 * @date 2021/2/1 9:38
 * @describe
 * https://blog.csdn.net/srs1995/article/details/109351177
 * https://zhuanlan.zhihu.com/p/151856103
 */
public class ClassLayOut {

    public static void main(String[] args){

        Map<String, Object> map = new HashMap<>();
        map.put("a", 1);
        map.put("b", "b");
        map.put("c", new Date());

        for (int i = 0; i < 10; i++) {
            map.put(String.valueOf(i), String.valueOf(i));
        }
        System.out.println("==========================================查看对象内部信息==================================================");
        System.out.println(ClassLayout.parseInstance(map).toPrintable());
        System.out.println("==========================================查看对象外部信息:包括引用的对象==================================================");
        System.out.println(GraphLayout.parseInstance(map).toPrintable());
        System.out.println("==========================================查看对象占用空间总大小==================================================");
        System.out.println(GraphLayout.parseInstance(map).totalSize());
    }
}

结果

==========================================查看对象内部信息==================================================
java.util.HashMap 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)          a8 37 00 f8 (10101000 00110111 00000000 11111000) (-134203480)
12      4         java.util.Set AbstractMap.keySet                        null
16      4         java.util.Collection AbstractMap.values                 null
20      4         int HashMap.size         13
24      4         int HashMap.modCount     13
28      4         int HashMap.threshold    24
32      4         float HashMap.loadFactor  0.75
36      4   java.util.HashMap.Node[] HashMap.table                             [null, (object), (object), (object), null, null, null, null, null, null, null, null, null, null, null, null, (object), (object), (object), (object), (object), (object), (object), (object), (object), (object), null, null, null, null, null, null]
40      4         java.util.Set HashMap.entrySet                          null
44      4         (loss due to the next object alignment)
Instance size: 48 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

==========================================查看对象外部信息:包括引用的对象==================================================
java.util.HashMap@7e0babb1d object externals:
          ADDRESS       SIZE TYPE                      PATH                           VALUE
        76b48a050         16 java.lang.Integer         .table[1].value                1
        76b48a060    5820328 (something else)          (somewhere else)               (something else)
        76ba17008         48 java.util.HashMap                                        (object)
        76ba17038         24 java.lang.String          .table[1].key                  (object)
        76ba17050         24 [C                        .table[1].key.value            [a]
        76ba17068         80 (something else)          (somewhere else)               (something else)
        76ba170b8         32 java.util.HashMap$Node    .table[1]                      (object)
        76ba170d8         24 java.lang.String          .table[2].key                  (object)
        76ba170f0         24 [C                        .table[2].key.value            [b]
        76ba17108         32 java.util.HashMap$Node    .table[2]                      (object)
        76ba17128         24 java.lang.String          .table[3].key                  (object)
        76ba17140         24 [C                        .table[3].key.value            [c]
        76ba17158      37616 (something else)          (somewhere else)               (something else)
        76ba20448         24 java.util.Date            .table[3].value                (object)
        76ba20460         32 java.util.HashMap$Node    .table[3]                      (object)
        76ba20480         24 [C                        .table[16].key.value           [0]
        76ba20498         24 java.lang.String          .table[16].key                 (object)
        76ba204b0         24 [C                        .table[16].value.value         [0]
        76ba204c8         24 java.lang.String          .table[16].value               (object)
        76ba204e0         32 java.util.HashMap$Node    .table[16]                     (object)
        76ba20500         24 [C                        .table[17].key.value           [1]
        76ba20518         24 java.lang.String          .table[17].key                 (object)
        76ba20530         24 [C                        .table[17].value.value         [1]
        76ba20548         24 java.lang.String          .table[17].value               (object)
        76ba20560         32 java.util.HashMap$Node    .table[17]                     (object)
        76ba20580         24 [C                        .table[18].key.value           [2]
        76ba20598         24 java.lang.String          .table[18].key                 (object)
        76ba205b0         24 [C                        .table[18].value.value         [2]
        76ba205c8         24 java.lang.String          .table[18].value               (object)
        76ba205e0         32 java.util.HashMap$Node    .table[18]                     (object)
        76ba20600         24 [C                        .table[19].key.value           [3]
        76ba20618         24 java.lang.String          .table[19].key                 (object)
        76ba20630         24 [C                        .table[19].value.value         [3]
        76ba20648         24 java.lang.String          .table[19].value               (object)
        76ba20660         32 java.util.HashMap$Node    .table[19]                     (object)
        76ba20680         24 [C                        .table[20].key.value           [4]
        76ba20698         24 java.lang.String          .table[20].key                 (object)
        76ba206b0         24 [C                        .table[20].value.value         [4]
        76ba206c8         24 java.lang.String          .table[20].value               (object)
        76ba206e0         32 java.util.HashMap$Node    .table[20]                     (object)
        76ba20700         24 [C                        .table[21].key.value           [5]
        76ba20718         24 java.lang.String          .table[21].key                 (object)
        76ba20730         24 [C                        .table[21].value.value         [5]
        76ba20748         24 java.lang.String          .table[21].value               (object)
        76ba20760         32 java.util.HashMap$Node    .table[21]                     (object)
        76ba20780         24 [C                        .table[22].key.value           [6]
        76ba20798         24 java.lang.String          .table[22].key                 (object)
        76ba207b0         24 [C                        .table[22].value.value         [6]
        76ba207c8         24 java.lang.String          .table[22].value               (object)
        76ba207e0         32 java.util.HashMap$Node    .table[22]                     (object)
        76ba20800         24 [C                        .table[23].key.value           [7]
        76ba20818         24 java.lang.String          .table[23].key                 (object)
        76ba20830         24 [C                        .table[23].value.value         [7]
        76ba20848         24 java.lang.String          .table[23].value               (object)
        76ba20860         32 java.util.HashMap$Node    .table[23]                     (object)
        76ba20880         24 [C                        .table[24].key.value           [8]
        76ba20898         24 java.lang.String          .table[24].key                 (object)
        76ba208b0         24 [C                        .table[24].value.value         [8]
        76ba208c8         24 java.lang.String          .table[24].value               (object)
        76ba208e0         32 java.util.HashMap$Node    .table[24]                     (object)
        76ba20900         24 [C                        .table[25].key.value           [9]
        76ba20918         24 java.lang.String          .table[25].key                 (object)
        76ba20930         24 [C                        .table[25].value.value         [9]
        76ba20948         24 java.lang.String          .table[25].value               (object)
        76ba20960         32 java.util.HashMap$Node    .table[25]                     (object)
        76ba20980        144 [Ljava.util.HashMap$Node; .table                         [null, (object), (object), (object), null, null, null, null, null, null, null, null, null, null, null, null, (object), (object), (object), (object), (object), (object), (object), (object), (object), (object), null, null, null, null, null, null]

Addresses are stable after 1 tries.


==========================================查看对象占用空间总大小==================================================
1752

Process finished with exit code 0

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值