7

Java对象头的内存布局探究

 3 years ago
source link: http://bboyjing.github.io/2020/04/24/Java%E5%AF%B9%E8%B1%A1%E5%A4%B4%E7%9A%84%E5%86%85%E5%AD%98%E5%B8%83%E5%B1%80%E6%8E%A2%E7%A9%B6/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Java对象头的内存布局探究

2020-04-24

| JVM

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

~❯ 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可见,默认是开启压缩指针的。下面先引入一个依赖,用于查看对象内存布局:

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

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

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类型成员变量

未开启压缩指针情况

* 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

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

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

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

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

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK