当前位置: 首页 > news >正文

JVM内置锁synchronized关键字详解

目录

JVM内置锁synchronized关键字详解

设计同步器的意义

如何解决线程并发安全问题?

synchronized原理详解

synchronized底层原理

synchronized在jdk1.6前后的变化【重点】

 jdk小于1.6时

jdk>=1.6时

轻量级锁何时升级为重量级锁??

锁膨胀是可逆的吗?

ReentrantLock

Monitor监视器锁

什么是monitor?

对象的内存布局

 对象头

对象头分析工具

System.out.println()方法的坑

锁的膨胀升级过程【synchronized优化的思路】【重点】

 偏向锁

轻量级锁

自旋

锁消除

锁粗化

分析:重量级锁和轻量级的总结?为什么加重量级锁(挂起阻塞线程)时消耗性能大??

逃逸分析

代码:

测试分析:

 原理分析:

总结

但是逃逸分析存在缺点:

锁膨胀举例【了解即可】

 举例1:



 JVM内置锁synchronized关键字详解

设计同步器的意义

多线程编程中,有可能会出现多个线程同时进行访问同一个共享,可变的资源的情况,这个资源我们称之为临界资源,这种资源可能是:对象,变量,文件等。

        共享:资源可以由多个线程同时访问

        可变:资源可以在其生命周期内被修改

引出的问题:

由于线程执行的过程是不可控的,所以需要采用同步机制来协同对对象可变状态的访问!

如何解决线程并发安全问题?

        实际上,所有的并发模式在解决线程安全问题时,采用的方案都是序列化访问临界资源。即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问。

Java 中,提供了两种方式来实现同步互斥访问:synchronized 和 Lock

同步器的本质就是加锁

加锁的目的:序列化访问临界资源,即同一时刻只能有一个线程访问临界资源(同步互斥访问)

不过有一点需要区别的是:当多个线程执行一个方法时,该方法内部的局部变量并不是临界资源,因为这些局部变量是在每个线程的私有栈中的,因此不具有共享性,不会导致线程安全问题。

synchronized原理详解

synchronized(JVM内置锁)是一种对象锁(锁的是对象而非引用),作用粒度是对象,可以用来实现对临界资源的同步互斥访问,是可重入的。

加锁的方式:

1.同步实例方法,锁是当前实例对象

2.同步类方法,锁是当前类对象

3.同步代码块,锁是括号里面的对象

synchronized底层原理

synchronized是基于JVM内置锁实现的,通过内部对象Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁)实现,它是一个重量级锁性能较低。当然,JVM内置锁在1.5之后版本做了重大的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销,,内置锁的并发性能已经基本与Lock持平。

synchronized关键字被编译成字节码后会被翻译成monitorenter 和 monitorexit 两条指令分别在同步块逻辑代码的起始位置与结束位置。

如图演示:

 

每个同步对象都有一个自己的Monitor(监视器锁),加锁过程如下图所示:

synchronized在jdk1.6前后的变化【重点】

 jdk小于1.6时

synchronized的性能十分的低,因为底层是由操作系统去维护的。

操作系统的内存空间分为用户空间和内核空间,内存空间分为两类:内核线程模型和用户线程模型。对于Java程序来说,它是属于内核线程模型。

内核线程模型的线程表是存在于内核空间的[线程表存储着CPU与JVM线程栈一一调度的映射关系,与其说是CPU调度Java栈的线程,不如说是操作系统底层的线程表进行调度Java线程栈的线程]。由于一开始是处于用户空间的,所以操作时需要从用户态切换到内核态(即是从用户空间切换到内核空间)。

我们知道切换的效率是很低的,大量消耗性能。所以在jdk1.6之前,synchronized底层由操作系统维护是性能极低的。

jdk>=1.6时

synchronized进行优化,分析过程:

(1) 当只有一个线程获取锁时,此时是不存在锁竞争的,所以无需加重量级锁一棒子打死,而是进行加一个偏向锁。

