• 承前启后,Java对象内存布局和对象头


    承前启后,Java对象内存布局和对象头

    大家好,我是小高先生。在我之前的一篇文章《并发编程防御装-锁(基础版)》中,我简要介绍了锁的基础知识,并解释了为什么Java中的任何对象都可以作为锁。在那里,我提到了对象头中有一个指向ObjectMonitor的指针,但没有深入探讨Java对象的内存结构。本文将引导大家深入了解Java对象的内存布局以及对象头结构,帮助大家更好地理解Java中的对象和锁,并为之后学习synchronized和锁升级打下基础。

    • new Object()怎么理解?
    • JOL
    • GC分代年龄

    new Object()怎么理解?

    平时我们开发的时候经常要new一个对象,很少想过这个对象存在哪里以及这个对象是有什么构成的,所以关于new Object的理解可归于两点:

    1. 位置所在(where)

    学过JVM的同学都应该知道JVM内存结构,我们new出来的对象就在堆区,大对象在老年代,小对象在新生代。

    1. 构成布局(what)

    1.对齐填充

    先讲一下对齐填充,这个比较简单。HotSpot虚拟机的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,也就是对象的大小是8字节的整数倍。对象头已经被设计成8字节的倍数,通常是1倍或2倍,如果实例数据大小不是8字节的整数倍,就需要用对齐填充添加一下,使对象大小为8字节的整数倍。

    2.对象头

    对象头是对象的另一个重要组成部分,它包含了一些关于对象的元信息。具体来说,对象头包括Mark Word和类元信息(类型指针)。

    Mark Word

    平时写代码的时候可能会通过hashcode()方法获取对象的哈希码,那哈希码保存在哪里呢?还有JVM中垃圾回收机制里说过,当一个对象在新生代经过15次Minor GC之后还存活下来就会进入老年代,那对象怎么记录GC次数呢?这些信息都记录在Mark Word中,先不需要都知道里面的标记代表什么,后续会讲到,这里只需要知道Mark Word存储了对象的一些重要信息。

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

    在64位系统中,Mark Word占8字节,类型指针占8字节,一共16字节。所以当我们new一个对象,还没有实例数据,那这个对象就是16字节。64为系统下,Mark Word就是64bit大小,存储结构如下:

    比如现在对象是无锁状态,那Mark Word前25bit不用,之后的31bit存hashCode,偏向锁标志位为1,锁标志位为01,其他锁状态会在相关文章中讲解。

    Mark Word默认存储hashCode、分代年龄和锁标志位等相关信息,这些信息都是与对象自身定义无关的数据,会根据对象的状态复用自己的存储空间,运行期间数据会随着锁状态而改变

    类元信息(类型指针)

    Phone ph = new Phone(),创建一个对象,会在JVM方法区存放对象的Klass类元信息,对象头的类型指针就是这个对象指向它的类元信息的指针,JVM通过类型指针确定这个对象是哪个类的实例,比如我创建Animal类,new Animal()实例的类型指针就会指向方法区中的Animal的Klass类元信息。由于Animal里面没有创建属性,所以Animal对象还没有实例数据,大小为16字节。

    public class ObjectHeadDemo {
        public static void main(String[] args) {
            Animal a = new Animal();
        }
    }
    class Animal{
    
    }
    

    我给大家百度了Klass类元信息具体是什么,因为一开始我也不懂。

    Klass类元信息是JVM中表示类的元信息的数据结构

    JVM中,每个Java类都对应一个Klass实例,是用C++编写的一个类,存储了Java类的所有元信息,包括属性、方法、修饰符等。Klass模型是JVM内部使用的一个概念,是Java类的内部表示。这个模型包含了类的所有信息,如类的完整名称、访问修饰符、父类、实现的接口、属性和方法等。这些信息在类加载的过程中被读取并存储到方法区。

    这么看起来,Klass就想是这个类的模板。

    3.实例数据

    class Animal{
    	int id;
        boolean flag = false;
    }
    

    如果创建了一个Animal对象,大小就是对象头(16字节)+ int(4字节)+ boolean(1字节)= 21字节,需要有对齐填充,21 + 3 = 24字节。

    JOL

    有关Java对象布局的理论知识已经学完了,那能不能从代码层面验证一下对象的结构呢。

    JOL(Java Object Layout)是一个专门用于分析Java虚拟机(JVM)中对象内存布局的工具箱。它能够提供非常精确的关于对象如何分布在JVM内存中的信息。

    1.先在POM文件中导入坐标

    <dependency>
        <groupId>org.openjdk.jolgroupId>
        <artifactId>jol-coreartifactId>
        <version>0.17version>
    dependency>
    

    2.通过指令查看JVM详细细节

    public class JOLDemo {
        public static void main(String[] args) {
            //VM的详细细节
            System.out.println(VM.current().details());
            //获取当前JVM实例的对象对齐方式
            System.out.println(VM.current().objectAlignment());
           
        }
    }
    class Customer{
        int id;
        boolean flag = false;
    }
    

    Object alignment指的是对象在内存中的对齐方式。8 bytes表示对象的大小必须是8的倍数,即对象的起始地址必须是8的整数倍。

    我们创建一个Object类,看看这个类具体的结构是什么样的,通过ClassLayout.parseInstance(o).toPrintable()来查看。

    public class JOLDemo {
        public static void main(String[] args) {
            Object o = new Object();
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
    

    参数 作用
    OFFSET 偏移量,也就是这个字段起始位置
    SIZE 字节大小
    TYPE Class中定义的类型
    DESCRIPTION 类型描述
    VALUE 是TYPE在内存中的值

    我们这里会发现类型指针只占了4字节,之前说的是占8字节,这是为什么?这个我们稍后讨论。

    另外一个细节会发现Instance size为16字节,因为对象头不够8的倍数,要补齐,也就是new一个对象默认大小为16字节。刚刚是创建了一个Object对象,现在再创建一个自己写的类看看结果,并且类中没有实例数据。结果表明自己写的类和Object类new出来的对象内存布局是一样的。

    public class JOLDemo {
        public static void main(String[] args) {
            Object o = new Object();
            //System.out.println(ClassLayout.parseInstance(o).toPrintable());
            
            Customer c = new Customer();
            System.out.println(ClassLayout.parseInstance(c).toPrintable());
        }
    }
    class Customer{
    	//只有对象头,没有实例数据
    }
    

    另外一种情况是类中有实例数据,看看结果,这次除了对象头以外多了实例数据,然后也是用对齐填充补到8的倍数。

    public class JOLDemo {
        public static void main(String[] args) {
            Object o = new Object();
            //System.out.println(ClassLayout.parseInstance(o).toPrintable());
    
            Customer c = new Customer();
            System.out.println(ClassLayout.parseInstance(c).toPrintable());
        }
    }
    class Customer{
        int id;
        String name;
    }
    

    现在来研究研究为什么类型指针是4而不是8,这是因为压缩指针的影响。使用java -XX:+PrintCommandLineFlags -version指令,启动Java程序的时候把所有参数打印出来。-XX:+UseCompressedClassPointers这个参数就表示使用了压缩指针,默认开启,结果就导致类型指针是4字节。


    试试不开启指针压缩是什么样,正常工作中是不会用到的,这里就是跟大家尝试一下,看看结果而已。

    GC分代年龄

    GC年龄在Mark Word中用4bit存储,最大为15,也就是JVM参数-XX:MaxTenuringThreshold = 15。我们也可以验证一下,调整参数改成16试试,运行时是有异常的。

    总结

    本文和朋友们一起学习Java对象内存布局的知识,对象由对象头、实例数据和对齐填充组成。对象头包括Mark Word和类型指针,Mark Word记录一些对象的关键信息,主要包括锁的状态,类型指针是可以确定这个对象是哪个类的实例。JVM要求对象大小是8字节的整数倍,所以当对象头和实例数据大小不是8字节整数倍的时候,就需要对齐填充帮忙补齐。

    东西不多但是挺重要的,可以更好地理解之前讲的synchronized底层分析,也可以对未来要学的锁升级做好准备。

  • 相关阅读:
    第四章 Docker 使用示例
    重新安装mysql时,Windows Service Name已经被占用了怎么办
    mysql查询结果拼接树结构(树节点的移动)
    lambda表达式
    学习大数据可以进入哪些公司?
    自定义注解、数字字典注解
    Redis 学习整理
    UDS入门至精通系列Service 86
    11.3 冒泡排序
    防火墙最新技术汇总
  • 原文地址:https://www.cnblogs.com/GXH980804/p/18025256