对象头信息分析
1. 对象的内存分布
- 在 HotSpot 虚拟机中,对象在内存中存储的布局可以分为 3 块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
- 对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等。
- 对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。如果对象是一个 java 数组,那么在对象头中还有一块用于记录数组长度的数据。
- 第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于 HotSpot VM 的自动内存管理系统要求对对象的大小必须 是 8 字节的整数倍。当对象其他数据部分没有对齐时,就需要通过对齐填充来补全(比如对象头+实例数据大小为30,对齐填充自动变为2,此时对象大小变为32)。
1.1 创建一个maven工程
- 引入对象分析jar包
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
1.2 创建一个空的实体类A
1.3 创建对象分析测试类HeadTest
package org.example.test;
import org.example.entity.A;
import org.openjdk.jol.info.ClassLayout;
public class HeadTest {
public static void main(String[] args) {
A a = new A();
//parseInstance:解析实例,toPrintable:格式化打印
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
1.4 查看打印
- 对象头占12字节,对齐填充占4字节,整个对象大小为16字节,是8字节的整数倍。
- 这里因为对象A内没有任何变量,为了满足对象大小为8的整数倍,所以有了4字节的对齐填充。
1.5 为对象A添加一个整数型变量
1.6 查看打印
1.7 结论
- 根据上面的结果可以认为一个对象的布局大体分为三个部分分别是
- 对象头(Object header,占12字节)
- 对象的实例数据
- 对齐填充(非必须)
2. 对象头的规范
- HotSpot关于jdk的说明http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html
- 对象头组成
- type–类型指针
- GC state–gc分代年龄
- synchronization state–锁的状态
- identity hash code–一致性哈希码
- 对象头Consists of two words
- mark word:包含锁状态,哈希码,gc状态位
- klass pointer:指向描述对象布局和行为的元对象,包含一个C++的虚拟表
- HotSpot通过markOop类型实现Mark Word,具体实现位于markOop.hpp文件中。具体实现可以查看http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/oops/markOop.hpp
- 显示mark word的大小是64位,也就是8字节,所以klass pointer=12-8,也就是4字节
- klass在开启指针压缩的情况下是4字节,在不开启指针压缩的情况下是8字节
3. HashCode
- 计算对象的hashCode
- hashCode的值与mark word内打印的结果刚好相反,因为intel的cpu使用的是小端存储,数据的高字节存储在高地址中,数据的低字节存储在低地址中 ;低字节先输入,高字节后输出,打印结果刚好相反。
- 这里可以看出对象头内确实包含哈希码(前提是计算过hashcode)
4. 锁的状态
4.1 无锁不可偏向(有hashcode,001)
- mark word的前25位没有使用,第26位到第56位存储哈希值,第57位没有使用,第58位到第61位存储gc分代年龄,第62位存储是否可偏向标识,第63位到第64位存储锁状态。
- 计算对象的hashcode
- 此处gc分代年龄只占4位,也可以看出为什么分代年龄不能超过15,因为最大值是1111,对应十进制也就是15。
4.2 无锁可偏向(无hashcode,101),但是没有偏向(因为没加锁)
- 关闭偏向延迟-XX:BiasedLockingStartupDelay=0
- 不计算对象的hashcode
4.3 偏向锁已经偏向(101)
- 不计算对象hashcode,并加锁
- 在代码中大量使用锁的情况下,不要计算hashcode,可能会引起冲突
- 查看打印
- 如果这时候,计算了hashcode
- 查看打印
- 因为不可偏向,但是被加了锁,所以直接膨胀为轻量锁
4.4 轻量锁(00)
- 创建HeadLockTest
package org.example.test;
import org.example.entity.A;
import org.openjdk.jol.info.ClassLayout;
public class HeadLockTest {
static A a = new A();
static Thread t1;
static Thread t2;
/**
* 线程第一次加锁,或者是同一个线程再次加锁--偏向锁
* 交替执行--轻量锁
* 资源竞争---mutex重量锁
*/
public static void testLock(){
synchronized (a){
System.out.println("name:"+Thread.currentThread().getName());
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("线程还未启动----无锁");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
t1 = new Thread(){
@Override
public void run() {
testLock();
}
};
t2 = new Thread(){
@Override
public void run() {
testLock();
}
};
t1.setName("t1");
t1.start();
//阻塞,等待t1执行完后再启动t2
t1.join();
t2.setName("t2");
t2.start();
}
}
- 关闭偏向延迟-XX:BiasedLockingStartupDelay=0
- t1与t2交替执行
- 查看打印
4.5 重量锁(10)
-
注释掉join方法,让t1与t2竞争同一把锁
-
查看打印
-
因为存在资源竞争,所以是重量锁,底层的同步或者锁机制是mutex。
5. 结论
- 当一把锁第一次被线程持有是偏向锁,如果这个线程再次加锁还是偏向锁
- 如果其他线程来加锁(交替执行),则膨胀为轻量锁
- 如果发生资源竞争,则膨胀为重量锁
- 过程不可逆(99%)
6. 偏向延迟
- jdk6默认开启偏向锁,但是有4秒的延迟,所以启动后4秒内不会产生偏向锁;
- 因为jvm内部使用了大量的锁,而jvm认为自己不会有偏向锁,至少都是轻量锁,为了减少偏向撤销带来的性能损耗,设置了4秒内不产生偏向锁,4秒后jvm已经启动成功,又不妨碍用户使用偏向锁。
- 偏向锁设置参数
//关闭延迟开启偏向锁
-XX:BiasedLockingStartupDelay=0
//禁止偏向锁
-XX:-UseBiasedLocking
//启用偏向锁
-XX:+UseBiasedLocking