(2)当多个线程进入竞争获取锁时,此时也不会立马让synchronized分配一个重量级锁,而是先分配一个轻量级锁。

一个线程先获取到该锁对象 然后进行执行代码块中的任务逻辑。其它线程也会进入synchronized内部,但是会类似给其它线程一个while循环 让其它没有获取到锁对象的线程进行循环等待【spin自旋】当获取到轻量级锁的线程执行完任务逻辑后,会进行释放锁,其它线程停止循环等待,然后去竞争这把锁,线程无需阻塞,也无需线程上下文的切换。

(3) 但是如果获取到锁的线程执行代码块中业务逻辑时间过长或者锁竞争太激烈了(线程过多,单一线程获取锁的概率降低),导致其它线程循环等待的次数大于synchronized内部规定的数值时,此时就会发生锁膨胀,升级轻量级锁为重量级锁。此时其它没 有获取到锁的线程就不再循环等待了【spin自旋】,而是变为阻塞等待。这样性能就会极具下降!

轻量级锁何时升级为重量级锁??

其实就是对比两部分的性能消耗。

(1) 上轻量级锁,没有获取到锁的线程进行循环等待时所消耗CPU的性能

(2)上重量级锁,没有获取到锁的线程会阻塞,当锁释放后,线程去唤醒阻塞的线程所消耗的性能+完成操作系统底层空间的切换(用户空间切换到内核空间)所消耗的性能

如果(1)大于(2),那么轻量级锁升级为重量级锁。

如果(1)小于(2),那么使用轻量级锁。

锁膨胀是可逆的吗?

不是。锁膨胀就是轻量级锁膨胀转化为重量级锁,锁膨胀的原因就是线程竞争锁激烈或任务执行调度时间过长导致循环等待时间过长(等待时间过长则会导致CPU性能消耗过大),如果锁膨胀可逆,那么肯定会有一天再次发生锁膨胀,并且锁转化的过程也是极其消耗性能的。锁膨胀可逆的观点是错误的。

ReentrantLock

 总结:

ReentrantLock锁的性能和jdk1.6之后优化过的synchronized性能差不多。主要还是看怎么去使用。

Monitor监视器锁

任何一个对象都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。Synchronized在JVM里的实现都是 基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter和MonitorExit指令来实现。

  • monitorenter:每个对象都是一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
    1. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者;
    2. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;
    3. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;
  • monitorexit:执行monitorexit的线程必须是objectref所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权,当然退出monitor的线程也可以进行重新竞争该monitor的使用权。

monitorexit,指令出现了两次,第1次为同步正常退出释放锁;第2次为发生异步退出释放锁;monitorexit第二次退出释放锁,是一个健壮性退出。

通过上面两段描述,我们应该能很清除的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成的,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能进行调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

看一个同步方法:

public class SynchronizedMethod {public synchronized void method() {System.out.println("Hello World!");}
}

反编译结果:

      从编译的结果来看,方法的同步并没有通过指令 monitorenter 和 monitorexit 来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了 ACC_SYNCHRONIZED 标示符。

JVM就是根据该标示符来实现方法的同步的:

        当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。

        两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。

什么是monitor?

        可以把它理解为 一个同步工具,也可以描述为 一种同步机制,它通常被 描述为一个对象。与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。也就是通常说Synchronized的对象锁,MarkWord锁标识位为10,其中指针指向的是Monitor对象的起始地址。在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的):

