实探java对象的内存布局

实探java对象的内存布局

在我深入学习synchronized的时候,我查了很多资料,发现synchronized锁住的是对象的对象头,然后我又了解什么是对象头,这块的资料就很多了。
总结一下就是:
在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充数据。
对象头:对象头主要结构是由Mark Word 和 Class Metadata Address
在这里插入图片描述
实例数据:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
之后再就是详细的介绍对象头占多大,每个字节代表啥意思等等。

说了一堆,我知道了synchronized的锁的原理,知道了对象头的作用,知道了对象在内存中的布局。总之就是理论的东西知道了不少。但是我还是想实际看看对象在内存中的布局到底是啥样的,然后我又找啊找,终于让我找到了。

首先引入这个依赖

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

第一次

我新建了一个空对象O

public class O {
}

然后将对象内存布局结构打印出来

    public static void main(String[] args) {
        O o = new O();
        //获取对象内存布局
        String s = ClassLayout.parseInstance(o).toPrintable();
        System.out.println(s);
    }

下面是打印出来的结果

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) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

发现了对象头是12个字节,总大小是16字节,空对象没有属性,方法,所以实例数据是0字节,有4字节的数据填充,验证了虚拟机要求对象起始地址必须是8字节的整数倍。

对象头12 byte
实例数据0 byte
填充数据4 byte
对象大小16 byte

第二次

新建一个对象F。

public class F {
    int f = 0;
}

对象O继承对象F,里面包含静态属性,非静态属性,数组,引用对象,方法。

public class O extends F{
    int i; // 非静态属性
    int[] arr = new int[5]; //数组
    Object object = new Object(); //相当于属性
    static int b = 5; //静态属性
    private void a(){
        int a = 0;
    }
}

打印出来的结果

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) 81 c1 00 f8 (10000001 11000001 00000000 11111000) (-134168191)
12 4 int F.f 0 //父类属性
16 4 int O.i 0 //非静态属性
20 4 int[] O.arr [0, 0, 0, 0, 0] //非静态属性数组(4bit,包含了数组的长度)
24 4 java.lang.String O.s (object) //引用类型属性(4bit)
28 4 java.lang.Object O.object (object) //引用类型属性(4bit)
Instance size: 32 bytes //总大小32byte(正好是8的整数倍,没有填充数据)
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

验证了 实例数据是存放类的非静态属性数据信息(引用类型都是4字节),包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。不保存静态属性和方法信息。

对象头12 byte
实例数据20 byte
填充数据0 byte
对象大小32 byte

从这两次就可以看出来

  • 对象头总是占用12个字节(这里也有的人说占用了16个字节,是因为Class Metadata Address默认开启了指针压缩,没有开启就是16个字节,涉及到硬件的设计了,这里就不说了,感兴趣的朋友自己搜索看看。)
  • 实例数据没有就不占用字节
  • 总字节是8的倍数,不够数据填充补足
  • 实例数据存放的非静态成员变量

这样我们的知识就不是只停留在理论阶段了,关于对象的内存布局自己可以亲自动手看看到底是啥样的!

实例数据和对象填充其实这里就没有什么好说的,通过上面的演示大家应该知道了是什么意思,那么这里最重要的就是对象头了。

对象头

对象头:对象头主要结构是由Mark Word 和 Class Metadata Address
这是我从其他的文章里面看到的,但是我现在想去验证一下,所以我去查看了一下 OpenjdK关于hotSpot的文档

在这里插入图片描述
这个相当于JVM规范中定义的对象头的概念,翻译一下
object header(对象头)

每个GC管理的堆对象开头的通用结构。 (每个oop都指向一个对象头。)包括有关堆对象的布局,类型,GC状态,同步状态和标识哈希码的基本信息。 由两个词组成。 在数组中,紧随其后的是长度字段。 请注意,Java对象和VM内部对象都具有通用的对象标头格式。

mark word

每个对象头的第一个部分。 通常,一组位域包括同步状态和标识哈希码。 也可以是指向与同步相关的信息的指针(具有特征性的低位编码)。 在GC期间,可能包含GC状态位。

klass pointer

每个对象头的第二个部分。 指向另一个对象(元对象),该对象描述原始对象的布局和行为。 对于Java对象,“容器”包含C ++样式“ vtable”。

