转载

“对象头(object header)”里知多少

“对象头(object header)”里知多少

前言

在家办公的第N周.

不知道公司还在不在了....

言归正传,回到正文“对象头”

对于学习Java来说, 对象头可以是入门的知识点之一.

假设有一扇门通向深入Java语言,那么对象头就是“进门须知”的这么一个地位,没什么技术要点,但是需要知道。

synchronized 的锁标志存哪了?”,“ 对象多大岁数呀 对象的分代年龄在哪看”等等,刚学Java时免不了这些疑问,这些就和对象头息息相关。

如果对“对象头”不清晰的读者,可以继续往下看了,本文将展示于你“对象”的 里大致都装了什么.

“对象头(object header)”里知多少

正文

简介

先抛出基本概念,后面笔者有具体的实践.

对象头(Object header) 是直译过来的,未免有些生硬,依笔者看叫“ 对象名片 ”还能上口一些,主要包含了对象的基本信息,比如:

  • 布局

  • GC状态

  • 类型

  • 同步状态

  • (identity) hash code

  • 数组长度 ( 前提你得是数组 )

identity hash code是指 不经重写 过由jvm计算的hashcode.

整个对象头由两个部分组成,即: klass pointerMark Word .

当然了,介绍这两个东西之前,此处需要强调一下:本文默认是基于 jdk1.8并且64位环境 进行描述的,算是一个标配。

klass pointer

klass pointer一般占32个bit即4个字节,如果你有足够的原因关闭默认的 指针压缩 ,即启动参数加上了 -XX:-UseCompressedOops 那么它就占64个bit.

不过此处还有一个细节:根据计算,堆大小超过32GB后,就算不关指针压缩并不会报错,只是指针压缩会失效。

但是这个笔者到是没有进行实际测试,原因嘛....

堆大小要32GB+内存, 还要留给非堆和OS一些内存,这种内存的机器是笔者三线小厂得不到的... “对象头(object header)”里知多少

klass pointer的存储内容是一个指针,指向了其类元数据的信息,jvm使用该指针来确定此对象是类的哪个实例.

什么意思?如果你有一个Person实例的引用,那么找到元数据就靠它了,如图:

“对象头(object header)”里知多少

Mark Word

关于mark word对于java程序员是比较重要的一块知识点,开局一张图:

“对象头(object header)”里知多少

表格中的“场景”,你也可以理解为“状态”,一个对象在一个时间点是处于一种状态的,但是状态之间 可能会切换 .

也就是你使用的对象,就处于当前表格中,其中“一行”的状态.

Mark Word在64位虚拟机下,也就是占用64位大小即8个字节的空间.

内具体容包括:

  • unused:未使用的

  • hashcode:上文提到的 identity hash code,本文出现的hashcode都是指identity hash code

  • thread: 偏向锁记录的线程标识

  • epoch: 验证偏向锁有效性的时间戳

  • age:分代年龄

  • biased_lock 偏向锁标志

  • lock 锁标志

  • pointer_to_lock_record 轻量锁lock record指针

  • pointer_to_heavyweight_monitor 重量锁monitor指针

如果你看cms_free这个字体有点奇怪那就对了,开始误画成了 unused ,后来反应过来默认开启“ 指针压缩 ”的情况,那么那一个bit应该是 cms_free .

cms_free 从名字就能看出和cms收集器有关系,因为cms算法是 标记-清理 的一款收集器,所以内存碎片问题是将不可达对象维护在一个列表 free list 中,笔者推测此处应该是标记对象是否在 free list 中.

关于 cms_free 的结论是笔者推测的,你大可不必相信,如果认为笔者说的不正确可以告诉我.

如果你觉得笔者说的不对,但是你又拿不出证据,倒也不用十分较真儿,毕竟jdk14 cms已经被移除了. “对象头(object header)”里知多少

初学者对这些东西感到头大,可以先从无锁状态的那一行下的内容开始了解.

如果看完这些东西,回味一下笔者上面说的“mark word对于java程序员是比较重要的一块知识点”,相信你也知道原因了. 这部分和程序关系很大,比如:

为什么晋升到老年代的年龄设置( XX:MaxTenuringThreshold )不能超过 15

因为就给了age 四个bit 空间,最大就是 1111 (二进制)也就是 15 ,多了没地方存.

为什么你的 synchronized 锁住的对象,没有“传说中的”偏向锁优化?

因为hashcode并不是对象实例化完就计算好的,是调用计算出来放在mark word里的。