ObjectMonitor() {_header       = NULL;_count        = 0; // 记录个数_waiters      = 0,_recursions   = 0;_object       = NULL;_owner        = NULL;_WaitSet      = NULL; // 处于wait状态的线程,会被加入到_WaitSet_WaitSetLock  = 0 ;_Responsible  = NULL ;_succ         = NULL ;_cxq          = NULL ;FreeNext      = NULL ;_EntryList    = NULL ; // 处于等待锁block状态的线程,会被加入到该列表_SpinFreq     = 0 ;_SpinClock    = 0 ;OwnerIsThread = 0 ;}

ObjectMonitor中有两个队列,_WaitSet和_EntryList,用来进行保存ObjectWaiter对象列表(每个等待锁的线程都会被封装为ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时:

1.首先会进入_EntryList集合,当线程获取到对象的monitor后,进入_Owner区域并且把monitor中的owner变量设置为当前线程,同时把monitor中的计数器count 加 1

2.若线程调用wait()方法,将释放当前持有的monitor,owner恢复为null,count自减1,同时该线程进入WaitSet集合中等待被唤醒

3.若当前线程执行完毕,也将释放monitor(锁)并复位count的值,以便其他线程进入获取monitor(锁);

同时,Monitor对象存在于每一个Java对象的对象头Mark Word中(存储的指针的指向),Synchronized锁便是通过这种方式获取锁的,也就是为什么Java中任意对象可以作为锁的原因,同时notify/notifyAll/wait等方法会使用到Monitor锁对象,所以必须在同步代码块中使用。监视器Monitor有两种同步方式:互斥与协作。

多线程环境下线程之间如果需要共享数据,需要解决互斥访问数据的问题,监视器Monitor可以确保监视器Monitor上的数据在同一时刻只会有一个线程在访问。

那么有个问题来了,我们知道synchronized加锁加在对象上,对象是如何记录锁状态的呢?

答案是锁状态是被记录在每个对象的对象头(Mark Word)中,下面我们一起认识一下对象的内存布局

对象的内存布局

HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

  • 对象头:比如 hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象)等。Java对象头一般占有2个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit,在64位虚拟机中,1个机器码是8个字节,也就是64bit),但是 如果对象是数组类型,则需要3个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。
  • 实例数据:存放类的属性数据信息,包括父类的属性信息;
  • 对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐;

 对象头

HotSpot虚拟机的对象头包括两部分信息,第一部分是“Mark Word”,用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,它是实现轻量级锁和偏向锁的关键。,这部分数据的长度在32位和64位的虚拟机(暂 不考虑开启压缩指针的场景)中分别为32个和64个Bits,官方称它为“Mark Word”。对象需要存储的运行时数据很多,其实已经超出了32、64位Bitmap结构所能记录的限度,但是对象头信息是与对象自身定义的数据无关的额 外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。例如在32位的HotSpot虚拟机 中对象未被锁定的状态下,Mark Word的32个Bits空间中的25Bits用于存储对象哈希码(HashCode),4Bits用于存储对象分代年龄,2Bits用于存储锁标志位,1Bit固定为0,在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容如下表所示。

但是如果对象是数组类型,则需要三个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。

对象头信息是与对象自身定义的数据无关的额外存储成本,但是考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据,它会根据对象的状态复用自己的存储空间,也就是说,Mark Word会随着程序的运行发生变化。

变化状态如下:

32位虚拟机

锁状态

25bit

4bit

1bit

2bit

23bit

2bit

是否偏向锁(是否禁用偏向)

锁标志位

无锁态

对象的hashCode

分代年龄

0

01

轻量级锁

指向栈中锁记录的指针

00

重量级锁

指向Monitor的指针

10

GC标记

11

偏向锁

线程ID

Epoch

分代年龄

1

01

64位虚拟机:

 现在我们虚拟机基本是64位的,而64位的对象头有点浪费空间,JVM默认会开启指针压缩,所以基本上也是按32位的形式记录对象头的。

手动设置-XX:+UseCompressedOops

哪些信息会被压缩?

1.对象的全局静态变量(即类属性)

2.对象头信息:64位平台下,原生对象头大小为16字节,压缩后为12字节

3.对象的引用类型:64位平台下,引用类型本身大小为8字节,压缩后为4字节

4.对象数组类型:64位平台下,数组类型本身大小为24字节,压缩后16字节

在Scott oaks写的《java性能权威指南》第八章8.22节提到了当heap size堆内存大于32GB是用不了压缩指针的,对象引用会额外占用20%左右的堆空间,也就意味着要38GB的内存才相当于开启了指针压缩的32GB堆空间。

