引言:Object obj = new Object(); 理解?
首先,这个对象肯定是存在jvm的堆内存的。然后谈谈它的对象布局。
内存布局
对象头、实例数据、对齐填充(保证8个字节的倍数)。

对象头
对象头又分为Mark Word和Class Pointer(类型指针),如果是数组对象,还有一个保存数组长度的空间Length。
Mark Word默认存储对象的HashCode、分代年龄和锁标志位等信息。这些信息都是与对象自身定义无关的数据,所以MarkWord被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间MarkWord里存储的数据会随着锁标志位的变化而变化。 Class Pointer(类型指针)指向它的类元数据,虚拟机通过这个指针来确定这个对象是哪个类的实例。在64位系统中,Mark Word占了8个字节,类型指针占了8个字节,所以对象头一共是16个字节,上面的空object对象默认就是16个字节。注意:Mark Word在不同的锁状态下存储的内容不同。
在64位JVM中是这么存的

虽然它们在不同位数的JVM中长度不一样,但是基本组成内容是一致的。
- 锁标志位(lock):区分锁状态,11时表示对象待GC回收状态, 只有最后2位锁标识(11)有效。
- biased_lock:是否偏向锁,由于无锁和偏向锁的锁标识都是 01,没办法区分,这里引入一位的偏向锁标识位。
- 分代年龄(age):表示对象被GC的次数,当该次数到达阈值的时候,对象就会转移到老年代。
- 对象的hashcode(hash):运行期间调用System.identityHashCode()来计算,延迟计算,并把结果赋值到这里。当对象加锁后,计算的结果31位不够表示,在偏向锁,轻量锁,重量锁,hashcode会被转移到Monitor中。
- 偏向锁的线程ID(JavaThread):偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的ID。 在后面的操作中,就无需再进行尝试获取锁的动作。
- epoch:偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁。
- ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。当锁获取是无竞争的时,JVM使用原子操作而不是OS互斥。这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM通过CAS操作在对象的标题字中设置指向锁记录的指针。
- ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到Monitor以管理等待的线程。在重量级锁定的情况下,JVM在对象的ptr_to_heavyweight_monitor设置指向Monitor的指针。
实例数据
对象的所有成员变量其中包含父类的成员变量和本类的成员变量,也就是说,除去静态变量和常量值放在方法区,非静态变量的值是随着对象存储在堆中的。按照4字节对齐。
对齐填充
因为虚拟机要求对象字节必须是8字节的整数倍,填充字符就是用于凑齐这个整数倍的。也就是说对象的大小必须是 8 字节的整数倍。对象头部分是 8 字节的倍数,所以当对象实例数据部分没有对齐时,就需要通过对齐填充来补全(非必须)。

可以使用jol测试。这篇博客的博主做过认真的测试可以参考:https://blog.csdn.net/sunshinezx8023/article/details/107356407
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.8</version>
</dependency>
Synchronized与锁升级
为什么每一个对象都可以成为一个锁
因为在Java的设计中 ,每一个Java对象生成就带了一把看不见的锁,它叫做内部锁或者Monitor锁,Monitor的本质是依赖于底层操作系统的Mutex Lock实现。Java中的每个对象都派生自Object类,而每个Java Object在JVM内部都有一个native的C++对象 oop/oopDesc进行对应。对象的监视器(monitor)锁对象由ObjectMonitor对象实现(C++),其跟同步相关的数据结构如下:
ObjectMonitor() {
_count = 0; //用来记录该对象被线程获取锁的次数
_waiters = 0;
_recursions = 0; //锁的重入次数
_owner = NULL; //指向持有ObjectMonitor对象的线程
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
}
看得懂就看对应的底层源码:https://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/89fb452b3688/src/share/vm/oops/markOop.hpp
锁升级原理
锁的升级步骤:无锁->偏向锁->轻量级锁->重量级锁,其中偏向锁可以通过配置关闭。锁升级功能主要依赖MarkWord中锁标志位和释放偏向锁标志位。还是看下面这幅图:

升级条件和过程
对象默认是无锁,锁标志位为01,偏向锁标示为0。在不存在锁竞争的时候线程进入同步方法或者同步代码块的时候就升级为偏向锁,此时会在对象头的Mark Word中记录下线程id(占用了54个字节),设置偏向锁标志位为1。因为偏向锁不会主动释放锁,所以以后线程再次获取锁的时候会比较线程id和对象头Mark Word中保存的线程id是否一致,如果一致,那么无需每次加锁解锁都去CAS更新对象头,性能极高,如果不一致,说明有第二个线程来竞争锁,此时又分为两种情况,如果对象头中的线程没有存活,则将对象头设置成无锁状态并撤销偏向锁,重新偏向 。如果对象头中的线程还存活,那么就撤销偏向锁,升级为轻量级锁,此时锁依然由原偏向锁的线程持有,另一个线程会进入自旋等待,如果自旋超过一定的次数(1.6以后自适应的)或者有第三个线程来竞争时会升级为重量级锁。
偏向锁
偏向锁是指当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争时,那么该线程在后续访问时不用通过CAS来获取锁,从而降低获取锁带来的消耗,即提高性能。偏向锁只适用于只有一个线程的情况。
偏向锁的撤销
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的,并且需要等到全局安全点的时候才能释放。

轻量级锁
轻量级锁就是自旋锁,适用于两个线程的情况。
重量级锁
重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。
锁的优缺点对比
| 锁 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块的场景,即没有线程竞争锁的同步场景 |
| 轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终竞争不到锁的线程,使用自旋会消耗CPU | 1.追求响应速度;2.少量线程竞争锁对象,同步块执行速度非常快(线程持有锁的时间不长) |
| 重量级锁 | 线程竞争不会自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 1.追求吞吐量;2.大量线程竞争锁对象,同步块执行速度较长(线程持有锁的时间较长) |
锁消除
Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,经过逃逸分析,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间。
public class LockElisionDemo {
static Object objectLock = new Object();//正常的
public void m1() {
//锁消除,JIT会无视它,synchronized(对象锁)不存在了。不正常的
Object o = new Object();
synchronized (o) {
// 业务代码
}
}
}
锁粗化
按理来说,同步块的作用范围应该尽可能小,仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,缩短阻塞时间,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。
但是加锁解锁也需要消耗资源,如果存在一系列的连续加锁解锁操作,可能会导致不必要的性能损耗。锁粗化就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁,避免频繁的加锁解锁操作。
public class LockCoarseningDemo {
static Object objectLock = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (objectLock) {
System.out.println("11111");
}
synchronized (objectLock) {
System.out.println("22222");
}
synchronized (objectLock) {
System.out.println("33333");
}
}, "a").start();
new Thread(() -> {
synchronized (objectLock) {
System.out.println("44444");
}
synchronized (objectLock) {
System.out.println("55555");
}
synchronized (objectLock) {
System.out.println("66666");
}
}, "b").start();
}
}

180

被折叠的 条评论
为什么被折叠?



