Java 对象头

1. 对象内存布局

对象除了我们自定义的一些属性外,在HotSpot虚拟机中,对象在内存中还可以分为三个区域:对象头,实例数据,对齐填充,这三个区域组成起来才是一个完整的对象!

  • 对象头: 看文章下方解释
  • 实例数据: 存放类的属性数据信息,包括父类的属性信息
  • 由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐
image.png

2.对象头

对象头中存储了对象是很多java内部的信息,如hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间等,Java对象头一般占有2个机器码

PS:在32位虚拟机中,1个机器码等于4字节,也就是32bit,在64位虚拟机中,1个机器码是8个字节,也就是64bit!

如果对象是数组类型,则需要3个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块区域用来记录数组长度。 HotSpot虚拟机的对象头包括两部分信息,第一部分为Mark Word,第二部分为class pointer,如果是数组对象,那么还有数组长度!

3.对象头形式

JVM中对象头的方式有以下两种(以32位JVM为例):

普通对象

image.png

数组对象

image.png

4 对象头的组成

4.1Mark Word

这部分主要用来存储对象自身的运行时数据,如hashcode、gc分代年龄等。mark word 的位长度为JVM的一个Word大小,也就是说32位JVM的Mark word为32位,64位JVM为64位
为了让一个字大小存储更多的信息,JVM将字的最低两个位设置为标记位,不同标记位下的Mark Word示意如下:

image.png

image.png

其中各部分的含义如下:
lock: 2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个mark word表示的含义不同。

biased_lock : lock 状态
0 01 无锁
1 01 偏向锁
0 00 轻量级锁
0 10 重量级锁
0 11 GC标记

biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。

age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-
XX:MaxTenuringThreshold选项最大值为15的原因。

identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。

thread:持有偏向锁的线程ID。
epoch:偏向时间戳。
ptr_to_lock_record:指向栈中锁记录的指针。
ptr_to_heavyweight_monitor:指向管程Monitor的指针。

64位下的标记字与32位的相似,不再赘述:

image.png

4.2 class pointer

这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。
如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项+UseCompressedOops开启指针压缩,其中,oop即ordinary object pointer普通对象指针。开启该选项后,下列指针将压缩至32位:

每个Class的属性指针(即静态变量)
每个对象的属性指针(即对象变量)
普通对象数组的每个元素指针
当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM不会优化,比如指向PermGen的Class对象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。

4.3.array length

如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度,这部分数据的长度也随着JVM架构的不同而不同:32位的JVM上,长度为32位;64位JVM则为64位。64位JVM如果开启+UseCompressedOops选项,该区域长度也将由64位压缩至32位。

5.使用JOL来分析java的对象布局

5.1 JOL 简介

JOL的全称是Java Object Layout。是一个用来分析JVMObject布局的小工具。包括Object在内存中的占用情况,实例对象的引用情况等等。

JOL可以在代码中使用,也可以独立的以命令行中运行。命令行的我这里就不具体介绍了,今天主要讲解怎么在代码中使用JOL

使用JOL需要添加maven依赖:

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

查看分析vm信息

public class ObjectHeadTest {
    public static void main(String[] args) {
        //查看字节序
        System.out.println(ByteOrder.nativeOrder());
        //打印当前jvm信息
        System.out.println("======================================");
        System.out.println(VM.current().details());
    }
}

输出:

LITTLE_ENDIAN
======================================
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

上面的输出中,我们可以看到:Objects are 8 bytes aligned,这意味着所有的对象分配的字节都是8的整数倍。

可以从上面的LITTLE_ENDIAN发现,内存中字节序使用的是小端模式。

  • 大端字节序:高位字节在前,低位字节在后,这是人类读写数值的方法。
  • 小端字节序:低位字节在前,高位字节在后,即以0x1122形式储存。

计算机电路先处理低位字节,效率比较高,因为计算都是从低位开始的。所以,计算机的内部处理都是小端字节序。

人类还是习惯读写大端字节序。所以,除了计算机的内部处理,其他的场合几乎都是大端字节序,比如网络传输和文件储存。

查看分析基本类型对象布局

分析String类型(分析string类型)

System.out.println(ClassLayout.parseClass(String.class).toPrintable());

输出:

java.lang.String object internals:
 OFFSET  SIZE     TYPE DESCRIPTION                               VALUE
      0    12          (object header)                           N/A
     12     4   char[] String.value                              N/A
     16     4      int String.hash                               N/A
     20     4          (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

先解释下各个字段的含义

  • OFFSET是偏移量,也就是到这个字段位置所占用的byte数,
  • SIZE是后面类型的大小,
  • TYPE是Class中定义的类型,
  • DESCRIPTION是类型的描述,
  • VALUETYPE在内存中的值。

分析上面的输出,我们可以得出,String类中占用空间的有5部分,第一部分是对象头,占12个字节,第二部分是char数组,占用4个字节,第三部分是int表示的hash值,占4个字节 ,总共20个字节。但是JVM中对象内存的分配必须是8字节的整数倍,所以要补全4字节,最后String类的总大小是24字节。

分析Long类型

System.out.println(ClassLayout.parseClass(Long.class).toPrintable());

输出:

java.lang.Long object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0    12        (object header)                           N/A
     12     4        (alignment/padding gap)                  
     16     8   long Long.value                                N/A
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

可以看到1个Long对象是占24个字节的,但是其中真正存储longvalue只占8个字节。

分析Long实例对象

System.out.println(ClassLayout.parseInstance(Long.MAX_VALUE).toPrintable());

输出:

java.lang.Long 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)                           05 23 00 f8 (00000101 00100011 00000000 11111000) (-134208763)
     12     4        (alignment/padding gap)                  
     16     8   long Long.value                                9223372036854775807
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

可以看出,对象实例的布局跟类型差不多

参考链接
https://juejin.cn/post/6994703298909437983
https://www.jianshu.com/p/3d38cba67f8b

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容