这是为什么呢?看下面引用中的红字(来自openjdk wiki:CompressedOops - CompressedOops - OpenJDK Wiki)。32bit最大寻址空间是4GB,开启了压缩指针之后呢,一个地址寻址不再是1byte,而是8byte,因为不管是32bit的机器还是64bit的机器,java对象都是8byte对齐的,而类是java中的基本单位,对应的堆内存中都是一个一个的对象。

对象头分析工具

运行时对象头锁状态分析工具JOL,他是OpenJDK开源工具包,引入下方maven依赖

<dependency><groupId>org.openjdk.jol</groupId><artifactId>jol-core</artifactId><version>0.10</version>
</dependency>

打印markword

System.out.println(ClassLayout.parseInstance(object).toPrintable());
object为我们的锁对象

System.out.println()方法的坑

底层源码:

 

 得出结论:多线程高并发环境下,调用System.out.println()会成为同步阻塞调用,性能极低,不建议使用。

锁的膨胀升级过程【synchronized优化的思路】【重点】

 锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。从JDK 1.6 中默认是开启偏向锁和轻量级锁的,可以通过-XX:-UseBiasedLocking来禁用偏向锁。下图为锁的升级全过程:

 偏向锁

偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。下面我们接着了解轻量级锁。

默认开启偏向锁开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0关闭偏向锁:-XX:-UseBiasedLocking

轻量级锁

倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合(线程自旋循环等待的时间较短的场景),如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

自旋

获取轻量级锁失败后,虚拟机为了避免线程真实的在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这种自旋锁的优化是基于在大多数情况下,线程持有锁执行业务逻辑的时间都不会太长,如果直接挂起阻塞操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态切换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。

锁消除

消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如下StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。锁消除的依据是逃逸分析的数据支持。

锁消除,前提是java必须运行在server模式(server模式会比client模式作更多的优化),同时必须开启逃逸分析

:-XX:+DoEscapeAnalysis 开启逃逸分析
-XX:+EliminateLocks 表示开启锁消除。

锁粗化

举个例子来说明锁粗化:

分析:重量级锁和轻量级的总结?为什么加重量级锁(挂起阻塞线程)时消耗性能大??

轻量级锁:

偏向锁失效后,我们并没有直接升级为重量级锁,而是变为轻量级锁。轻量级锁中,未获取到锁的线程会进行自旋循环等待,这个自旋循环等待的过程会消耗占用CPU的性能。但是假设线程竞争不激烈,这个自旋循环等待的过程就很短,那么消耗的CPU相对较低。但是如果线程竞争激烈时或业务逻辑执行时间较长,导致单一线程获取到锁的概率降低,自旋循环的次数超过额定范围,那么就会由轻量级锁转化为重量级锁。

重量级锁:

重量级锁就是把未获取到锁的线程进行阻塞挂起,加入到一个EntryList队列中等待其它线程去唤醒。从操作系统底层内存空间模型来看,Java程序是运行在用户空间的,安全系数为ring3,但是对于线程的调度我们必须切换到ring0级别的内核空间,因为内核空间才具有调用线程的线程表【对于操作系统内部的程序指令才会一开始就运行在ring0,安全系数较高。像第三方程序,java程序一开始都运行在ring0级别的用户空间,想要调用线程对应操作系统底层的库函数,就必须要切换到ring0级别的内核空间,切换的过程中,我们会进行安全校验】

所以当我们想唤醒一个阻塞的线程时,需要从用户态切换回内核态(用户空间切换回内核空间),因为内核态中维护了线程表,线程表中维护了与线程的一一映射的关系调度。CPU会去内核空间中的线程表进行调度线程地址映射,最终由内核空间进行调度操作系统底层库函数进行调用JVM线程栈空间进行操作调用。

总结:

我们知道,从用户态切换到内核态所消耗的性能是非常大的。这就是为什么我们一直不舍得直接转化为重量级锁,加重量级锁就好比直接把同步的任务交给操作系统底层了,操作系统底层只能根据切换状态来维护,是十分消耗性能的。但是如果线程竞争激烈,未获取到锁的线程自旋循环等待所消耗的CPU性能已经大于加重量级锁时切换状态消耗的CPU性能时,那么就需要从轻量级锁升级未重量级锁。