你调用过hashcode方法(或者隐式调用:存到hashset里,map的key,调用了默认未经重写的toString()方法等等),把“坑位”占了,偏向锁想存的线程id没地方存了,自然就直接是 轻量级锁 了.

(或者你只是单纯的测试的时候忘了加 -XX:BiasedLockingStartupDelay=0 了)

看起来设计的有点不合理旦又透着合理,底层的设计就是这么朴实无华,且枯燥。

本文重点是介绍“对象头”,所以不会重点介绍锁,就如刚才说的“调用过hashcode再同步发现是轻量锁”,其实还有很多种情况,

比如: 在 synchronized 块内调用的hashcode计算方法,就算有了偏向锁也会被撤销,膨胀为重量级锁。如果有缘,你可能在未来能看到笔者单独描述锁的文章. “对象头(object header)”里知多少

实践

实践出真知,工欲善其事必先利其器,工具使用 jol 工具即可,openjdk提供的分析对象大小,布局等信息的工具.

本文使用最简单的maven引入工程的方式进行测试. 你也可以选择命令行的方式,两种使用方式都在上方jol的链接中有介绍.

引入jol之后,我们开始打印对象布局试试.

数组对象布局

    public static void main(String[] args) {
// 声明一枚长度为3306的数组
int[] intArr = new int[3306];
// 使用jol的ClassLayout工具分析对象布局
System.out.println(ClassLayout.parseInstance(intArr).toPrintable());
}

print:
------------------------------------------------------
[I 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) 6d 01 00 f8 (01101101 00000001 00000000 11111000) (-134217363)
12 4 (object header) ea 0c 00 00 (11101010 00001100 00000000 00000000) (3306)
16 13224 int [I.<elements> N/A
Instance size: 13240 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
复制代码

如果你是第一次看jol打印的布局图,可以直接看笔者标注好的下图: “对象头(object header)”里知多少

对象头的三个部分,分别印证了上文提到的klass pointer和Mark Word,以及数组独有的长度属性.

右侧分别用了三种进制展示了“每一行”的value, 呼应上文:

没有调用过原始的hashcode方法(包括System.identityHashCode方法), 那么hashcode位置都是0.

由图可得出结论,对象在刚实例化好的时候,非常“ 干净 ”,乍眼看去两排 0 ,但是只有一个 1 显得十分突兀.

其实它是 001 ,为上文提到的 偏向锁标志+锁标志 ,所有锁的状态如下:

偏向锁标志 锁标志 状态
0 01 无锁
1 01 偏向锁
- 00 轻量级锁
- 10 重量级锁
- 11 GC标记

这回就清晰了,虽然有一个 1 ,但其实是无锁的状态.

回到布局图,因为是数组对象,所以在第四行保存了 数组长度 ,非数组对象自然是没有的.

对象大小的计算,也是非常精准的, 即: 13224 (一个int为四个字节乘3306) + 16 = 13240个字节.

自己实践的提示

看到对象布局设计的如此朴实无华,你也可以动手实践一下.

笔者也附上几个小提醒:

  • 上文提到的,hashcode对偏向锁和重量锁的影响

  • 上文提到的 -XX:BiasedLockingStartupDelay 参数对偏向锁测试的影响
  • 注意测试机器的 大小端 对结果顺序的影响,一般大家的机器都是小端,所以value的 打印顺序 是和上文Markword开局那张图描述的字段顺序相反.

不注意这几点,你可能打印完一脸茫然,不知道哪些bit对应哪些字段,甚至和自己的预想结果不一样.

最后

对于对象的结构这一部分其实有很多细节值得你去拓展阅读, 本文只是简单的介绍了“Object header”这一小部分.

看完本文,有没有和笔者一样,想说一句:

底层的设计就是这么朴实无华,且枯燥.

开个玩笑, 事实上多看看, 你会发现非常有乐趣:blush:.

拓展推荐

  • heapdump-is-a-lie 一篇对heapdump工具展示的对象大小质疑文章,并且提到jol工具的准确性.

  • markOop.hpp jdk8关于markword的源码部分

  • markWord.hpp jdk14关于markword的源码部分,观察一下去cms后发生了哪些变化

  • ObjectHeader.txt 对象头在三种情况的布局(64位, 64位压缩指针, 32位)

  • HotSpotGlossary.html hotspot的术语表,本文只涉及了对象头的几个单词.

“对象头(object header)”里知多少
原文  https://juejin.im/post/5e830f546fb9a03c341d9346
正文到此结束
Loading...