在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:
对象头(Header)
实例数据(Instance Data)
对齐填充(Padding)
对象头:HotSpot虚拟机对象的对象头包括三部分信息。
第一部分是用来存储对象自身的运行时数据。如哈希码(HashCode)、分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32bit和64bit的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为“Mark Word”。
Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位。以32bit的HotSpot虚拟机为例,如对象处于无锁状态下,各状态如下所示
在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。Mark Word可能变化为存储以下4种数据
在64位虚拟机下,Mark Word是64bit大小的,其存储结构如下所示
可以看出,在64位虚拟机中,一个对象头的长度最小为8字节(MarkWord)+ 4字节(类型指针,默认开启的指针压缩)= 12个字节。而对象的长度必须为8字节的整数倍,那么就必须还有4字节的填充数据,那么一个对象就最少有16个字节的长度。
实例数据:对象真正存储的有效信息
对齐填充:由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是
任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数(1倍或者
2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。
通过工具可以分析下对象的具体结构,依赖地址如下
<dependency>
<groupId>org.openjdk.jolgroupId>
<artifactId>jol-coreartifactId>
<version>0.12version>
dependency>
编写代码
public class T {
public static void main(String[] args) {
T obj = new T();
System.out.println(obj);
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
控制台打印结果
com.example.demo.test.T@7106e68e
com.example.demo.test.T object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 8e e6 06 (00000001 10001110 11100110 00000110) (115772929)
4 4 (object header) 71 00 00 00 (01110001 00000000 00000000 00000000) (113)
8 4 (object header) 05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
从打印结果可以看出该对象一共占有16字节(对象头占8字节+类型指针占4字节+填充数据占4字节)。
由于jdk1.8默认开启指针压缩,所以类型指针只占4字节,通过虚拟机参数(-XX:-UseCompressedOops)关闭指针压缩再看下效果
com.example.demo.test.T@7106e68e
com.example.demo.test.T object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 8e e6 06 (00000001 10001110 11100110 00000110) (115772929)
4 4 (object header) 71 00 00 00 (01110001 00000000 00000000 00000000) (113)
8 4 (object header) 28 30 c5 17 (00101000 00110000 11000101 00010111) (398798888)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
这个时候对象的结构只剩下对象头信息了,MarkWord还是8个字节,而类型指针变成了8字节。
为了看到实例数据,我们需要在对象中定义一些变量,修改代码如下
public class T {
public int m1 = 123;
public long m2 = 456L;
public String m3 = "789";
public static void main(String[] args) {
T obj = new T();
System.out.println(obj);
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
控制台打印结果
com.example.demo.test.T@7106e68e
com.example.demo.test.T object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 8e e6 06 (00000001 10001110 11100110 00000110) (115772929)
4 4 (object header) 71 00 00 00 (01110001 00000000 00000000 00000000) (113)
8 4 (object header) 05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
12 4 int T.m1 123
16 8 long T.m2 456
24 4 java.lang.String T.m3 (object)
28 4 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
可以发现,实例数据由m1、m2、m3组成,分别占用4字节、8字节、4字节。
代码中打印了该对象的hashCode为7106e68e,该对象的MarkWord信息如下(可以发现,这里的取值是倒过来的):
00000000 00000000 00000000 01110001 00000110 11100110 10001110 00000001
(00 00 00 71 06 e6 8e 01)
可以看到,最后的3bit(1bit标识偏向锁,2bit描述锁的类型)是跟锁相关的,而Synchronized的锁优化升级就是修改的这几位上的标识用来区分不同的锁,从而采取不同的策略来提升性能。
Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
HotSpo的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程;如果没有设置,则使用CAS竞争锁。
一旦出现另外一个线程去尝试获取这个锁的情况,偏向模式就马上宣告结束。根据锁对象目前是
否处于被锁定的状态决定是否撤销偏向(偏向模式设置为“0”),撤销后标志位恢复到未锁定(标志位
为“01”)或轻量级锁定(标志位为“00”)的状态。
偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0。
// 如有必要可以使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0
public class T {
public static void main(String[] args) throws InterruptedException {
System.out.println(ClassLayout.parseInstance(new T()).toPrintable());
//
Thread.sleep(5*1000);
System.out.println(ClassLayout.parseInstance(new T()).toPrintable());
}
}
运行结果如下,可以发现程序启动后睡眠5秒,对象的偏向锁就打开了。
com.example.demo.test.T object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
com.example.demo.test.T object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
(1)轻量级锁加锁线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
如果出现两条以上的线程争用同一个锁的情况,那轻量级锁就不再有效,必须要膨胀为重量级锁,锁标志的状态值变为“10”,此时Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也必须进入阻塞状态。
(2)轻量级锁解锁轻量级解锁时,会使用原子的CAS操作将Displaced MarkWord替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
当系统检查到锁是重量级锁之后,将想要获得锁的线程进行阻塞,被阻塞的线程不会消耗CPU。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。
/**
* 关闭偏向锁的延迟
* -XX:BiasedLockingStartupDelay=0
*/
public class T {
public static void main(String[] args) throws InterruptedException {
T obj = new T();
System.out.println("########## " + Thread.currentThread().getName() + " ##########");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
new Thread(() -> {
synchronized (obj) {
System.out.println("########## " + Thread.currentThread().getName() + " ##########");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
},"A线程").start();
new Thread(() -> {
synchronized (obj) {
System.out.println("########## " + Thread.currentThread().getName() + " ##########");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
},"B线程").start();
}
}
运行结果可能出现以下几种情况
偏向锁->偏向锁->轻量级锁
偏向锁->偏向锁->轻量级锁,这个情况需要多运行几次才会出现
########## main ##########
com.example.demo.test.T object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
########## A线程 ##########
com.example.demo.test.T object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 d8 29 19 (00000101 11011000 00101001 00011001) (422172677)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
########## B线程 ##########
com.example.demo.test.T object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 70 ef 1c 1a (01110000 11101111 00011100 00011010) (438103920)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
偏向锁->轻量级锁->重量级锁
偏向锁->轻量级锁->重量级,这个情况需要多运行几次才会出现
com.example.demo.test.T object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
########## A线程 ##########
com.example.demo.test.T object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 90 f0 d7 1a (10010000 11110000 11010111 00011010) (450359440)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
########## B线程 ##########
com.example.demo.test.T object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) ba 8c 74 03 (10111010 10001100 01110100 00000011) (57969850)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
偏向锁->偏向锁->重量级锁
########## main ##########
com.example.demo.test.T object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
########## A线程 ##########
com.example.demo.test.T object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 08 bb 19 (00000101 00001000 10111011 00011001) (431687685)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
########## B线程 ##########
com.example.demo.test.T object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 0a cb f1 17 (00001010 11001011 11110001 00010111) (401722122)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
偏向锁->重量级锁->重量级锁
########## main ##########
com.example.demo.test.T object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
########## A线程 ##########
com.example.demo.test.T object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 2a 8c 9e 03 (00101010 10001100 10011110 00000011) (60722218)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
########## B线程 ##########
com.example.demo.test.T object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 2a 8c 9e 03 (00101010 10001100 10011110 00000011) (60722218)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total