Java对象在内存布局与锁升级
一. 对象内存构成
对象的组成组成
JVM 中,Java对象保存在堆中时,由以下三部分组成:
- 对象头(object header):包括了关于堆对象的布局、类型、GC状态、同步状态和标识哈希码的基本信息(12byte)。
对象头由三部分组成:
1,Mark Word
2,指向类的指针(指向元空间)
3,数组长度(只有数组对象才有)
- 实例数据(Instance Data):主要是存放类的数据信息,父类的信息,对象字段属性信息。
- 对齐填充(Padding):为了字节对齐,填充的数据,不是必须的(对象的大小是2^3的整数倍,时间换空间)。
Object o = new Object();在内存中占多大内存?
在开启压缩的情况下,对象头(object header)占据12byte(96bit),其中 mark word占8byte(64bit),klass pointe 占4byte,另外剩余4byte是填充对齐的。对象的引用在栈内存占4byte,总共是16+4=20byte
对象
Mark Word在64位JVM中的存储
锁升级:
- 无锁状态:
当没有被使用时,这就是一个普通的对象,Mark Word记录对象的HashCode(调用HashCode方法时会写入,调用之前为空),锁标志位是01,是否偏向锁那一位是0
- 偏向锁
- 当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1,前54bit记录抢到锁的线程id,表示进入偏向锁状态。
- 当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,Mark Word中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码。
- 当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,但是Mark Word中的线程id记录的不是B,那么线程B会先用CAS操作试图获得锁,这里的获得锁操作是有可能成功的(比如线程A已经over了,线程一般不会自动释放偏向锁)。如果抢锁成功,就把Mark Word里的线程id改为线程B的id,代表线程B获得了这个偏向锁,可以执行同步锁代码。
- 偏向锁的释放:偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁状态的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销需要等待全局安全点(即没有字节码正在执行,STW),它会暂停拥有偏向锁的线程,撤销后偏向锁恢复到未锁定状态或轻量级锁状态。
- 轻量级锁
- 线程B在偏向锁状态抢锁失败,代表当前锁有一定的竞争,当到达全局安全点时线程A被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程A继续往下执行同步代码。
- JVM会在当前线程的线程栈中开辟一块名为Lock Record的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁Mark Word中保存指向这片空间的指针。上述两个保存操作都是CAS操作,如果保存成功,代表线程抢到了同步锁,就把Mark Word中的锁标志位改成00,可以执行同步锁代码。如果保存失败,表示抢锁失败,竞争太激烈。
- 自旋锁
- 轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。从JDK1.7开始,自旋锁默认启用,自旋次数由JVM决定。如果抢锁成功则执行同步锁代码。
- 如果锁的线程能在很短时间内释放资源,那么等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞状态,只需自旋,等持有锁的线程释放后即可立即获取锁,避免了用户线程和内核的切换消耗。
- 重量级锁
- 自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为10,Mark Word中存储重量级锁(互斥锁)的指针。在这个状态下,后面等待锁的线程也要进入阻塞状态。。
- 锁降级:只会发生在GC的额时候,这时候只有GC线程使用,没有讨论的意义
- 锁消除:只有一个线程在使用某个资源,JVM会消除这个资源的锁
- 锁粗化
while(i<1000){
a.show();
}
这时候JVM会将a的锁加到while上面
参考博客:
java对象头与synchronized锁的升级过程 - 七月流星丶 - 博客园 (cnblogs.com)