探索Java Object Header内存布局

深入学习Java基础知识
2021-05-17 14:29 · 阅读时长12分钟
小课

Java对象的内存占用主要分为三部分,对象头、成员变量以及内存对齐填充。本文主要探讨一下对象头中的字段,其内容主要包括两个部分,一是Mark Word,主要用于标记对象的状态,比如锁的状态信息,是轻量级锁、偏向锁还是重量级锁,还有hashCode值以及GC标识等等,二是Klass Pointer,用于记录当前实例所属的Class实例的指针。

包括继承自父类的成员变量

注释

在不同的环境下,对象头部的内存占用大小是不同的,在64位环境,开启压缩指针时的情况下,对象头共占12个字节,其内存布局如下。

探索Java Object Header内存布局

如果需要查看在其他环境下,对象头的内存布局以及大小占用请点击查看更多按钮。

上图右边表示当前对象的状态,State对应左边表示的是当前状态下,该对象头部字段存储的数据,也就是说在不同状态下,对象头中保存的数据是不完全相同的,但是Klass Pointer是不会变的,所以下面主要分析Mark Word部分,首先引入jol-core,它的全称是Java Object Layout Core,一个用来分析JVM中对象内存布局的工具。

implementation 'org.openjdk.jol:jol-core:0.16'

下面使用它来分析实例在不同状态下其对象头内存布局。

正常Normal状态

正常状态下,Mark Word保存了identity_hashcode(对象的hashCode),age(分代年龄),biased_lock(偏向锁标识位)=0,lock(锁状态标识位)=01,cms_free(在64位开启指针压缩环境中,CMS回收算法用到),下面通过JOL工具,在线运行查看该状态下,对象头部的内存布局。

1import org.openjdk.jol.info.ClassLayout;
2
3public class Main {
4
5    public static void main(String[] args) {
6        Object object = new Object();
7        //需要调用hashCode之后,才会将值存在对象头里
8        object.hashCode();
9        System.out.println(
10                ClassLayout.parseInstance(object).toPrintable()
11        );
12    }
13}
注意: 这个Java运行环境不支持自定义包名,并且public class name必须是Main

由于每个人环境不同,得到的hashCode可能不一样,运行完成后会得到类似以下的输出

java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000006504e3b201 (hash: 0x6504e3b2; age: 0)
  8   4        (object header: class)    0x00001000
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

从上面可以看出,hashCode=0x6504e3b2,age=0,将Mark Word字段0x0000006504e3b201转化为二进制得到完整的bit位。

0000000000000000000000000110010100000100111000111011001000000001

其中后8位分别是cms_free(1bit)=0,age(4bit)=0000,biased_lock(1bit)=0,lock(2bit)=01,需要注意的是只有在对象的hashCode被调用过了,它的值才会被存在对象头中。

偏向锁Biased状态

在偏向锁状态下,Mark Word保存了thread(偏向锁的线程地址),epoch(偏向锁优化机制用到),age(分代年龄),biased_lock(偏向锁标识位)=1,lock(锁状态标识位)=01,cms_free,由于在Java 15时,偏向锁已经被弃用,本站Java在线运行环境是Java 16,所以无法在线运行查看效果,可复制以下代码到本地Java 15以下环境运行。

1import org.openjdk.jol.info.ClassLayout;
2
3public class Main {
4    final Object object = new Object();
5
6    public static void main(String[] args) {
7        new Main().test();
8    }
9
10    private void test() {
11        synchronized (object) {
12            System.out.println(
13                    ClassLayout.parseInstance(object).toPrintable()
14            );
15        }
16    }
17}

在本地运行完成后,由于环境不同,每个人运行的结果会有一定差异,会得到类似以下输出

java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x00007fe34a80b005 (biased: 0x0000001ff8d2a02c; epoch: 0; age: 0)
  8   4        (object header: class)    0x00001000
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

从上面可以看出,thread=0x0000001ff8d2a02c,epoch=0,age=0,将Mark Word字段0x00007fe34a80b005转化为二进制得到完整的bit位。

0000000000000000011111111110001101001010100000001011000000000101

其中后8位分别是cms_free(1bit)=0,age(4bit)=0000,biased_lock(1bit)=1,lock(2bit)=01。

轻量级锁Lightweight Locked状态

在获取轻量级锁时,会在当前线程的虚拟机栈创建一个Lock Record的空间,用于保存当前对象Mark Word的数据,然后在Mark Word中保存一个指向Lock Record地址的指针(ptr_to_lock_record),同时修改lock(锁状态标识位)=00。下面通过JOL工具,在线运行查看该状态下,对象头部的内存布局。

1import org.openjdk.jol.info.ClassLayout;
2
3public class Main {
4    final Object object = new Object();
5
6    public static void main(String[] args) {
7        new Main().test();
8    }
9
10    private void test() {
11        synchronized (object) {
12            System.out.println(
13                    ClassLayout.parseInstance(object).toPrintable()
14            );
15        }
16    }
17}
注意: 这个Java运行环境不支持自定义包名,并且public class name必须是Main

运行完成后,由于环境不同,每个人运行的结果会有一定差异,会得到类似以下输出

java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x00007f5377045920 (thin lock: 0x00007f5377045920)
  8   4        (object header: class)    0x00000682
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

将Mark Word字段0x00007f5377045920转化为二进制得到完整的bit位。

0000000000000000011111110101001101110111000001000101100100100000

其中后2位是lock(2bit)=00。

重量级锁Heavyweight Locked状态

在升级为重量级锁时,会为当前锁对象创建一个监视器ObjectMonitor,用于保存当前锁对象的原始头信息、重入次数、竞争失败队列、竞争队列和阻塞队列等信息,然后在Mark Word中保存一个指向ObjectMonitor地址的指针(ptr_to_heavyweight_monitor),同时修改lock(锁状态标识位)=10。下面通过JOL工具,在线运行查看该状态下,对象头部的内存布局。

1import org.openjdk.jol.info.ClassLayout;
2
3public class Main {
4    final Object object = new Object();
5
6    public static void main(String[] args) {
7        new Main().test();
8    }
9
10    private void test() {
11        new Thread(() -> {
12            synchronized (object) {
13                try {
14                    Thread.sleep(1000);
15                } catch (InterruptedException e) {
16                    e.printStackTrace();
17                }
18            }
19        }).start();
20        synchronized (object) {
21            System.out.println(
22                    ClassLayout.parseInstance(object).toPrintable()
23            );
24        }
25    }
26}
注意: 这个Java运行环境不支持自定义包名,并且public class name必须是Main

运行完成后,由于环境不同,每个人运行的结果会有一定差异,会得到类似以下输出

java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x00007fcfe0c13e02 (fat lock: 0x00007fcfe0c13e02)
  8   4        (object header: class)    0x00001000
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

将Mark Word字段0x00007fcfe0c13e02转化为二进制得到完整的bit位。

0000000000000000011111111100111111100000110000010011111000000010

其中后2位是lock(2bit)=10。

GC标记Marked for GC状态

lock(锁状态标识位)=11,早期的垃圾收集器会使用此字段作为垃圾收集的标志,但是随着发展,有一些现代化的垃圾收集器已经不再使用这个字段了。

对象头object headerlock锁状态轻量级锁偏向锁重量级锁