在java的世界中,万物皆可用对象来描述,而一个对象在jvm中又是由以下几个部分组成:
- 对象头
- 实例数据
- 对其填充
结构如图所示
为了能够更清楚的了解对象头,我们可以借助一个java工具,用来打印对象头,用maven引入jol-core,版本我用的0.12版本,差异影响不是很大
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.12</version>
</dependency>
-
对象头是什么
对象头,是用来描述java对象一些抽象信息的公共结构,例如如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等
根据openjdk的官方文档对象头由两部分组成:
- mark word(在64位jvm下该部分由8byte组成)
- klass pointer(在64位jvm下,如果开启了指针压缩(jvm默认开启了)该部分由4byte组成,如果关闭了指针压缩,该部分由8byte组成)
mark word是什么
在64位jvm下,markword会根据不同的锁的状态,呈现不同的存储形态,如下图
最后三个bit比较重要,根据这三个bit可以划分出以下几种状态,下面会通过代码展现各种状态的mark work(由于jvm虚拟机默认开启了4秒钟的延迟偏向,所以在以下实验中需要将延迟偏向时间设置为0(添加jvm参数-XX:BiasedLockingStartupDelay=0),不然不会出现想要的结果)
- 101无锁可偏向
- 如图所示最后3位为101,代表处于无锁可偏向状态
- 001无锁不可偏向
- 偏向锁依赖线程id,但是hashcode和线程id共用的一份存储空间,如果计算过对象的hashcode,那么将不会再有偏向锁的过程,如图所示,最后3位已经变成了001,此时是无锁状态,但是已经不能偏向了,并且可以看到打印出来的hashcode和对象头中存储的hashcode是一样的
- 00轻量级锁
- A线程执行完 B线程抢到锁,两个线程交替抢到锁,此时锁的状态就是轻量级锁。锁的状态为00
public static void main(String[] args) throws InterruptedException {
User user = new User();
//线程1
new Thread(()->{
synchronized (user){
System.out.println(ClassLayout.parseInstance(user).toPrintable());
}
}).start();
Thread.sleep(5000);
//线程2
//必须要有个线程一直存活,这样第3个线程的线程id和第一个线程的id才会不一样,
//线程的id不一样才会触发轻量级锁,如果线程的id一样,那么锁的状态就还是偏向锁
new Thread(()->{
for (;;){
try {
Thread.sleep(30000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
Thread.sleep(1000);
//线程3
new Thread(()->{
synchronized (user){
System.out.println(ClassLayout.parseInstance(user).toPrintable());
}
}).start();
}
打印一下对象头
可以看到刚开始锁的状态是无锁可偏向状态(101),第一个线程释放锁后第二个线程抢到锁的时候就变成了轻量级锁(00)
- 10重量级锁
- 以下代码模拟两个线程竞争锁,从打印的结果来看,第二个线程加入竞争之后,锁的状态由无锁可偏向(101)变为重量级锁(010)
public static void main(String[] args) throws InterruptedException {
User user = new User();
new Thread(()->{
synchronized (user){
for (;;){
System.out.println(ClassLayout.parseInstance(user).toPrintable());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
Thread.sleep(2000);
new Thread(()->{
synchronized (user){
}
}).start();
}
打印一下对象头
- 11被gc标记
- 跟垃圾收集器有关
klass pointer是什么
Klass Pointer是一个指向方法区中Class信息的指针,虚拟机通过这个指针确定该对象属于哪个类的实例。在64位的JVM中,支持指针压缩功能,根据是否开启指针压缩,Klass Pointer占用的大小将会不同,如果开启了指针压缩(jvm默认开启),那么将会占用4个字节,如果关闭了指针压缩,那么将会占用8个字节,可以根据jvm参数
-XX:(+-)UseCompressedOops来开启或者关闭指针压缩 我们可以打印对象的对象头,来看到klass pointer占4个字节
关闭指针压缩之后再来打印
可以看到klass pointer已经变成了8个字节
-
实例数据
实例数据大小常常是不固定的,用户可以定义不同的字段,导致不同的实例数据大小,我们可以根据jol-core来看一个对象的实例数据的大小
定义一个类User
public class User {
private int age;
}
打印User对象的对象头
public static void main(String[] args) {
User user = new User();
System.out.println(ClassLayout.parseInstance(user).toPrintable());
}
该部分就是user对象的实例数据部分,由于User类中只有一个int的字段,所以该实例数据部分只有4个字节
-
对其填充
在jvm规范中,要求一个对象所占用到的内存是8bit的倍数,所以当不足8bit的整数倍时,会有一部分对其填充的部分。 在User中添加一个flag字段,再次打印对象头
public class User {
private int age;
private boolean flag;
}
打印user对象的对象头
public static void main(String[] args) {
User user = new User();
System.out.println(ClassLayout.parseInstance(user).toPrintable());
}
可以看到图中的1代表新增的boolean实例数据,大小是1个bit,因为jvm要求是8bit的倍数,所以在2处有一个7个bit的对齐填充