一 java性能调优工具箱
1.1操作系统的工具和分析
1.1.1 cpu使用率
1.top命令
可以按1,可以显示每个cpu的使用率
2.vmstat 1 命令,每隔一秒采样一次
说明1秒(1000毫秒)内,用户态用了6%,系统态用了3%,空闲86%
3.dstat 命令 ,是一个用来替换vmstat、iostat、netstat、nfsstat和ifstat这些命令的工具,是一个全能系统信息统计工具
4.sar
1.1.2 cpu运行队列
1.vmstat
数字是所有正在运行或待运行的线程数(即一旦有cpu就可以运行),如果试图运行的线程数超过了可用的cpu,性能就会下降;有时,瞬间数字会有提高,这对性能不会有实质的影响;但是,如果在相当长的时间内,运行队列很长,说明系统已经过载,这时你应该检查系统,减少机器正在处理的工作量。
1.1.3 磁盘使用率
监控磁盘使用率有两个目的:
- 第一个目的与应用本身有关,如果应用正在做大量的磁盘I/O操作,那I/O就很容易成为瓶颈
- 第二个目的,即使预计不会有很高的I/O,有助于监控系统是否在进行内存交互
1.vmstat 1
bi: 发送到块设备的块数,单位:块/秒
bo: 从块设备接收到的块数,单位:块/秒
2.dstat
3.iostat -xm 5,先安装 yum install sysstat
w_await :每次io写的时间,
%util: io使用率
4.sar -b 1
1.1.4 网络使用率
1.dstat 1, 查看每秒接收发送的量
2.watch ifstat 查看每个网卡的每秒的量
3.sar -n DEV 1 每1秒更新
2.java监控工具
- jcmd:打印java进程所涉及的基本类,线程,vm信息,参数
- jhat:读取内存堆转储,并有助于分析
- jmap:提供堆转储
- jinfo:查看jvm的系统属性,可以动态设置一些系统属性(mangerable)
- jstack:转储java线程信息
- jstat:提供gc和类装载的信息
- jvisualvm:监视jvm的gui工具
这些工具可以广泛用于以下领域
- 基本的jvm信息
- 线程信息
- 类信息
- 实时gc分析
- 堆转储的时候分析
- jvm性能分析
二 垃圾收集入门
2.1.垃圾收集概述
2.1.1分代垃圾收集器
垃圾收集调优目的:减少full gc的次数和间隔
很多时候我们没有机会重写代码,又面临需要提高java应用性能的压力,这种情况下对垃圾收集器的调优就变得至关重要。收集器种类:
- Serial收集器(常用于单cpu环境)
- throughput(或者parallel)收集器
- CMS收集器(concurrent)
- G1收集器(concurrent)
2.1.1.1 分代垃圾收集器
所有的垃圾收集器都遵循同一个方式,即根据情况将堆分成不同的代–老年代和新生代。
2.1.1.2 Minor GC
对象先在新生代中分配,新生代填满时,垃圾收集器会暂停所有的应用线程(STW),回收新生代空间,仍然在适用的对象被移动到其他地方(S或old),然后清空新新生代。这种操作被称为Minor GC;
2.1.1.3 full GC
对象不对被移到老年代,最终老年代也会被填满,jvm需要找出老年代中不再使用的对象,并对他们进行回收。而这便是垃圾收集器算法差异最大的地方。
简单的垃圾收集算法直接停掉所有的应用线程,找出不再使用的对象,对其进行回收,接着对堆空间进行整理,这个过程被称为full GC,通常导致应用程序线程长时间的停顿。
另一方面,通过更复杂的计算,我们还有可能在应用程序运行的同时找出不再适用的对象,CMS和G1收集器就是通过这种方式进行垃圾收集的。由于他们不需要停止应用线程就可以找出不再用使用的对象,CMS和G1收集器被称为Concurrent垃圾收集器。
适用CMS或G1收集器时,应用经历的停顿会更少(也更短),其代价是应用程序会消耗更多的cpu;CMS和G1也可能遭遇长时间的full GC停顿,这个就是调优垃圾收集的目的。
2.1.2 GC 算法
2.1.2.1 Serial 垃圾收集器
新生代收集:单线程,需要停顿
老年代收集:单线程,需要停顿
通过 -XX:UseSerialGC 开启
2.1.2.2 throughput 垃圾收集器
新生代收集:多线程,需要停顿
老年代收集:多线程,需要停顿
通过 -XX:UseParallelGC 或 -XX:UseParallelOldGC 开启
2.1.2.3 CMS 垃圾收集器
新生代收集:多线程,需要停顿
老年代收集:
CMS收集器在收集老年代时不再暂停应用程序,而是使用若干个后台线程定时对老年代空间进行扫描,及时回收不再使用的对象,并不进行碎片整理,只在后台线程扫描老年代时发生极其短暂的停顿。如果CMS无法获取所需的CPU资源或老年代过于碎片化无法找到空间分配对象,CMS就蜕变为Serial 收集器行为:暂停所有的应用线程,使用单线程回收,整理老年代空间。这之后有恢复到并发运行,再次启动后台线程。
通过 -XX:UseParNewGC 或 -XX:UseConcMarkSweepGC 开启
2.1.2.4 G1 垃圾收集器
新生代收集:多线程,需要停顿
老年代收集:
设计的目的,为了尽量缩短处理超大堆(大于4G)时时产生的停顿。G1收集器将堆划分为若干个区域,有的区域属于新生代,有的属于老年代。老年代的收集有后台线程完成,大多数的工作不需要暂停应用程。由于老年代被划分程不同的区域,G1收集器通过将对象从一个区域复制到另一个区域,完成对象的清理工作,意味着,正常的处理过程中,实现了堆的压缩处理(至少是部分整理)。
G1收集器比CMS收集器更不容易遭遇full GC
通过 -XX:UseG1GC 开启
2.1.3 选择GC收集器
- 堆小(小于或等于4G)推荐CMS收集器
- 大堆,推荐使用G1收集器
2.2.GC调优基础
2.2.1 调整堆的大小
-Xms2048m :设置初始大小
-Xmx2048m :设置最大大小
如果机子上只有一个jvm,堆的大小不要超过物理内存的一半;或者full GC后应该释放70%的控件,可以通过压测应用,做full GC,来测试预估。
静态变量也存放到堆中
2.2.2 代空间的调整
-XX:NewRatio=N :设置新生代与老年代的空间占用比率,默认值为2,即33%
-XX:NewSize=N :设置新生代的初始大小
-XX:MaxNewSize=N:设置新生代的最大大小
-XmnN :将NewSize 和MaxNewSize 设置为同一个值的快捷方法
2.2.3 永久代和元空间大小调整
-XX:MetaspaceSize=N和 -XX:MaxMetaspaceSize=N
2.2.4 控制并发
-XX:ParallelGCThreads=N
影响下面的线程数
- throughput 收集器的并发线程数
- CMS 收集器收集新生代的线程数
- G1 收集器收集新生代的线程数
- CMS 收集器 STW 阶段(但非full GC)
- G1 收集器 STW 阶段(但非full GC)
从上面可以看出基本都是stw时的线程控制,不控制CMS 和G1的后台线程
2.2.5 自适应调整
根据优化策略,jvm会不断尝试,寻找优化性能的机会,所以在jvm的运行过程中,堆,代,Survivor空间的大小都可能发生变化
关闭自适应的两种方法
- -XX:-UseAdaptiveSizePolicy 默认是启用的
- 堆的最大最小设置成一样,且新生代的最大最小设置成一样,自适应调整功能会被关闭
2.3.垃圾收集工具
2.3.1 开启日志
- 使用-verbose:gc或-XX:+PrintGC 开启,默认不开启
- -XX:+PrintGCDetails 创建更详细的GC日志
- -XX:+PrintGCTimeStamps 开启打印时间戳
- -XX:+PringGCDateStamps 开启打印时间
- -Xloggc:filename 修改输出到某个文件,默认输出到控制台
- -XX:+UseGCLogfileRotation 开启日志文件循环
- -XX:NumberOfGCLogfiles=N 设置GC文件个数,默认无限制
- -XX:GCLogfileSize=N 设置每个日志文件的大小,默认无限制
2.3.2 分析日志
打开GCeasy官网,上传GC日志 ,查看分析结果
具体分析结果解析,参考
https://blog.csdn.net/CoderBruis/article/details/101234738
2.3.3 在线查看gc情况
- visualvm 工具,生产上需要tomcat开启
- jstat -gcutil process_id 1000
三 垃圾收集算法
3.1 理解 Throughput收集器
3.1.1 使用多个线程收集新生代和老年代,需要停顿
3.1.2 堆自适应调整
-XX:MaxGCPauseMills=N :设定应用可以承受的最大停顿时间,设置的越小,堆就越小,不要调整此参数
XX:GCTimeRatio=N:设定你希望应用程序在垃圾回收上花费多少时间(与应用线程的运行时间相比较),默认99=99%,19=95%,默认设置已经很优了,一般不需要调整
3.1.3 和静态调整
静态设置堆的大小也可能获得最优的性能。设置合理的性能目标,让JVM根据设置确定堆的大小是学习这种调优很好的入门课程
3.2 理解 CMS 收集器
java8会收集元空间中不再载入的类,java7不会
3.2.1 新生代收集
和Throughput收集器一样
3.2.2 老年代收集
分不同的阶段,使用线程数为,使用整数计算方法(ParallelGCThreas取值区间1-4,那么就是1了)
- 初始标记阶段 ,需要停顿
- 标记阶段,不需要停顿
- 预清理阶段,不需要停顿
- 可中断预清理,其实就是等待一次新生代的gc,如果两次新生代的gc需要10秒,那么再等待5(一半)秒,再开始真正的标记阶段,防止新生代的中断和老年代的中断连续发生,造成长时间的停顿
- 二次标记阶段,需要停顿
- 清除阶段,不需要停顿
- 并发重置阶段,不需要停顿
3.2.3 CMS收集器可能遇到的问题
1 并发模式失效:新生代发生垃圾回收,同时老年代没有足够的空间容纳晋升的对象时,就会退化成单线程模式收集老年代,同时应用线程停顿,只是回收老年代无效的对象,并不进行碎片处理
2 晋升失败:老年代有足够的空间,但是由于空间碎片化,导致晋升失败,此时会发生full GC
3.2.4 并发模式失效的调优
避免并发模式失效是提醒CMS收集器处理能力,获得高性能的关键。
1 最简单的方法就是增大堆
2 否则,下一个步骤就是通过调整CMSInitiatingOccupancyFraction参数,尽早启动后台线程的运行
3 另外,调整后台线程的并发数目对解决这个问题也有帮助,ConcGCThreads 默认 = (3 + ParallelGCThreads)/ 4
3.3 理解 G1 收集器
G1名字的由来:先收集垃圾最多的分区
G1收集器主要有4种操作
- 新生代垃圾收集
- 后台收集,并发周期(找出垃圾最多的分区,打上标记X)
- 混合式垃圾收集(每次收集几个X,会有多次,完毕后,才会进行下一个并发周期)
- 必要时的full GC
3.3.1 后台收集,并发周期
- 初始标记阶段(initial-mark),停顿 (和新生代收集一起运行)
- 根分区扫描(concurrent-root-region-scan),不停顿
这个阶段不能发生新生代垃圾收集,因此预留足够的cpu给后台线程运行很重要,如果扫描根分区时,新生代刚好用完,新生代垃圾收集(会暂停所有的应用线程)必须等待跟扫描结束才能完成,意味着新生代垃圾收集时间会更长。这是一个信号,说明需要调优。
- 并发标记阶段(concurrent-mark),不停顿
- 重新标记(remark),停顿
- 清理阶段(cleanup),停顿
- 并发清理(concurrent-cleanup)
这之后,正常的并发周期就结束了,至少垃圾定位就完成了。清理阶段真正回收的垃圾很少,真正做的事情就是定位到了X分区。
3.3.2 混合式垃圾收集(mixed GC)
名字由来:不仅进行正常的新生代垃圾收集,同时也回收部分X分区
混合式垃圾收集会持续运行直到(几乎)所有标记的分区都被回收,这之后,新生代垃圾收集会恢复常规的新生代垃圾收集。最终,G1收集器会启动再一次的并发周期,决定哪一个分区应该在下一次的垃圾回收中释放。
3.3.3 触发full GC的情况
- 并发模式失效
发生的并发周期,并发周期还没完成,老年代满了。
意味着:堆的大小应该增加了,或者并发周期应该更早开始,或者增加并发周期的线程,让他运行的更快
- 晋升失败
发生的混合收集阶段,老年代在垃圾收集释放出足够的内存之前填满了
意味着: 混合式收集需要更迅速的完成;每次新生代垃圾收集需要处理更多的老年代分区
- 疏散失败
进行新生代垃圾收集,S和O没有足够的空间了
- 巨型对象分配失败
3.3.4 G1垃圾收集器调优
主要目标:避免发生并发模式失败或者疏散失败,一旦发生这些失败,就会发生full GC
调整方法:
- 调整-XX:MaxGCPauseMillis=N 默认200毫秒,优先使用,为了达到这个目标,jvm会自动调整其他很多东西
- 增大堆的大小,或者调整新生代和老年的比例
- 增加后台线程的数目(假定有足够的cpu)
对于应用线程暂停的运行周期,可以使用ParallelGCThreads来设置;对于并发阶段可以使用ConGCThreads来设置
- 以更高的频率进行G1的后台垃圾收集活动
-XX:InitialtingHeapOccupancyPercent=N,默认45,即,整个堆占用达到45%,就开始启动并发周期
- 调整在混合式垃圾收集周期
老年代的标记分区回收完成之前,G1收集器无法启动新的并发周期。因此,在混合式垃圾收集周期中尽量处理更多的分区(最终的混合式周期变少)可以让G1收集器更早启动标记周期
混合式垃圾收集的工作量取决于三个因素
1)一个分区有多少垃圾,就别认定为X,-XX:G1MixedGCLiveThresholdPercent=N,默认35%
2)最大混合式周期数,-XX:G1MixedGCCountTarget=N,默认8,减小该参数可以帮助解决晋升失败的问题(代价是混合式周期停顿的时间更长)
3)GC停顿可以忍受的最大时长 MaxGCPauseMillis 来设定,默认200毫秒
3.4 高级调优
3.4.1 晋升及S空间
新生代被划分为e,s0,s1的原因:这种布局让对象在新生代有更多的机会被回收,不再局限于只能晋升到老年代(最终填满老年代)
两种情况,对象会晋升到老年代:
- S空间实在太小,新生代垃圾收集时,如果S被填满,Eden剩下的活跃对象会直接晋升到老年代
- 对象S中经理的周期达到了上限
主要参数说明
- S空间的初始大小
-XX:InitialSurvivorRatio=N,默认是8,每个S大概10%的新生代的空间
- S空间的最大大小
-XX:MinSurvivorRatio=N,默认3,每个S空间的最大值位新生代的20%
- 自动调整S是根据参数来决定的-XX:TargetSurvivorRatio=N,默认是50,意味着:每次垃圾回收之后要能保证S有50%的空间是空闲的,否则将会调整S的大小
- 初始晋升阀值 -XX:InitialTenuringThreshold=N,默认T和G1为7,CMS为6
- 最大晋升阀值-XX:MaxTenuringThreshold=N,默认T和G1为15,CMS为6,JVM最终会在1和最大阀值之间选择一个合适的值
3.4.2 分析晋升日志
打开-XX:+PrintTenuringDistribution 默认为false,可以在gc日志中增加晋升日志,查看gc日志时,最重要的是观察Minor GC 中是否存在由于S过小,对象直接晋升到O的情况,我们要尽量避免这种情况,如果大量短期的对象最终填满老年代,会导致频繁的full GC。
如果有这种情况,解决方法:推荐流程是,增大堆的大小(或者至少增大新生代),同时减少对象的存活率。总之:存货时间不长的对象,不需要晋升到老年代。
3.4.3 分配大对象
TLAB:线程本地分配缓冲区。使对象分配更快成为可能。“大型”是个相对的概念,它取决于TLAB。G1收集器对超大型对象还有额外的考量,G1在收集巨型对象是效果非常显著。对TLAB的调整不常见,对G1的分区大小调整,很常见。
3.4.3.1 TLAB
开启-XX:+PrintTLAB 日志,每次新生代垃圾收集时,GC日志中同时包含了两种类型的信息:每个线程有一行描述该线程的TLAB的使用情况,以及一行摘要信息,描述TLAB整体的使用情况。
在每个线程日志中,如果发现,线程有大量的对象分配发生在TLAB之外,就应该考虑对TLAB进行调整。
3.4.3.2 调整TLAB大小
-XX:TLABSize=N 初始大小,在每次GC时可能调整
-XX:-ResizeTLAB 防止自动调整
3.4.3.3 巨型对象
如果无法在Eden中分配,则直接在Old中分配,如果它是一个短暂的对象,则会对垃圾收集造成负面影响。对于这种情况,除非修改应用程序,放弃使用短暂的巨型对象,否则别无他法。
如果对象超过了G1的分区,也会直接分配到Old
3.4.3.4 G1分区的大小
- G1分区的大小,是在启动时动态算出的;最小1M,最大32M
- -XX:G1HeapRegionSize=N设置G1分区大小,
1)只有在处理巨型对象时需要
2)堆的最小值和最大值设置的差别很大时,否则开始设置的分区大小为1M,运行过程中堆增大很多,则,分去就会太多 ;最好分区的个数接近2048个
3.4.3.5 使用G1分配巨型对象
- 超过分区大小一般,就被认为是巨型对象,会直接从old中找连续的空闲分区(很可能发生full GC)
- 巨型对象只能在G1并发周期中回收,好消息时,巨型对象回收会更加迅速,因为他是所在分区的唯一对象。巨型对象会在并发周期的清理阶段被回收释放。
- 增大分区的大小,使其能够在一个分区中分配所需的所有对象,可以提升性能。
- 如果分配巨型对象时发生full GC,首先要确定导致问题的巨型对象的大小,接下来看是否可以修改程序减小该对象;如果不能,就要调整分区的大小了。比如巨型对象是524,304字节,G1分区至少是1.1M,由于G1收集算法中,分区的大小总是2的冪,所以G1的分区大小是2M,才能保持在标准的G1分区中分配这些对象。
四 堆内存最佳实践
4.1 堆分析
4.1.1 堆直方图
- jcmd 18135 GC.class_histogram > class.txt
- jmap -histo 18135 > ss.txt 包含会被回收的对象,或者jmap -histo:live 18135 > ss.txt 先执行一次full GC
4.1.1 堆转储
- jcmd 18135 GC.heap_dump ./dump.hprof 执行前先full GC,如果不需要加上 -all
- jmap -dump:live,file=./dump2.hprof 18135
- jmap -heap 18135 控制台直接查看堆的统计信息
打开工具
1.jhat
2.jvisualvm
3.mat
4.1.2 内存溢出错误
-
原生内存不足
-
元空间内存不足
-
堆内存不足
-
达到GC的开销限制
-
自动转储
4.2 减少内存使用
4.2.1 减少对象大小
- 对象头(mark word+klass word)
1)普通对象32位上位8字节,64位为16字节,64位开启指针压缩( -XX:+UseCompressedOops默认启用)的情况下是12字节
2)数组对象32位以及堆小于64G的64位JVM上为16字节,其他为64字节
- reference:在32位以及堆小于64G的64位JVM上为4字节;在启用大堆的64位JVM为8
- 对象会长度会对齐为8的倍数
4.2.2 不可变对象或标准化对象
JVM内最好只有一个FLASE 和一个TRUE
4.2.3 字符串的保留
String自己提供了标准化方法String.intern() ,如果有大量的重复的字符串,使用就很有效果;应该注意,保留字符串的表是在原生内存中的,他是一个大小固定的Hashtable,java8中大小为60013
4.3 对象生命周期管理
有些情况下,正常的生命周期不是最优的。有些对象的创建成本很高,而管理这些对象的生命周期可以改进应用的效率,即便以让垃圾收集器多做些工作为代价。本届将探索,正常的生命周期何时应该有所改变,以及如何改变,手段可以是重用对象,或者是维护指向这些对象的特殊引用。
4.3.1 对象重用
- 一般有两种实现方式:对象池和线程局部变量。
- 这种技术适用于初始化成本很高,数量又很少的对象
- 线程和可重用对象之间存在一一对应关系,可以使用线程局部变量
4.3.2 软引用,弱引用和其他引用
- 软引用:如果一个对象在以后还可能被使用,使用软引用
软引用本质上是一个最近最久未用(LRU)的对象池。这些对象超过一定的时长,就会被回收。这个时长和gc后的空闲堆大小及-XX:SoftRefLRUPolicyMSPerMB=N有关,空闲堆越大,时长越长。
- 弱引用:如果一个对象同时被多个线程使用,使用软引用
比如,A用户的session中有个查询结果的对象,B也感兴趣,则可以使用弱引用,B就可以找到这个对象,前提是A没有登出(session没有销毁),如果没有普通引用指向这个对象,对象就会被回收
非确定引用自身会消耗内存,而且长时间抓住其他对象的内存,应该谨慎使用。个人认为现在有很多缓存技术,比如spring redis,基本上不会再使用非确定引用了
四 原生内存最佳实践
4.1 内存占用
4.1.1 内存占用最小化
- 堆
- 线程栈(堆外,引用的对象当然在堆内)
- 代码缓存
- 直接字节缓冲区,可以通过-XX:MaxDirectMemorySize=N限制,默认不限制
4.1.2 原生内存追踪(NMT)
借助-XX:NativeMemoryTracking=off|summary|detail 开启跟踪,默认是关闭的。对大多应用而言,NMT的概要模式已经足够了,它支持我们确定JVM提交了多少内存(以及这些内存干什么用了)
- jcmd 18135 VM.native_memory 命令可以查看
- NMT跟踪,检查JVM内存占用随时间变化的情况
jcmd 11348 VM.native_memory baseline 建立基线
jcmd 11348 VM.native_memory summary.diff 当前和基线做比较
4.2 针对不同操作系统优化JVM
4.2.1 linux上配置开启大页
使用大页通常可以明显提升应用的性能, 在大多数操作系统,必须显示开启使用大页。
- 传统大页,需要使用UseLargePages 开启,并且需要配置操作系统支持,可以参考原书8.2.1节
- 透明大页,在内核2.6.32版本开始支持透明大页
4.2.2 压缩的oop
为什么在堆小于32G的64位操作系统上,java引用的大小是4字节呢?因为默认启用了压缩的oop;使用的堆大小最好不要在32G-38G之间。
五 线程与同步的性能
5.1 线程池与ThreadPoolExecutor
所有的线程池本质都是一样的:有一个队列,任务被提交到这个队列中。
- ThreadPoolExecutor + SynchronousQueue
如果所有的线程都在忙碌,则启动一个新线程,直到最大值;如果已经到了最大值,并且都在忙碌,则新来的任务直接拒绝
- ThreadPoolExecutor + LInkedBlockedingQueue (无界队列)
线程的最大值直接被忽略,永远只有最小线程数
- ThreadPoolExecutor + ArrayBlockingQueue (有界队列)
队列被填满,又来了一个新任务,才会启动新线程(直到最大)
5.2 线程同步
5.2.1 同步与可伸缩性
如果在码没有同步的需求,原来是1个CPU,现在增加到8个CPU,我们可能会希望速度提升8倍。但是在有20%代码需要串行化执行时,速度仅仅提升3.33倍
5.2.2 锁定对象的开销
5.2.2.1 获取同步锁的成本
- 非竞争的synchronized锁(非膨胀锁)开销很小
- 竞争的synchronized锁(膨胀锁)开销是固定的
- 非竞争的cas锁开销更小
- 竞争的cas锁开销,会随着线程数的增加而增大
5.2.2.2 第二个开销,依赖于JMM
- 当一个线程离开同步块时,必须将任何修改的值刷新到主内存
- 基于cas的保护,确保操作期间修改的变量被刷新到主内存
- 标记为volatile的变量,无论什么时候被修改,总会在主内存中更新。
5.2.3 避免同步
- 线程局部变量,例如ThreadLocalRandom
- 使用基于CAS的替代方案
5.2.4 伪共享
比如有下面这个类
一个线程在不停的修改l1,另一个线程在不停的修改l2,会发现很慢。原因是:cpu加载对象数据时会加载一块数据到高速缓存,线程1把l1和l2加载进去了,线程2也把l1和l2加载进去了,线程1修改了l1,就要通知线程2的cpu的数据失效,线程2需要重新从主内存中读取数据。
这个就是伪共享:还没有使用共享呢,性能已经是使用共享的效果了。
5.3 线程调优
5.3.1 调节线程栈大小
-Xss=N 来修改
5.3.2 偏向锁
偏向锁的理论依据:如果一个线程最近用到了某个锁,那么线程下一次执行由同一把锁保护的代码所需的数据可能仍然在处理器的缓存中。如果给这个线程优先获取这把锁的权利,缓存命中率可能就会增加。
5.3.1 自旋锁
在处理锁的竞争问题时,jvm有两种选择
- 让线程进入忙循环,执行一些指令(这就是自旋),然后再次检查这个锁
- 把线程放入一个队列,在锁可用时通知他
JVM会在这两种情况间寻求合理的平衡,自动调整将线程移交到待通知队列之前的自旋时间。java8已经没有参数可以控制了。
六 参考