逃逸分析

代码:

/*** @Description: TODO* @Author: etcEriksen* @Date: 2023/3/4**/
public class TestScape {/*** 进行两种测试* 关闭逃逸分析,同时调大堆空间,避免堆内GC的发生,如果有GC信息将会被打印出来* VM运行参数:-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError** 开启逃逸分析* VM运行参数:-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError** 执行main方法后* jps 查看进程* jmap -histo 进程ID**/public static void main(String[] args) {long start = System.currentTimeMillis();for (int i = 0; i < 500000; i++) {alloc();}long end = System.currentTimeMillis();//查看执行时间System.out.println("cost-time " + (end - start) + " ms");try {Thread.sleep(100000);} catch (InterruptedException e1) {e1.printStackTrace();}}private static S alloc() {//Jit对编译时会对代码进行 逃逸分析//并不是所有对象存放在堆区,有的一部分存在线程栈空间//该创建的对象只在当前方法使用,并不发生逃逸,所以可以优化S student = new S ();return student;}static class S {private long id;private int age;}}

测试分析:

方案1.关闭逃逸分析,调大堆空间的内存大小(避免GC垃圾回收产生):

运行:

 通过命令行窗口查看:发现在堆空间创建了50000个S实例对象

方案2.开启逃逸分析

设置JVM参数

运行:

 通过命令行窗口查看:

 原理分析:

分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在堆内存,而是分解为标量存储在栈空间中。这样就避免了在堆空间中重复创建多个S实例对象,大大的节省了堆空间的内存

总结

使用逃逸分析,编译器可以对代码做如下优化:

一、同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。

二、将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。

三、分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。

是不是所有的对象和数组都会在堆内存分配空间?

不一定

在Java代码运行时,通过JVM参数可指定是否开启逃逸分析, -XX:+DoEscapeAnalysis : 表示开启逃逸分析 -XX:-DoEscapeAnalysis : 表示关闭逃逸分析。从jdk 1.7开始已经默认开启逃逸分析,如需关闭,需要指定-XX:-DoEscapeAnalysis

但是逃逸分析存在缺点:

1.技术不成熟

2.逃逸分析,性能消耗较大

锁膨胀举例【了解即可】

前置知识须知:

32位虚拟机(即使是64位,也会进行指针压缩,压缩为32位,减少占用的空间)

下图:从左往右:高位到低位

锁状态

25bit(最高位)

4bit

1bit

2bit(最低位)

23bit

2bit

是否偏向锁(是否禁用偏向)

锁标志位

无锁态

对象的hashCode

分代年龄

0

01

轻量级锁

指向栈中锁记录的指针

00

重量级锁

指向Monitor的指针

10

GC标记

11

偏向锁

线程ID

Epoch

分代年龄

1

01

打印输出的锁:恰好和上图二进制位对应相反

 举例1:

 为什么一上来,没线程竞争就直接加轻量级锁??

JVM启动的时候,内部会启动大概几十个类,并且会调用很多方法,你不得不怀疑这些方法存在着同步的嫌疑,所以得出一个结论:JVM启动时内部会有多线程竞争激烈导致偏向锁可能直接升级为轻量级锁。

为了避免这种情况,JVM搞出一个机制:延迟加载偏向锁,直接分配轻量级锁。这种做法是为了避免在JVM启动时由于线程竞争激烈导致锁升级所带来的性能消耗。

 测试:先休眠5秒再进行获取锁,可能JVM延迟加载的休眠时间到了

