Java对象的内存布局

概述

    我们知道关于Java对象的内存布局是由JVM所管理,所以我们就从Java虚拟机的官方文档出发,了解Java对象的内存布局。

对象内存布局

     对象在内存中存储的布局可以分为3块区域:对象头实例数据对齐填充

对象实例
对象头
实例数据
对齐填充

对象头

这是OpenJDK里hotspot的文档关于对象头的描述:
在这里插入图片描述
大致意思是: 是每个gc管理的堆对象开头的公共结构。(每个oop指针都指向一个对象头。)包括堆对象的布局、类信息、GC状态、同步状态和标识哈希码的基本信息。由两个组成。在数组中,它后面紧跟着一个长度字段。注意,Java对象和vm内部对象都有一个通用的对象头格式。

这里解释以下oop:
关于oop的描述:
在这里插入图片描述
大致意思: 是一个对象指针。特别是指向gc管理的堆的指针。(这个词很传统。一个“o”可以代表“普通”。实现为本机地址,而不是句柄。Oops可能被编译或解释的Java代码直接操作,因为GC知道Oops在这些代码中的活性和位置。Oops也可以由C/ c++代码的短周期直接操作,但是必须由这些代码在每个安全点的句柄中保存。
    上面对象头中说一个对象头由两个字组成。我们来看看这两个字:

第一个字mark word

在这里插入图片描述
大致意思: 每个对象标头的第一个字。通常是一组位域,包括同步状态和标识哈希码。也可以是同步相关信息的指针(具有特征的低比特编码)。在GC期间,可能包含GC状态位。

意思是用于存储对象自身的运行时数据:(如哈希码、GC分代年龄、锁状态标志、线程持有锁、偏向线程ID、偏向时间戳等)。称“Mark Word”,(考虑到存储成本,是一个非固定的数据结构)。

我们来看看mark word里的具体信息:

下面这段代码是Open JDK里关于mark word的源码
在这里插入图片描述
这里面的注释,说明了32位机和64位机的mark word信息。
我们来看64位下对mark word的描述。
unused 25 :未使用25位
hash:31 :哈希占了31位
age:4 :表示老年代,新生代的占了4位。(这里需要说明一下,对象在Survivor区每“熬过”一次Minor GC ,对象的年龄就会+1,默认值为15,超过这个值就会晋升到老年代中。因为4位可以表示的最大值为15)。
biased_lock:1: 表示偏向锁占1位
lock:2 : 表示锁占2位
对象有5种状态:无锁偏向锁重量级锁轻量级锁GC
HotSpot虚拟机对象头Mark Word

存储内容标志位状态
对象哈希码、对象分代年龄01未锁定
指向锁记录的指针00轻量级锁定
指向重量级锁的指针10膨胀(重量级锁定)
空,不需要记录信息11GC标记
偏向线程ID、偏向时间戳、对象分代年龄01可偏向
第二个字 klass pointer

在这里插入图片描述
大致意思: 每个对象标头的第二个字。指向描述原始对象的布局和行为的另一个对象(元对象)。对于Java对象,“klass”包含一个c++风格的“vtable”。

意思是:另一部分是类型指针。(即对象指向它的类元数据的指针,虚拟机通过这个指针确定这个对象是哪个类的实例)。

注意:对象头一共占128位,其中mark word占64位,klass pointer占64位

实例数据

    是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。这部分的存储顺序会受到虚拟机分配策略参数和字段在Java源码中定义顺序的影响。

对齐填充

下面结合对象信息来介绍。

这里我们可以通过编写一个类,把该类的对象信息输出验证以下:

那么问题来了,Java如何输出一个对象的信息呢?

OpenJDK里有一个jol(java object layout)的jar包。可以输出对象的信息。
一个普通类:

public class Simple { 
    private int state  = 1; //只有一个成员
}
//通过main方法
 private static Simple simple = new Simple();

    public static void main(String[] args) {
    	//这句话便可以将对象信息输出
        System.out.println(ClassLayout.parseInstance(simple).toPrintable());
    }

来我们看输出结果:

com.aiun.test.Simple 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)                           47 c1 00 f8 (01000111 11000001 00000000 11111000) (-134168249)      
  12     4    int Simple.state                              1
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

我们可以清楚的看到,对象头输出了三块,每一块4字节,一共12字节。

但是这里我们只看到了对象头和实例数据,并没有看到对齐填充,怎么回事?

来,我们在给类加一个属性。

public class Simple {
 private int state  = 1;
 private boolean flag = true;
}

//来,我们看输出结果
com.aiun.test.Simple 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)                           47 c1 00 f8 (01000111 11000001 00000000 11111000) (-134168249)
  12     4       int Simple.state                              1
  16     1   boolean Simple.flag                               true
  17     7           (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total

这次我们可以看出来,输出结果和上一次不一样了,多了 (loss due to the next object alignment) 意思是:由于下一次对象对齐而造成的损失
所以这另外的7个字节并不属于类信息,而是对齐填充造成的。
注意 对齐填充: 仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍

那么为什么是8字节的整数倍,这样有什么优点呢?
  1. 系统要求
  2. 可以提高GC回收效率
    对齐原理参考文档

接下来讨论一下mark word里面的信息:

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)                     

就这个输出信息来讨论:

*上面说对象头一个占128位,为什么这里打印出来占12字节96位呢?

这是因为Java虚拟机默认开启了指针压缩。由于在64位CPU下, 指针的宽度是64位的, 而实际的heap区域远远用不到这么大的内存, 使用64bit来存对象引用会造成浪费, 所以应该压缩来节省资源。可以通过-XX:-UseCompressedOops参数来关闭指针压缩。

根据上面源码注释里面说,前25位是未使用,应该全为0,为什么对象信息输出后,第8位就为1?有1不是说明有数据吗?

因为存储是分大端、小段存储的。小端存储是反着来存储的(也就是高地址低字节),大端存储是顺着来存(高地址高字节)。所以这里是反正存储的。

就算反着来看,那接下来31位为hash,怎么全是0呢?

按理说Java的hashCode()确实存在,为什么这里没有呢?我们输出一下hashCode看看

21685669
 0     4           (object header)                           01 a5 e5 4a (00000001 10100101 11100101 01001010) (1256563969)
 4     4           (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)

我们看到调用了hashCode()方法后,对象输出信息里就有hash值了,因为hash值是通过C++代码计算的。
我们将输出的结果转换为16进制:21685669 <—> 14AE5A5,可以看出输出的确实是hash码。

最后面的3个字节表示锁的状态,我们现在来加一下锁,看看
public static void main(String[] args) {
        Simple simple = new Simple();
        synchronized (simple) {
            System.out.println(ClassLayout.parseInstance(simple).toPrintable());
        }
    }

输出

0     4           (object header)                           70 f6 ad 02 (01110000 11110110 10101101 00000010) (44955248)
4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)

最后三位为1101表示偏向锁状态,10表示重量级锁(synchronized)。

总结:

我们可以看到出一个对象信息涉及到很多知识,对象状态、指针压缩、GC年龄、偏向延迟、批量撤销、锁膨胀可逆等相关知识。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值