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

2020/4/3 17:01:19

本文主要是介绍“对象头(object header)”里知多少,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

前言

在家办公的第N周.

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

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

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

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

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

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

正文

简介

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

对象头(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一些内存,这种内存的机器是笔者三线小厂得不到的...

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

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

Mark Word

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

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

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

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已经被移除了.

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

如果看完这些东西,回味一下笔者上面说的“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计算方法,就算有了偏向锁也会被撤销,膨胀为重量级锁。如果有缘,你可能在未来能看到笔者单独描述锁的文章.

实践

实践出真知,工欲善其事必先利其器,工具使用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
       (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
       (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
       (object header)                           6d 01 00 f8 (01101101 00000001 00000000 11111000) (-134217363)
       (object header)                           ea 0c 00 00 (11101010 00001100 00000000 00000000) (3306)
   int [I.<elements>                             N/A
Instance size: 13240 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
复制代码

如果你是第一次看jol打印的布局图,可以直接看笔者标注好的下图:

对象头的三个部分,分别印证了上文提到的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”这一小部分.

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

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

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

拓展推荐

  • 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)”里知多少的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程