傻瓜都看得懂的Java对象头内存模型

Java对象内存模型

image-20200910184519016

前言

我们的一个Java对象在内存中究竟长什么样子,我们类文件最终会被编译为字节码文件,然后被类加载器加载,并加入到内存。我们的字节码文件是个二进制文件,虽然我们可以通过可以把.class文件反编译为JVM指令,但是还是无法观察到Java对象的信息。

初探内存模型

内存可视化工具

Java对象内存模型可视化工具,提供一个工具类,可以讲一个对象的内存信息变成可以打印(print)的形式。

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

基本使用

我们new一个空的Object对象,我们看看打印结果是什么

@Test
public void test0() {
    Object o = new Object();
    String s = ClassLayout.parseInstance(o).toPrintable();
    System.out.println(s);
}

结果

看不懂,别着急。

  • OFFSET表示初始字节,SIZE表示字节大小,Instance size:16 bytes 实例大小,VALUE表示值。

image-20200910164851054

我们得到几个信息:

  • 一个空的Obect对象有16字节的大小,一个字节是八位,所以一共是128位,也就是128个0101这种二进制组成的。
  • OFFEST=0,SIZE=4表示从第0的个字节开始,右移四位。我们看到一个空的对象的前面8个字节几乎没有信息,如果我们赋予Object一个值,或者其他操作,说不定就有信息了。
  • 前12位的描述都是object header(对象头),第12字节往后4个字节的描述是(loss due to the next object alignment)没有指向下一个对象的地址。

根据以上信息我们试着给这个Object赋值

@Test
public void test0() {
    Object o = new Integer(2);
    String s = ClassLayout.parseInstance(o).toPrintable();
    System.out.println(s);
}

我们看到第12字节往后4个字节的位置存放了int类型的值,VALUE为2。
image-20200910171309043

我们知道int类型的长度为32位,也就是4个字节,那如果我们给Object赋值一个Long会怎么样?

    @Test
    public void test0() {
        Object o = new Long(123);
        String s = ClassLayout.parseInstance(o).toPrintable();
        System.out.println(s);
    }

我们知道Long占用8个字节,显然这边4个字节是不够的,我们发现最终申请了24字节的大小,其中long占用了8字节,对象头4+4+4=12字节,还有4个字节的描述是alignment/padding gap(对齐填充)。

image-20200910171731763

对齐填充

关于这个对齐填充,我查阅了一下资料,总结这么几点:

Java使用对齐填充,为什么对象必须是8个字节的倍数?

  • 其目的是alignment,允许以某些空间为代价更快地访问内存。如果数据未对齐,那么处理器需要在加载内存后进行一些调整才能访问它。
  • 此外,垃圾收集简化(加速)越大最小分配单元的大小。
  • Java不太可能需要8个字节(64位系统除外),但由于32位架构是常态在创建Java时,Java标准中可能需要4字节对齐。简单的说,8的倍数就完事了。

理性分析

光看这个打印图说明不了什么,我们看一下Java对象内存模型示意图,来找一找方向。

我们看到,一个Java对象由对象头markword,类型指正class pointer,实例数据 instance data 和对齐 padding组成,之前控制台结果我们知道,一个new Object()对象的对象头分为三部分,每部分4字节,实例数据占用4个字节。结合这个图我们对普通的对象进行分析,数组多了一个数组长度四个字节,也就是是说一个数组的长度最多占用2^31的大小;

image-20200909165625022

如图:

  • markword占用8个字节
  • class pointer占用4个字节
  • 实例数据为空时,对齐padding占4个字节

可以把markword和class pointer这12个字节概括为对象头,而实例数据更具其大小,可能会导致对象的大小为16或者24字节。

image-20200910173255681

class pointer

@Test
public void test0() {
    Object o = new Object();
    Object o2 = new Object();
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
    System.out.println(ClassLayout.parseInstance(o2).toPrintable());
}

我们看到,不同的Object对象,class pointer(OFFSET 8 SIZE 4)是一致的,也就是说,我们这个class pointer代表了这个对象是什么类型的,而且每次运行这个代码,结果都是一致的,所以这个二进制码是精确代表某个类的(下面我们讲hashcode的时候,结果是变化的)。

image-20200910173954425

markword

我们看到markdown里面默认有一个01,实际上01代表的是字节,1的二进制是0001。其他值空空如也,那么我们的markword里面又涵盖什么信息呢?

我们知道Java的每个对象都可以当一把锁使用,我们参考如下图,首先一共是64bit,也就是8个字节,这就是markword里面的的信息。

image-20200910180214679

观察此图,我们得到几个结论:

  • 默认情况下,一个类是无锁态,我们会有3bit记录锁的标记,4bit记录分带年龄,25bit记录hashcode。
  • 当我们把类传入synchronized里才会附加一些锁的信息。

我们一一进行测试:

分代年龄

我们知道垃圾回收清除不掉的时候,我们给清不掉的对象分代年龄加1,我们先答应一下没进行垃圾回收的,再进行一个垃圾回收,再打印看看。

image-20200910175023374

