我们知道,一个对象在内存中有三部分,对象头,实例数据,对齐填充。
1.对象头包含运行时元数据和类型指针。运行时元数据包含哈希值(这个哈希值就是创建出来的对象在内存中的地址)、GC分代年龄、锁状态标志等。类型指针,指向方法区(元空间)中对象所属的类型。如果创建的是数组,对象头还会保存数组的长度。
2.实例数据。类中定义的成员变量。规则:先放父类的成员变量,在放子类的成员变量。相同宽度的变量被放在一起。如果compactFileds参数为true,子类的窄变量可能插入到父类变量的空隙。
3.对齐填充
虚拟机栈中保存的是一个个栈帧,栈帧中是一个个成员变量,其中cust成员变量指向了堆空间中创建的某个类实例,对象实例包含对象头、实例数据、以及对齐填充。
看下图,对象B没有任何参数时,对象结构如下

对象头12byte 对齐填充4byte
对象L有一个int类型的参数a时,对象L结构如下
对象头12byte 实例数据4byte
1byte=8bit
仔细看下上图中右边的VALUE,对象头12byte=96位

第一个word包含了锁的信息,hashcode,gc等信息,第二个word主要指向对象的元数据(对象存储在元空间的地址),它是一个指针。

发现hashcode存在于对象头,但是为什么顺序是倒着存储,这里不得不提到小端存储。

什么是小端存储?高字节存在于高地址,低字节存在于低地址。

图片源自子路。图片上面8byte应该是笔误,int是4个byte=32bit,1对应的就是00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001,从左往后是高字节->低字节,计算机存储方式是小端存储,入栈时高字节在栈的底部,低字节在顶部,出栈时自然就变成00000001 00000000 00000000 00000000 00000000 00000000 00000000 00000000,太刁了!
回头再看对象头里面的markword的结构

前25位没有用到,第二十五位也一定是0,如图所示

再次验证hashcode确实存在于对象头,可以发现0001011(不是00001011)对应的16进制就是b!
这里再拿hashcode举例,hashcode打印出来是b4c966a,对应的进制0001011(7bit) 01001100 10010110 01101010,从左往后是高字节->低字节,入栈时栈底-栈顶b->4c->96->6a,出栈时就是我们看到的 01101010 10010110 01001100 00001011(补位一个0) 00000000 00000000 00000000,一共56位,前提是计算了hashcode。
再看markword的剩余的8bit,第一位unused,第二位-第五位存储的是对象分代年龄,第六位:偏向标识,是否可偏向。第七八位:锁状态。
回过头再梳理下,new一个对象B,里面没有任何属性或方法时,存储的B是16byte,12byte是对象头,4byte是对齐填充。在B类里面加一个int类型的属性a时,存储的L也是16byte,12byte对象头,4byte实例数据。
再来说说锁的几种状态。主要有三种状态,无锁状态,加锁状态,gc标记状态。那么我们可以理解Java当中获取锁其实就是给对象上锁,也就是改变对象头的状态,如果上锁成功则进入同步代码块。
先说说无锁状态,在不打印hashcode的情况下,此时是无锁可偏向(101 第一个1表示不可偏向 后面01是任何对象初始化后都是01),但未偏向。睡眠5秒是因为jdk1.8默认是4s后开启偏向锁。

怎么证明此时是可偏向的?加一行加锁的代码。

再看看打印hashcode的情况下,此时时无锁不可偏向。(001)

怎么证明此时不可偏向?加一行加锁的代码。

在有资源竞争(同时竞争一把锁,不是交替执行)的情况下,会变成重量锁。(代码省略)

性能排序:偏向锁>轻量锁>重量锁
为什么偏向锁性能高?
因为偏向锁是不是走操作系统的,此时首先判断是否可偏向,其次判断是否偏向了,如果未偏向,通过cas将线程id设置到对象头。如果已偏向,取出对象头的线程id和当前线程的id比较,如果相同拿到锁继续执行。从头到尾没有和操作系统交互。
所以如果面试官问你synchronized是不是重量锁?
如果是同一个线程加锁,偏向锁。
如果是不同线程交替执行,轻量锁。
如果是资源竞争抢锁(mutex实现互斥锁),重量锁。
package com.example.并发编程.test;
import com.example.并发编程.entity.B;
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;
@Slf4j(topic = "example")
public class TestJol {
//static B b = new B();
public static void main(String[] args) throws InterruptedException {
Thread.sleep(5000);
final B b = new B();
System.out.println(Integer.toHexString(b.hashCode()));
log.debug(ClassLayout.parseInstance(b).toPrintable());
synchronized (b){
log.debug(ClassLayout.parseInstance(b).toPrintable());
}
}
}
package com.example.并发编程.entity;
/**
* 对象头
* 实例数据
* 对齐填充
*/
public class B {
}
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>RELEASE</version>
<optional>true</optional>
</dependency>
Java对象内存布局与锁状态解析
本文详细探讨了Java对象在内存中的布局,包括对象头、实例数据和对齐填充。解释了对象头中的运行时元数据和类型指针,以及如何存储哈希码和锁状态。介绍了小端存储的概念,并通过实例分析了对象头中的MarkWord结构。此外,还讨论了Java锁的三种状态(无锁、偏向锁和重量锁),以及它们在并发性能上的差异。文章使用JOL库展示了对象实例的内存布局,并通过代码示例展示了不同锁状态的转换过程。
653

被折叠的 条评论
为什么被折叠?