放在谷歌翻译里面翻译的,原谅我英文比较差,就不润滑翻译了。
到这里我就稍微了解了下,对象头分为两部分,第一部分是mark word,存放的同步状态,哈希码,与同步相关的指针,GC状态。第二部分是klass pointer至于为什么其他文章写的是Class Metadata Address我也就不深究了,因为我也不知道,但是意思上还是都差不多的,类的指针和类的元数据地址,都是代表的是指向这个对象所在类的地址。

上面的东西说白了还是概念,只不过是官方的权威的概念,接下来我还是想看看对象头的具体实现。包括哪些数据存放哪些东西。所以我下载了OpenJDK的源码来找找代码。
至于怎么下载openjdk的源码,我从openjdk的官方下载禁止下载,然后到github上搜一下openjdk就好了。
在这里插入图片描述
在这里插入图片描述
在markOop.hpp(对应的就是mark word)文件中,分为32位JVM和64位JVM
从上图可以知道32bit的 占用32bit,

hash25bit
age(GC分代年龄)4bit
biased_lock(偏向锁)1bit
lock2bit

64bit的占用64bit,具体的还是看上图。
看到这里想到以前看过的一个面试题,每发生一次young GC,存活的对象分代年龄增加1,到多少的时候会进入老年代,答案是15,为什么是15,不能是16吗?如果以前肯定是懵逼的,我哪儿知道为什么是15,今天应该就知道了,从上面知道对象头中保存的分代年龄,不管32位还是64位的虚拟机都是4bit, 最大值是1111,即16,保存的范围是0~15,所以分代年龄是15。当然如果后面虚拟机变更成age 用5位表示,那对象的分代年龄最大就可以达到31次。

在这里插入图片描述
我们这里使用的是64的JVM,所以对象头的mark word是占用了64bit即8个字节,上面图中对象头总共是12个字节,那么剩下的4个字节就是保存klass pointer即指向类的信息的指针。
mark word :64bit
klass pointer : 32bit
这里解释一下,指针是需要用64位来表示的,但是由于64位太耗内存,所以采用了指针压缩的方法,来用32位表示64位的地址。其他的涉及到的东西太底层,不是咱们讨论的东西,感兴趣的朋友可以网上随便搜索下指针压缩,大量的文章让你借鉴参考。

mark word

接下来就是重点看看mark word中保存的东西,在不同状态下都是如何表示的。
对象在不同状态下mark word表示的含义是不同的。
对象总共有5种状态:
无状态(刚 new 出来),偏向锁,轻量级锁,重量级锁,gc(对象要被回收)
下图是对象不同状态下mark word表示的含义
在这里插入图片描述
这里对象的状态有5种,但是lock是占用2位,总共能表示4种状态 即00,01,10,11。所以这里(biased_lock)偏向锁联合lock来表示5种状态。

我们这里来看下无锁状态的mark word
从上面的图中我们知道无锁状态下的mark word
unsed:25 | hashcode:31| unsed:1 | age:4 | biased_lock:1 | lock:2
下面我们实际证明一下

    public static void main(String[] args) {
        O o = new O();
        //计算对象的hashcode值,16进制表示
        System.out.println(Integer.toHexString(o.hashCode()));
        //获取对象内存布局
        String s = ClassLayout.parseInstance(o).toPrintable();
        System.out.println(s);
    }

实际输出的mark word
在这里插入图片描述
理论的mark word
unsed:25 | hashcode:31| unsed:1 | age:4 | biased_lock:1 | lock:2
实际的mark word
(00000001 00101110 01001100 01011111)
(01101000 00000000 00000000 00000000)
根据理论说unsed:25 ,意思前25位应该都是0,但是和实际的不一样,我陷入了懵逼,后来查阅了资料发现,我用的笔记本是微软的架构,存储是小端存储,说白了就是倒着存(不是每一位倒着存,是每一个字节倒着存),至于为啥我也不知道,太深奥了,我不想知道为啥,我只想验证下。所以我们就倒着看,发现后25位确实是0,
在这里插入图片描述

unsed:25hashcode:31unsed:1age:4biased_lock:1lock:2
黄色绿色红色蓝色灰色粉红色

我上面的实际输出图中输出了hashcode,和这里比对的值是一样的,说明mark word的存储确实是这样子的。

文章就先讲到这里了,关于对象其他状态下的验证,大家可以自行去试试。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值