bboyjing's blog

Java对象头的内存布局探究

  本章节主要来验证下是否开启压缩指针对对象头内存布局的影响,因为在其他地方看到的结论不一样,还是得眼见为实。先看下JVM版本以及默认启动参数:

1
2
3
4
5
~❯ java -XX:+PrintCommandLineFlags -version
-XX:G1ConcRefinementThreads=4 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC
java version "11.0.1" 2018-10-16 LTS
Java(TM) SE Runtime Environment 18.9 (build 11.0.1+13-LTS)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.1+13-LTS, mixed mode)

-XX:+UseCompressedClassPointers可见,默认是开启压缩指针的。下面先引入一个依赖,用于查看对象内存布局:

1
2
3
4
5
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>

开启压缩指针(默认)情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class User {
int i;
}
public class CompressedLayout {
public static void main(String[] args) {
User o = new User();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
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) 48 02 06 00 (01001000 00000010 00000110 00000000) (393800)
12 4 int User.i 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

  由结果可以看出,对象一共占用16个字节,头部占12个字节。组成结构如下:

  • Header:8字节Mark World + 4字节Class类型指针
  • 实例数据:4字节的int类型成员变量

未开启压缩指针情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* vm args: -XX:-UseCompressedClassPointers
*
*/
public class UnCompressedLayout {
public static void main(String[] args) {
User o = new User();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
cn.didadu.sample.jvm.objLayout.User 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) 28 99 20 26 (00101000 10011001 00100000 00100110) (639670568)
12 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
16 4 int User.i 0
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

  由结果可以看出,对象一共占用24个字节,头部占16字节。组成结构如下:

  • Header:8字节Mark World + 8字节Class类型指针
  • 实例数据:4字节的int类型成员变量
  • 对其填充:4字节,由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是任何对象的大小都必须是8字节的整数倍。所以对象内存布局中有了对其填充这一项。

由上面两个结果可以看出是否开启压缩指针,直接影响的是对象头部的大小,更具体的说是指向Class的类型指针那一部分。开启压缩时占4字节,未开启压缩时占8字节(64位的虚拟机)。

Mark World布局

  顺便来看下Header的前8个字节Mark World区域,这8个字节存储了对象哈希码、对象分代年龄、锁状态等。根据最后一个字节的标志位,Mark World一共有5种表现形式,下面以64位的形式来看下这5种的布局情况:

  • 正常对象,无锁,这是新建对象的Mark World

    1
    | 25位未使用 | 31位HashCode(调用对象的hashCode后写入) | 1位未使用 | 4位age(所以age最大值为15) | 1位偏向锁标记(此处为0,表示不是偏向锁) | 2位锁状态(此处为01,结合前面1位表示未锁定) |
  • 偏向锁对象的Mark World

    1
    | 54位线程ID | 2位epoch(偏向的时间戳) | 1位未使用 | 4位age(所以age最大值为15) | 1位偏向锁标记(此处为1,表示是偏向锁) | 2位锁状态(此处为01,结合前面1位表示可偏向) |
  • 轻量锁的Mark World

    1
    | 62位指针,指向线程栈帧中的锁记录 | 2位锁标识(此处为00,表示轻量级锁定) |
  • 重量级锁的Mark World

    1
    | 62位指针,指向关联的监视器对象 | 2位锁标识(此处为10,表示膨胀为重量级锁) |
  • 被GC标记过的Mark World

    1
    | 62位空 | 2位状态标识(此处为11,表示被GC标记) |

本章节简单地验证了下对象头的内存布局,就到这儿了。