美文网首页
JVM-对象内存布局

JVM-对象内存布局

作者: 甜甜起司猫_ | 来源:发表于2021-06-29 09:53 被阅读0次

    jvm-对象内存布局

    对象内存结构概述

    对象的创建过程:

    1. jvm将对象所在的class文件加载到方法区中
    2. jvm读取main方法入口,将main方法入栈,执行创建对象代码
    3. 在main方法的栈内存中分配对象的引用,在堆中分配内存放入创建的对象,并将栈中的引用指向堆中的对象

    堆内存中的对象由3部分组成:

    1. 对象头 header
    2. 实例数据 instance data
    3. 对齐填充字节 padding

    对象头

    对象头存储的是对象在运行时状态的相关信息、指向该对象所属类的元数据的指针,如果对象是数组对象那么还会额外存储对象的数组长度

    对象头的组成

    普通对象:

    1. 标记字 mark word
    2. 指针类型 klass pointer

    数组对象:

    1. 标记字 mark word
    2. 指针类型 klass pointer
    3. 数组长度

    mark word

    在对象头中,mark word 一共有64个bit,用于存储对象自身的运行时数据,标记对象处于以下5种状态中的某一种:

    (此处应有图)

    在mark word中,锁(lock)标志位占用2个bit,结合1个bit偏向锁(biased_lock)标志位,这样通过倒数的3位,就能用来标识当前对象持有的锁的状态,并判断出其余位存储的是什么信息。


    基于mark word的锁升级流程:

    无锁/可偏向

    锁对象刚创建时,没有任何线程竞争,对象处于无锁状态。在上面打印的空对象的内存布局中,根据大小端,最后3位001,表示处于无锁态,并且处于不可偏向状态。这是因为在jdk中偏向锁存在延迟4秒启动,也就是说在jvm启动后4秒后创建的对象才会开启偏向锁,我们通过jvm参数-XX:BiasedLockingStartupDelay=0取消这个延迟时间,这时最后3位为101,表示当前对象的锁没有被持有,并且处于可被偏向状态。

    偏向锁

    在没有线程竞争的条件下,第一个获取锁的线程通过CAS将自己的threadId写入到该对象的mark word中,若后续该线程再次获取锁,需要比较当前线程threadId和对象mark word中的threadId是否一致,如果一致那么可以直接获取,并且锁对象始终保持对该线程的偏向,也就是说偏向锁不会主动释放。
    偏向锁的CAS环节在修改thread id

    无锁和偏向锁状态时,偏向锁标记位是有值的,所以看后三位

    轻量级锁

    当两个或以上线程交替获取锁,但并没有在对象上并发的获取锁时,偏向锁升级为轻量级锁。在此阶段,线程采取CAS的自旋方式尝试获取锁,避免阻塞线程造成的cpu在用户态和内核态间转换的消耗。
    轻量级锁的CAS环节在自旋获取锁

    轻量级锁和重量级锁时,已不需要偏向锁标记,所以只有后两位

    综合上面3个阶段,整个加锁状态的变化流程如下:

    1. 主线程首先对user对象加锁,首次加锁为101偏向锁
    2. 子线程等待主线程释放锁后,对user对象加锁,这时将偏向锁升级为00轻量级锁
    3. 轻量级锁解锁后,user对象无线程竞争,恢复为001无锁态,并且处于不可偏向状态。如果之后有线程再尝试获取user对象的锁,会直接加轻量级锁,而不是偏向锁

    重量级锁

    当两个或以上线程并发的在同一个对象上进行同步时,为了避免无用自旋消耗cpu,轻量级锁会升级成重量级锁。这时mark word中的指针指向的是monitor对象(也被称为管程或监视器锁)的起始地址。在两个线程同时竞争user对象的锁时,会升级为10重量级锁。

    mark word中的其他信息

    hashcode

    无锁态下的hashcode采用了延迟加载技术,在第一次调用hashCode()方法时才会计算写入。

    只有在调用没有被重写的Object.hashCode()方法或System.identityHashCode(Object)方法才会写入mark word,执行用户自定义的hashCode()方法不会被写入。

    当对象被加锁后,mark word中就没有足够空间来保存hashCode了,这时hashcode会被移动到重量级锁的Object Monitor中。

    epoch

    偏向锁的时间戳

    分代年龄(age)

    在jvm的垃圾回收过程中,每当对象经过一次Young GC,年龄都会加1,这里4位来表示分代年龄最大值为15,这也就是为什么对象的年龄超过15后会被移到老年代的原因。在启动时可以通过添加参数来改变年龄阈值-XX:MaxTenuringThreshold,当设置的阈值超过15时,启动时会报错。

    Klass Pointer 类型指针

    Klass Pointer是一个指向方法区中Class信息的指针,虚拟机通过这个指针确定该对象属于哪个类的实例。在64位的JVM中,支持指针压缩功能,根据是否开启指针压缩,Klass Pointer占用的大小将会不同:

    1. 未开启指针压缩时,类型指针占用8B (64bit)
    2. 开启指针压缩情况下,类型指针占用4B (32bit)

    在jdk6之后的版本中,指针压缩是被默认开启的

    实例数据

    实例数据存储的是对象的真正有效数据,也就是各个属性字段的值,如果在拥有父类的情况下,还会包含父类的字段。字段的存储顺序会受到数据类型长度、以及虚拟机的分配策略的影响

    对齐填充字节

    在java对象中,需要对齐填充字节的原因是,64位的jvm中对象的大小被要求向8字节对齐,因此当对象的长度不足8字节的整数倍时,需要在对象中进行填充操作。注意图中对齐填充部分使用了虚线,这是因为填充字节并不是固定存在的部分,这点在后面计算对象大小时具体进行说明

    相关文章

      网友评论

          本文标题:JVM-对象内存布局

          本文链接:https://www.haomeiwen.com/subject/ssuhultx.html