Java对象结构与内置锁
Java内置锁的很多重要信息都存放在对象结构中
1.Java对象结构
Java对象包括三部分:对象头、实例数据和对齐字节
-
对象头:
- 第一个字段叫做Mark Word,用于存储自身运行时数据,例如GC标志位,哈希码,锁状态信息
- 第二个字段叫做Class Pointer(类对象指针),用于存放方法区Class对象的地址,虚拟机通过这个指针来确定这个对象属于哪个类的实例
- 第三个字段叫做Array Length(数组长度)。如果队形是一个Java数组,那么此字段必须要有,用于记录数组长度的数据;如果对象不是一个数组,那么此字段不存在,所以这是一个可选字段
-
实例数据
实例数据存放的就是对象的实例数据,用于成员属性值,包括父类的成员属性值。
-
对齐填充
没有作用,只是为了填充长度
2.对象结构中核心字段的运用
- Mark Word字段主要用来表示对象的线程锁状态,另外可以用来配合GC存放该对象的hashCode
- Class Pointer字段是一个指向方法区的Class信息指针,意味着该对象随时可以知道自己是哪个对象的实例
- Array Length字段占用32位,这是可选的,只有当本对象是一个数组对象时才会有这个部分
- 对象体用于保存对象的属性值,是对象的主题部分,占用的内存空间大小取决于对象的属性数量和类型
- 对齐字节并不是必然存在的
3.对象结构中字段的长度
Java内置锁的状态共有四级:无锁、偏向锁、轻量级锁、重量级锁。在jdk1.6之前Java的内置锁只有一个重量级锁,但是在1.6之后,JVM为了提高锁的获取和释放效率,对synchronized的实现进行了优化,引入了偏向锁和轻量级锁,从此Java的内置锁就有了4种状态,并且会随着竞争的情况逐渐升级,而且是不可逆的过程,即不会降级,也就是说只能升级。
Mark Word字段的结构与Java内置锁的状态相关。为了让Mark Word字段存储更多的信息,JVM将Mark Word最低的两个位设置为Java的内置锁状态位,不同的锁状态下的32位Mark Word结构也不同
64位Mark Word的构成
- lock: 锁状态标记,占两个二进制位,由于希望用尽可能少的二进制位表示尽可能多的信息,因此设置了lock标记。该标记的值不同,整个Mark Word表示的含义就不同
- biased_lock :对象是否为偏向锁标记,只占一个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有启用偏向锁
lock和biased_lock两个标记位组合在一起共同表示Object实例处于什么样的锁状态
- age: 4位的Java对象分代年龄。
- identity_hashcode:31位的对象标识hashCode,采用延时加载技术,当调用Object.hashCode方法或者System.identityHashCode()方法计算对象的HashCode后,其结果将被写入到该对象的头中。当对象被锁定的时候,该值会移动到Moniteor中
- thread: 54位的线程Id
- epoch: 偏向时间戳
- ptr_to_lock_record: 62位,轻量级锁状态下指向栈帧中锁记录的指针
- ptr_to_heavyweight_monitor:62位,在重量级锁的状态下指向对象监视器的指针
4.无锁、偏向锁、轻量级锁、重量级锁
在jdk1.6版本之前,所有的Java内置锁都是重量级锁,重量级锁会造成CPU在用户态和核心态之间频繁切换,所以代价高、效率低。jdk1.6版本为了减少获得锁和释放锁所带来的性能消耗,引入了偏向锁和轻量级锁的实现。
-
无锁状态
Java对象刚创建的时候还没有任何线程来竞争,说明该对象时处于无锁状态,这时候偏向锁标识位是0,锁状态是01,无锁状态下的Mark Word如图所示:
-
偏向锁状态
偏向锁是指一段同步代码一直被同一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。如果内置锁处于偏向状态,当有一个线程来竞争锁的时候,先用偏向锁,标识内置锁偏爱这个线程,这个线程要执行该锁关联的同步代码时,不需要再做任何检查和切换。偏向锁在竞争不激烈的情况下效率非常高
偏向锁状态的Mark Word会记录内置锁自己偏爱的线程id,内置锁会将该线程当作自己熟悉的人
- 轻量级锁
当有两个线程开始竞争这个锁对象的时候,情况就发生了变化,不在是偏向哪个线程了,锁会升级为轻量级锁,两个锁公平竞争,哪个线程先占有锁对象,锁对对象的Mark Word就只想哪个线程栈帧中的锁记录。
当锁处于偏向锁,又被另一个线程企图抢占时,偏向锁就会升级成轻量级锁,企图强占的线程就会通过自旋的形式尝试获取锁,不会阻塞抢占线程,以便提高性能。
自旋的原理十分简单,如果持有锁的进程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要在内核态和用户态之间切换线程来进入阻塞状态,他们只需要等一等(自旋),等持有锁的线程释放掉锁后即可立即获取锁,这样就避免了用户线程和内核切换的消耗。