Java对象在内存中存储结构

本文详细介绍了Java对象在HOTSPOT虚拟机中的存储结构,包括对象头(Mark Word、Class Pointer和数组长度)、实例数据以及对齐填充。讲解了对象头的Mark Word中存储的运行时数据,如哈希码、GC年龄、锁状态等,并分析了不同锁状态的转换。此外,还讨论了如何通过对象头获取元数据以及两种访问对象的方式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Java对象存储:(HOTSPOT虚拟机)

一个Java对象可以分为三部分存储在内存中,分别是:

  • 对象头Object Header
  • 实例数据Instance
  • 对齐填充Padding

在这里插入图片描述

什么是对象头,具体包括什么

在HotSpot虚拟机中,对象头分为两部分(普通对象)

  • 1.对象自身的运行时数据: 包括存储哈希码,GC分代年龄,锁状态标记,线程持有的锁,偏向锁ID,偏向时间戳等.这部分被叫做mark word,在32位和64位的虚拟机分别占32bits和64bits.
  • 2.对象的类型指针: 即指向对象的类元数据的指针.虚拟机可以通过该指针判定对象实例属于那个类.

注:在Java对象中比较特殊的是数组,一个数组实例的对象头中必须包含数组的长度.JVM可以通过对象头中的数组长度数据来判定数组的大小,这是访问数组类型的元数据是无法得知的

JVM中对象头有两种:

1.普通对象
Object Header: 包括mark word和class pointer
2.数组对象
Object Header : 包括mark word,class pointer和array length
数组特有的length ,JVM需要通过数组对象头的length来判定数组的大小.

                                Mark Word

64位虚拟机
在这里插入图片描述
32位虚拟机在这里插入图片描述

Mark Word分析 (64位):

  • 对象的hashcode(identity hash code):
    占31bit,根据内存地址生成的hash值.

  • GC分代年龄:
    age 占4bits,在GC过程中,如果新生代对象在Survivor区复制一次,年龄+1.当对象年龄达到设定的阈值时,对象将会进入老年代.或者有一半的的对象年龄相等,则大于该年龄的对象直接进入老年代.默认情况GC年龄阈值为15.因为age只占4位 ,所以最大值为1111= 15.

  • 锁标志位: 占2bit位,希望用尽可能少的二进制位表示尽可能多的信息,所以设置了锁的标记.该标记的值不同,整个mark word 表示的含义不同.

  • biased_lock(是否为偏向锁) : 对象是否为偏向锁,只占1个bit. 为1时表示对象启用偏向锁,为0表示对象没有偏向锁.

    无锁和偏向锁的锁标志都是01 ,区分的方式是通过1bit的是否为偏向锁(biased_lock).偏向锁的这1bit等于1,无锁这一位则为0

  • 锁的状态

    • new (未锁定状态)
      在一个对象刚new出来的时候,没有线程竞争,就是一个普通对象,不需要加锁,此时biased_lock为1,锁的标志位为01 , 表示无锁可偏向 . ( 如果计算了对象的hashcode ,则会记录对象的hashcode,锁的标志位为01.biased_lock位置为0. 此时表示无锁不可偏向状态)

    • 偏向锁:biased_lock
      如果已经计算了对象的hashcode ,则表示该锁不能偏向 .直接升级为轻量级锁.(对象的hashcode和偏向线程id只能存储一个)
      1.当第一个线程A来获取资源的时候,这个时候只有线程A一个,没有其他线程来竞争,他会将biased_lock标志位置为1,锁标志为01, 表示已经偏向它的状态.线程ID也会记录A的id.
      2.当A线程再次获取该资源的时候,JVM发现mark word里面的线程id是A的id,锁的标志位是01,biased_lock是1,表示A已经获得该偏向锁.

    • 轻量级锁(自旋,自旋锁,look-free,CAS轻量级锁):
      当线程B尝试获取该锁时(此时有了锁的竞争),JVM发现此时锁处于偏向状态,mark word的线程id记录的是A,此时线程B会尝试通过CAS的方式(在用户空间)获取锁.两个线程都将对象的hashcode复制到自己新建的用于存储锁的记录空间LockRecord,通过CAS的操作,将对象的mark word的内容修改为自己新建的记录空间的地址来竞争mark word.成功则获取资源,失败则继续CAS操作.
      自旋的线程在自旋过程中,成功获取资源,整个状态仍然处于轻量级锁的状态.

    • 重量级锁:

      竞争加剧 在jdk6: 自旋锁自旋次数超过10,或者等待线程超过CPU核数的二分之一,升级为重量级锁
      jdk6以后:jdk自适应自旋,来判断什么时候升级

      自旋的线程将被阻塞,需经过os,提供一个等待队列和一个竞争队列.等待操作系统的调度

  • 当前线程指针: 存放获取偏向锁的线程id,占54bit

  • Epoch: 偏向锁的时间戳,占2bit

  • ptr_to_lock_record: 轻量级锁状态下,指向栈中锁记录的指针。

  • ptr_to_heavyweight_monitor: 重量级锁状态下,指向对象监视器Monitor的指针。