我们看到加了一位,之前我们的图可知,一个对象默认有一个01的锁状态还有一个1代表偏向锁,所以默认有一个001,这个新的1代表分代年龄1岁。由于图中得知分代年龄占用4个bit,所以一个类最多15岁。JVM调优问你永久代经常OOM怎么办,你说分代年龄调整到31,让他进行永久代的机会变难一点,不好意思,你可以走了。

image-20200910175150200

hashcode

32位操作系统
image-20200910175731406
64位操作系统
image-20200910180214679

以64位操作系统为例:我们看到,无锁态时,锁标记我们看到了,为什么看不到这个hashcode呢?这里有点像懒加载,我们知道hashcode是个本地方法,C++实现。当我们不去调用hashcode方法时,我们的Java对象头是不会记录信息的。

我们调用以下hashcode方法

@Test
public void test0() {
    Object o = new Object();
    o.hashCode();
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
}

我们看到结果发生了变化
image-20200910180051995
那么我们Object算出来的hashcode和这些0101,有什么关系吗?我们知道hashcode是31位的,我们对比一下:

    @Test
    public void showHash(){
        System.out.println("hashcode:"+Integer.toBinaryString(object.hashCode()).substring(0,7));
        System.out.println("hashcode:"+Integer.toBinaryString(object.hashCode()).substring(7,15));
        System.out.println("hashcode:"+Integer.toBinaryString(object.hashCode()).substring(15,23));
        System.out.println("hashcode:"+Integer.toBinaryString(object.hashCode()).substring(23,31));
        System.out.println(ClassLayout.parseInstance(object).toPrintable());
    }

我们发现:在不重写Object的hashcode方法时,我们对象头的hashcode等于我们object算出来的hashcode值
在这里插入图片描述

奇怪,为什么顺序反了?是这样的,我ClassLayout工具只是负责打印对象头的二进制值,实际上我们JVM读取对象头信息时,是从高位往低位读取的。

**注意!**这并不代表逆序,而是先顺序的读取最高的8位,然后读取稍低的8位,以此类推。所以我们实际读取到的顺序是(图中白色的顺序):

00000000 00000000 00000000 01100110 11001101 01010001 1100011 00000001

image-20200918101531657

对比一下,之前的图,图确实不错,顺序是对的!

image-20200918102017751

之前我们创建空对象的时候我们看到01就猜想是不是锁标志位01,然后垃圾回收一次时,分代年龄还在01的前面还要在前面一位置为1,所以我们猜想01前面的0是不是偏向锁标识位,我们这回证明了吧!

image-20200918102114887


锁信息

偏向锁

好了这个对象头已经被我们肢解的差不多了,我们跑几个锁试试。关于这些锁的知识,我单独整理了一篇博客。

@Test
public void test4() {
    Object o = new Object();
    synchronized (o) {
        System.out.println(ClassLayout.parseInstance(o).toPrintable());

    }
}

根据示意图我们得知,此时我们的Object对象从无锁态变成偏向锁态,对象头记录了当前的线程的信息。

image-20200918102709672

分析一波:

我们先整理一下这个对象头,实际顺序是:

image-20200918102550083

image-20200918102416205

我们看到和图片有点对不上啊!偏向锁还有一个隐式的条件,我们的Java程序如果4秒之内就执行完了,说明是个小程序,还锁什么锁啊?

这里我们少一个条件,那就是让线程睡个5秒,来保证偏向锁的开启。(JVM启动之后会有4秒的延迟才会开启偏向锁)

public class Test1 {
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(5000);
        Object o = new Object();
        synchronized (o){
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
}

运行结果

image-20200918103618265

哈哈,来了老弟

在讲轻量级锁之前,我发现一个很有意思东西

public class Test1 {
    static Object o = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(5000);
        synchronized (o) {
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
}

如果把Object作为静态的,那么我们发现这个锁标志位变成了00,按照标识图。这是轻量级锁的意思,但是我们执行完synchronized再打印一个锁信息,锁又是无锁态有人知道为什么吗?欢迎评论

image-20200918112426683

轻量级锁

没搞懂,以后再来

重量级锁

这还不简单吗?睡久一点肯定重量啊.

我们知道锁不能降级只能升级,我们跑两个线程,第一个线程进去睡10秒,第二个线程等10秒早重量了!然后等到两个线程执行完,我们再看看锁信息

public class Test1 {

    static Object o = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(10);
        for (int i = 0; i < 2; i++) {
            new Thread(Test1::run).start();
        }
        TimeUnit.SECONDS.sleep(15);
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }

    public static void run() {
        synchronized (o) {
            try {
                TimeUnit.SECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

结果

image-20200918110947526

总结:

  • Java对象由markword,classpoint,instance data,padding组成,其中
    • markdword占用8字节 存放锁,分代年龄和hashcode信息。
    • classpoint占用4字节 表示这个对象是哪个类
    • 实例数据根据数据类型有所不同 int 4字节 long 8字节
    • padding为对齐填充,保证整个Java对象的内存空间是8的整数倍
  • hashcode只要在调用hashcode方法的时候才会加入对象头中吗,每次计算结果都不一样
  • 数组则多一个数组长度,以后面试官问你为什么数组的长度是2^31答的上来的
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值