 结论:

(1) 当调用偏向锁对象的hashCode时,偏向锁升级为轻量级锁

因为偏向锁没有空间去存储哈希值。轻量级锁可以使用 

(2) 轻量级锁的哈希值存储在lock record(markword)中

(3)重量级锁的哈希值记录在Monitor里面

相关文章:

JVM内置锁synchronized关键字详解

目录 JVM内置锁synchronized关键字详解 设计同步器的意义 如何解决线程并发安全问题&#xff1f; synchronized原理详解 synchronized底层原理 synchronized在jdk1.6前后的变化【重点】 jdk小于1.6时 jdk>1.6时 轻量级锁何时升级为重量级锁&#xff1f;&#xff1f;…...

【2021.12.25】xv6系统入门学习

【2021.12.28】为xv6系统添加一个开机密码 文章目录【2021.12.28】为xv6系统添加一个开机密码0、说明1、Ubuntu20上安装xv62、测试指令3、修改系统代码4、添加自己的程序命令0、说明 xv6 是 MIT 设计的一个教学型操纵系统。 记录Ubuntu上安装x86版本的xv6系统&#xff0c;为其…...

Linux内核4.14版本——drm框架分析(4)——crtc分析

目录 1. struct drm_crtc结构体 2. crtc相关的API 2.1 drm_crtc_init_with_planes 2.5 drm_mode_setcrtc 3. func的一些介绍 3.1 struct drm_crtc_helper_funcs 3.2 struct drm_crtc_funcs 4. 应用层的调用 4.1 drmModeSetCrtc &#xff08;drmlib库里&#xff09;---…...

用原生js手写分页功能

分页功能如下&#xff1a; 数据分页显示&#xff0c;每页显示若干条数据&#xff0c;默认当前页码为第一页。例如&#xff1a;每页5条数据&#xff0c;则第一页显示 1-5 条&#xff0c;第二页显示 6-10 条&#xff0c;依此类推。当页码为第一页时&#xff0c;上一页为禁用状态…...

CornerNet介绍

CornerNet: Detecting Objects as Paired Keypoints ECCV 2018 Paper&#xff1a;https://arxiv.org/pdf/1808.01244v2.pdf Code&#xff1a;GitHub - princeton-vl/CornerNet 摘要&#xff1a; 提出了一种single-stage的目标检测算法CornerNet&#xff0c;它把每个目标检…...

【SpringBoot】日志使用

默认配置 Spring Boot默认帮我们配置好了日志 //记录器Logger logger LoggerFactory.getLogger(getClass());Testpublic void contextLoads() {//System.out.println();//日志的级别&#xff1b;//由低到高 trace<debug<info<warn<error//可以调整输出的日志级…...

关于slice扩容性能损耗的探究

背景 ​ 如果让我评选最伟大的数据结构&#xff0c;在我心中答案只有两个&#xff0c;数组和哈希表&#xff0c;这两个是我的程序的重要组成部分&#xff0c;同时也是我饭碗的重要组成部分。slice和map简洁明了的API很容易让我们有一种他们提供了无限大的空间&#xff0c;可以…...

Java实现单向链表

✅作者简介&#xff1a;热爱Java后端开发的一名学习者&#xff0c;大家可以跟我一起讨论各种问题喔。 &#x1f34e;个人主页&#xff1a;Hhzzy99 &#x1f34a;个人信条&#xff1a;坚持就是胜利&#xff01; &#x1f49e;当前专栏&#xff1a;Java数据结构与算法 &#x1f9…...

3月4日,30秒知全网,精选7个热点

///印度最大供电商罕见于现货市场购煤&#xff0c;能源供应短缺成忧 据知情人士透露&#xff0c;这家印度国有发电公司计划在下周左右发布300万吨的招标 ///QQ音乐推出AIGC黑胶播放器 这是国内音乐行业首个运用AI技术&#xff0c;通过文字、图片指令快速生成不同风格的播放器…...

EXCEL-职业版本(2)

Excel-职业版本&#xff08;2&#xff09; 定位 1.如何快速定位到不连续的空值&#xff0c;填充为0 1.在任意空单元格里复制0 2.选中数据区域CtrlA 3.CtrlG 4.选择【定位条件】 5.选择【空值】 6.ctrlV 粘贴 即可 2.怎么一次性计算每个小组的数量 单价和金额的和? 1.选中…...

java中延时队列的实现

大家好&#xff0c;我是一名CRUD工程师&#xff0c;最近我朋友突然来问我如何实现延时队列&#xff0c;我脱口而出就是MQ。不过突然想到公司的项目好像用的是java的一个原生类。于是我就想着趁周末的时间好好的去探究一下各方法实现延时队列的优缺点。 延迟消息 延迟消息就是字…...

基于java的circle buffer的实现

总目录链接==>> AutoSAR入门和实战系列总目录 文章目录 缓冲区示例什么是循环缓冲区?方法 1:使用数组插入元素删除元素方法 2:使用链表插入元素:删除元素:当数据经常从一个地方移动到另一个地方或从一个进程移动到另一个进程或被频繁访问时,它不能存储在永久性内存…...

通用方法——为什么重写equals还要重写hashcode

本文介绍java.lang.Object类中的两个方法&#xff1a;equals和hashCode。这两个方法大家应该都知道&#xff0c;但是这两个方法的作用是什么、为什么重写equals还要重写hashCode、它们之间有什么关系和约定等&#xff0c;今天就来带大家了解一下。 1、hashCode hashCode即散列…...

JavaSE学习进阶day2_01 包和权限修饰符

第一章 包 1.1 包 包在操作系统中其实就是一个文件夹。包是用来分门别类的管理技术&#xff0c;不同的技术类放在不同的包下&#xff0c;方便管理和维护。 在IDEA项目中&#xff0c;建包的操作如下&#xff1a; 这个咱们在基础班就谈到过。 包名的命名规范&#xff1a; 路径…...

Android性能调优 - 省电优化

省电&#xff1a;通过工具Battery Historian查看到:耗电大头: 屏幕、网络、cpuled/oled屏幕显示:降低亮度&#xff0c;开深色模式&#xff1b;锁屏间隔缩短到 &#xff1b;亮屏需要一直持有唤醒锁&#xff0c;还有gps定位也需要用到唤醒锁;网络&#xff1a; 常用的网络优化措施…...

ElasticSearch - SpringBoot整合ES之全文搜索匹配查询 match

文章目录1. 数据准备2. match 匹配查询1. 全文检索2. 简化查询DSL语句3. match 匹配查询原理官方文档地址&#xff1a;https://www.elastic.co/guide/en/elasticsearch/reference/index.html权威指南&#xff1a;https://www.elastic.co/guide/cn/elasticsearch/guide/current/…...

句子的改写和扩写

目录 1.句子改写 2.句子扩写 &#xff08;不低于15个句子算是长句子&#xff0c;不能太多长句子&#xff09; 1.句子改写 我绝不会嫁给你的。 如果你是世界上最后一个男人&#xff0c;我就去寺庙。 If you married me,I would jump into the well. 如果你嫁给我&#xff0c;我…...

DockerFile创建及案例

DockerFile dockerfile是用来构建docker镜像的文件&#xff0c;命令脚本参数脚本&#xff01; 构建步骤 编写一个dockerfile文件docker build 构建成为一个对象docker run 运行镜像docker push 发布镜像&#xff08;DockerHub、阿里云镜像仓库&#xff09; 去官网Docker-Hub…...

第十四届蓝桥杯三月真题刷题训练——第 1 天

目录 题目1&#xff1a;数列求值 代码&#xff1a; 题目2&#xff1a;质数 代码&#xff1a; 题目3&#xff1a;饮料换购 代码&#xff1a; 题目1&#xff1a;数列求值 题目描述 本题为填空题&#xff0c;只需要算出结果后&#xff0c;在代码中使用输出语句将所填结果输出…...

基于容器云提交spark job任务

容器云提交spark job任务 容器云提交KindJob类型的spark任务&#xff0c;首先需要申请具有Job任务提交权限的rbac&#xff0c;然后编写对应的yaml文件&#xff0c;通过spark内置的spark-submit命令&#xff0c;提交用户程序(jar包)到集群执行。 1、创建任务job提交权限rbac …...

Linux系统调用之目录操作函数

前言 如果&#xff0c;想要深入的学习Linux系统调用中mkdir&#xff0c;rmdir&#xff0c;rename&#xff0c;chdir&#xff0c;getcwd等这些有关于目录操作函数&#xff0c;还是需要去自己阅读Linux系统中的帮助文档。 具体输入命令&#xff1a; man 2 mkdir/rmdir/rename/ch…...

设计模式-策略模式

前言 作为一名合格的前端开发工程师&#xff0c;全面的掌握面向对象的设计思想非常重要&#xff0c;而“设计模式”是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的&#xff0c;代表了面向对象设计思想的最佳实践。正如《HeadFirst设计模式》中说的一句话&…...

面试+算法:罗马数字及Excel列名与数字互相转换

概述 算法是一个程序员的核心竞争力&#xff0c;也是面试最重要的考查环节。 试题 判断一个罗马数字是否有效 罗马数字包含七种字符&#xff1a;I&#xff0c;V&#xff0c;X&#xff0c;L&#xff0c;C&#xff0c;D和M&#xff0c;如下 字符数值I1V5X10L50C100D500M1000…...

Connext DDS路由服务Routing Service(1)

1 简介 RTI路由服务是一种开箱即用的解决方案,允许开发人员快速扩展和集成不同或地理位置分散的实时系统。它跨域、LAN和WAN扩展RTI ConnextDDS应用程序,包括防火墙和NAT穿越。 它还支持DDS到DDS的桥接,允许您对数据进行转换。这允许未修改的DDS应用程序进行通信,即使它们是…...

如何使用SaleSmartly进行Facebook Messenger 营销、销售和支持

如何使用SaleSmartly&#xff08;ss客服&#xff09;进行Facebook Messenger 营销、销售和支持上篇文章我们讲了什么是Facebook Messenger CRM以及获得Facebook Messenger CRM的注意事项&#xff0c;现在你有更多时间与客户聊天&#xff0c;让我们看看你如何使用SaleSmartly&am…...

教资教育知识与能力中学教学

目录 3.1 教学概述 3.2 教学过程 3.3 教学原则*【简答/辨析重点】 3.4 教学方法 3.5 教学组织形式 3.6 教学工作基本环节 3.7 教学评价 3.1 教学概述 1、教学的意义【14/18辨析】 教学是传授系统知识、促进学生发展的最有效形式&#xff1b; 教学是学校进行全面发展教…...

IDEA中使用Tomcat的两种方式:集成本地Tomcat使用Tomcat Maven插件

一、前言 在IDEA中创建完一个Maven Web项目&#xff0c;并补齐了目录以后&#xff0c;准备使用Tomcat时&#xff0c;就需要在自己创建的项目中去部署Tomcat&#xff0c;前文已经介绍了如何创建Maven Web&#xff0c;所以这里就不多加赘述&#xff0c;直接讲述部署Tomcat的方法…...

IP 地址的简介

IP 地址 Internet 依靠 TCP/IP 协议&#xff0c;在全球范围内实现不同硬件结构、不同操作系统、不同网络系统的主机之间的互联。在 Internet 上&#xff0c;每一个节点都依靠唯一的 IP 地址相互区分和相互联系&#xff0c;IP 地址用于标识互联网中的每台主机的身份&#xff0c…...

3D动作/动画特效

硕士/博士符合一本高校人才引进条件的硕士、博士&#xff0c;教研能力突出者可签合作高校正式编制本科/硕士成绩优异专业扎实、有创新思维者可在签约工作后在校继续读研读博【产业模式】数字经济→数字孪生→升级转型【细份领域】数字产业、数字工程、数字教研、数字政企【合作…...

python 多线程编程之_thread模块

_thread模块除了可以派生线程外&#xff0c;还提供了基本的同步数据结构&#xff0c;又称为锁对象&#xff08;lock object&#xff0c;也叫原语锁、简单锁、互斥锁、互斥和二进制信号量&#xff09;。 下面是常用的线程函数&#xff1a; 函数描述start_new_thread(function,…...