Class Pointer (类型指针)

这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例.
-XX:+UseCompressedClassPointers 为4字节(默认开启) 不开启为8字节 (对象属于哪个Class)

array length(数组特有)

如果对象是一个数组,那么对象头还要有额外的空间存储数组的长度,JVM可以通过对象头中的数组长度数据来判定数组的大小,这是访问数组类型的元数据是无法得知的. 长度为4个字节

实例数据(Instance)

对象头是对象的额外开销,只有实例数据才是一个对象实例存储的有效信息,也是在程序代码中所指定的各种类型的字段内容.这部分内容同时记录了子类从父类继承所得的各类型数据.

-XX:+UseCompressedOops 为4字节(默认开启) 不开启为8字节 Oops Ordinary Object Pointers(成员变量的引用 比如Object o)

对其填充(padding)

对齐填充在对象数据中并不是必然的,只是起着占位符的作用,没有特别含义.HotSpot要求对象起始地址必须是8字节的整数倍.对象头的大小刚好符合要求,因此当实例数据没有对齐时,就由对齐填充来对齐数据.

如何获取的元数据

虚拟机在加载类的时候会将类的信息,常量,静态变量和即时编译器编译后的代码等数据存储在方法区(Method Area).类的元数据,即类的数据描述也被存在方法区.我们知道对象头中会存有对象的类型指针,通过类型指针可以获取类的元数据.因此,对象的类型指针其实指向的是方法区的某个存有类的信息地址
有两种对象定位的方法:(间接和直接)

通过句柄访问对象(间接)

对象的类型指针被放在句柄池中;

通过实例数据指针去堆中找对象,同时通过类型数据指针去方法区找对象的类型
在这里插入图片描述

通过Reference指针直接访问对象(直接)

对象的类型指针被存放在对象本身的数据中
先通过实例数据指针去堆中找到对象,再通过类型数据指针去方法区找对象的类型.
在这里插入图片描述

对象的基本存储方式:

  • 对象的引用:存储在栈中
  • 对象的实例数据:存储在堆中
  • 对象的元数据:存储在方法区

补充

在jdk8中为什么大于4秒钟的时候才会有偏向锁状态?
其实这是因为虚拟机在启动的时候对于偏向锁有延迟。
因为在项目启动的时候,有大量的同步块(synchronized),多个线程访问的时候,需要消除偏向锁。会很麻烦,反而会降低效率。
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 修改上面的jvm参数,可以开启jvm偏向锁延迟配置,延迟为0
-XX:-UseBiasedLocking关闭偏向锁

锁状态转换:

代码示例

public class ObjectTest {
    static ObjectTest a = new ObjectTest();  // 1
    public static void main(String[] args) {
        System.out.println("new 后"+ClassLayout.parseInstance(a).toPrintable());

        a.hashCode();   // 2
        System.out.println("计算hashcode后"+ClassLayout.parseInstance(a).toPrintable());

        synchronized (a) {   // 3
            System.out.println("偏向后"+ClassLayout.parseInstance(a).toPrintable());
        }
        
        new Thread("t1"){   // 4
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lock();
            }
        }.start();

        new Thread("t2"){  //5
            @Override
            public void run() {
                lock();
            }
        }.start();
    }

    public static void lock(){
        synchronized (a){
            System.out.println(Thread.currentThread().getName()+ClassLayout.parseInstance(a).toPrintable());
        }
    }
}

添加依赖

<dependencies>
        <!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.11</version>
        </dependency>
    </dependencies>

通过ClassLayout.parseInstance(Instance).toPrintable()方法查看对象布局.

new一个对象, 一开始为无锁可偏向状态.有线程操作 ,则偏向该线程,设置偏向线程id为该线程id.
在这里插入图片描述
如果new一个对象之后计算了该对象的hashcode,则锁状态为 无锁不可偏向状态 ,会存储hashcode值 , 有线程来会直接升级为轻量级锁.
在这里插入图片描述
new一个对象,先有一个线程, 则偏向线程id设置为该线程id ,又来一个线程,有了锁的竞争 ,则升级为轻量级锁.
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值