Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >JVM优化之逃逸分析及锁消除

JVM优化之逃逸分析及锁消除

作者头像
哲洛不闹
发布于 2019-08-14 08:05:25
发布于 2019-08-14 08:05:25
1.1K0
举报
文章被收录于专栏:java一日一条java一日一条

如果能确认某个加锁的对象不会逃逸出局部作用域,就可以进行锁删除。这意味着这个对象同时只可能被一个线程访问,因此也就没有必要防止其它线程对它进行访问了。这样的话这个锁就是可以删除的。这个便叫做锁消除,本文是JVM实现机制的系列文章,这也正是今天要讲的主题。

众所周知,java.lang.StringBuffer是一个使用同步方法的线程安全的类,它可以用来很好地诠释锁消除。StringBuffer是Java1.0的时候开始引入的,可以用来高效地拼接不可变的字符串对象。它对所有append方法都进行了同步操作,以确保当多个线程同时写入同一个StringBuffer对象的时候也能够保证构造中的字符串可以安全地创建出来。

很多程序其实并不需要这层线程安全保障,因此在Java 5中又引入了一个非同步的java.lang.StringBuilder类来作为它的备选。这两个类都继承了包私有(注:简单来说就是没有修饰符的类)的java.lang.AbstractStringBuilder类,它们的append方法的实现也非常类似。

不同之处就在于StringBuffer的同步操作:

和StringBuilder作一下对比:

调用StringBuffer的append方法的线程,必须得获取到这个对象的内部锁(也叫监视器锁)才能进入到方法内部,在退出方法前也必须要释放掉这个锁。而StringBuilder就不需要进行这个操作,因此它的执行性能比StringBuffer的要高——至少乍看上去是这样的。

不过在HotSpot虚拟机引入了逃逸分析之后,在调用像StringBuffer这样的对象的同步方法时,就能够自动地把锁消除掉了。这只会出现在方法域内部所创建的对象上,只有这样才能保证不会发生逃逸。

Java的性能测试一般都会用到Java Microbenchmark Harness(JMH)。我们就用JMH来测试一下,当现代的JVM能够确认StringBuffer对象只能被一个线程访问时,它是如何通过消除StringBuffer上的锁来缩小性能上的差距的。

锁消除是一项非常有效的优化,在Java 8中它是默认开启的,不过你也可以通过-XX:-DoEscapeAnalysis这个VM参数来关掉它,这样可以看下优化的效果。开启(默认)了逃逸分析后,StringBuffer和StringBuilder的性能基本上是一样的。(结果报告统计的是每秒执行的操作数。分数越高说明性能越好。)

如上所示,关掉了逃逸分析后,StringBuffer的代码要慢15%左右——而这个差别主要就是由于调用append()方法时的加锁操作导致的。

锁粗化(Lock Coarsening)

HotSpot虚拟机还有一些额外的锁优化的技术,虽然从技术上讲它们并不属于逃逸分析子系统中的一部分,但也是通过分析作用域来提高内部锁的性能。当连续获取同一个对象的锁时,HotSpot虚拟机会去检查多个锁区域是否能合并成一个更大的锁区域。这种聚合被称作锁粗化,它能够减少加锁和解锁的消耗。

当HotSpot JVM发现需要加锁时,它会尝试往前查找同一个对象的解锁操作。如果能匹配上,它会考虑是否要将两个锁区域作合并,并删除一组解锁/加锁操作。

我们来看一个程序,它会连续获取同一个对象的监视器锁:

它的字节码如下,看起来非常的冗长:

[代码最后的注释对应着后面的输出结果行。——Ed.]

先来回顾一下,操作内部锁对应的字节码是monitorenter和monitorexit。

字节码中的每一条monitorenter指令都会对应着两条monitorexit指令,它们分别对应着不同的执行路径。原因是第一条monitorexit指令会在正常退出锁区域时释放监视器锁,而第二条指令则是在异常退出时进行释放。

这段字节码看起来可能很奇怪,因为在源程序中同步块中只有一个int变量的自增操作而已。代码中并没有抛异常,不过它的确有可能会异常退出锁区域。(如果线程捕获到InterruptedException异常就可能会这样,比如调用了执行线程的stop()方法。因此,需要有第二条执行路径来确保监视器锁一定能被释放掉,即使是抛了非受检异常(unchecked exception)也是如此。从JVM规范中可以了解到更多相关知识。)锁粗化是默认开启的,不过也可以通过启动参数-XX:-EliminateLocks来关掉它。

嵌套锁

同步块可能会一个嵌套一个,进而两个块使用同一个对象的监视器锁来进行同步也是很有可能的。这种情况我们称之为嵌套锁,HotSpot虚拟机是可以识别出来并删除掉内部块中的锁的。当一个线程进入外部块时就已经获取到锁了,因此当它尝试进入内部块时,肯定也仍持有这个锁,所以这个时候删除锁是可行的。

在写作本文的时候,Java 8中的嵌套锁删除只有在锁被声明为static final或者锁的是this对象时才可能发生。

下面是一个碰到嵌套同步块时删除内部锁的例子:

HotSpot虚拟机会删除掉内部的嵌套锁,因此这段代码最终会变成这样:

嵌套锁优化是默认开启的,不过也可以通过启动参数-XX:-EliminateNestedLocks来关掉它。

数组及逃逸分析

非堆上分配的空间要么存储在栈上,要么就在CPU寄存器中,这些都是相对稀缺的资源,因此逃逸分析和其它优化一样,(在实现上)肯定会面临妥协。HotSpot JVM上的一个默认限制是大于64个元素的数组不会进行逃逸分析优化。这个大小可以通过启动参数-XX:EliminateAllocationArraySizeLimit=n来进行控制,n是数组的大小。

假设有段热点代码,它会去分配一个临时数组用于从缓存中读取数据。如果逃逸分析发现这个数组的作用域没有逃逸出方法体外,便不会在堆上分配内存。不过如果数组大小超过64的话(哪怕并不是全都用到)便仍会存储到堆里。这样数组的逃逸分析优化便不会起作用,也仍会从堆内分配内存。

在下面的JMH基准测试中,test方法会分别新建大小为63、64、65的非逃逸数组。(大小为63的数组之所以也参与测试,是为了证明64的数组比65的快并不是因为内存对齐的缘故。)

每轮测试都只使用到了数组的前两个元素,也就是a[0]和a[1]。需要注意的是,逃逸分析只受限于数组长度的大小,和实际使用到多少个元素是没有关系的。

从结果来看,一旦数组分配不能受益于逃逸分析的优化时,性能便会出现大幅下降。(这里的分数也是对应的每秒的操作数,分越高性能越好。)

如果你需要在热点代码中分配更大的数组,可以通过配置让虚拟机对大数组进行优化。把元素上限调整成65,然后再跑一下基准测试便会发现性能也对齐了。

命令行:

的执行结果是:

可以看到结果是一样的。

结论

本文及上一篇关于逃逸分析的文章给大家展示了HotSpot JVM所蕴藏的一些魔力。同时大家也能看到这些优化背后的复杂度。Java的每一次大的版本发布都会往JVM中增加一些新的特性。

事实上,Oracle也在研究下一代的编译技术。它便是Graal,这是一款可插拔、可扩展的、Java实现的just-in-time (JIT)编译器。它是Metropolis项目的重要组成部分,这个项目的目标是尽可能多地使用Java语言来构建JVM的运行时程序。

正如JEP 317中所提到的,Graal编译器是Java 10的一项实验性的新功能。它的主要目标是让开发人员和专业平台的负责人能够自己去编写专属的、满足自身特殊需求的JIT编译器。对于新的优化技术的设计和原型完成来说,Graal是一个非常合适的平台。

本文及上一篇文章所提到的作用域分析的方法可以用来实现很多优化技术。首先便是分配消除(allocation elimination,也就是标量替换,注:指的是把对象分解成int等基础类型,在栈和寄存器中分配空间,这样就可以不在堆上分配内存,也不需要GC进行回收了),还有我们讨论到的这些锁相关的技术。这些只是HotSpot JVM中成熟的C2编译器的所提供的JIT编译技术中的一些例子。后续的文章还会陆续介绍HotSpot JVM中用来提升代码性能的一些其它的技术。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-08-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 java一日一条 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
详解synchronized和锁升级,以及偏向锁和轻量级锁的升级
synchronized 是一个同步关键字,在某些多线程场景下,如果不进行同步会导致数据不安全,而 synchronized 关键字就是用于代码同步。什么情况下会数据不安全呢,要满足两个条件:一是数据共享(临界资源),二是多线程同时访问并改变该数据。
业余草
2021/12/06
1.1K0
详解synchronized和锁升级,以及偏向锁和轻量级锁的升级
4. synchronized详解
  多线程编程中,有可能会出现多个线程同时访问同一个共享、可变资源的情况,这个资源我们称之其为临界资源;这种资源可能是:对象、变量、文件等。
用户7798898
2020/09/27
4740
4. synchronized详解
虚拟机--逃逸分析
如果对象发生逃逸,那会分配到堆中。(因为对象发生了逃逸,就代表这个对象可以被外部访问,换句话说,就是可以共享,能共享数据的,无非就是堆或方法区,这里就是堆。)
终码一生
2022/04/14
4260
Java中的锁 Lock和synchronized
锁是java并发编程中最重要的同步机制。锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息。
Java技术江湖
2019/09/25
4950
synchronized源码分析
(3)修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁
用户6182664
2020/05/07
4360
【并发编程】2 synchronized底层实现原理、Java内存模型JMM;monitor、CAS、乐观锁和悲观锁;对象的内存结构、Mark Word、锁升级
本文为5、6小节,1~4节请查阅【并发编程】1 synchronized底层实现原理、Java内存模型JMM;monitor、CAS、乐观锁和悲观锁;对象的内存结构、Mark Word、锁升级
寻求出路的程序媛
2024/05/13
1170
【并发编程】2 synchronized底层实现原理、Java内存模型JMM;monitor、CAS、乐观锁和悲观锁;对象的内存结构、Mark Word、锁升级
Java并发之锁优化
“ 高效并发是从JDK 1.5到JDK 1.6的一个重要改进,HotSpot虚拟机开发团队在这个版本上花费了大量的精力去实现各种锁优化技术,如适应性自旋(Adaptive Spinning)、 锁消除(Lock Elimination)、 锁粗化(Lock Coarsening)、 轻量级锁(Lightweight Locking)和偏向锁(Biased Locking)等,这些技术都是为了在线程之间更高效地共享数据,以及解决竞争问题,从而提高程序的执行效率”
每天学Java
2020/06/02
5200
synchronized 原理分析
synchronized 原理分析 1. 在阅读源码时做了大量的注释,并且做了一些测试分析源码内的执行流程,由于博客篇幅有限,并且代码阅读起来没有 IDE 方便,所以在 github 上提供JDK1.8 的源码、详细的注释及测试用例。欢迎大家 star、fork ! 2. 由于个人水平有限,对源码的分析理解可能存在偏差或不透彻的地方还请大家在评论区指出,谢谢! 1. synchronized 介绍    在并发程序中,这个关键字可能是出现频率最高的一个字段,他可以避免多线程中的安全问题,对代码进行同步。
lwen
2018/04/17
6410
synchronized 原理分析
Java锁详解[通俗易懂]
单线程的情况,下面代码中的count,始终只会被一个线程累加,调用addOne()10次,count的值一定就累加了10。
全栈程序员站长
2022/09/08
3040
JUC并发编程之Synchronized关键字详解
在多线程中,有可能会出现多个线程同时访问同一个共享、可变资源的情况,这个资源我们称之其为临界资源;这种资源可能是:对象、变量、文件等。
黎明大大
2021/07/23
3730
JUC并发编程之Synchronized关键字详解
synchronized优化手段:锁膨胀、锁消除、锁粗化和自适应自旋锁...
synchronized 在 JDK 1.5 时性能是比较低的,然而在后续的版本中经过各种优化迭代,它的性能也得到了前所未有的提升,上一篇中我们谈到了锁膨胀对 synchronized 性能的提升,然而它也只是“众多” synchronized 性能优化方案中的一种,那么我们本文就来盘点一下 synchronized 的核心优化方案。
磊哥
2021/08/12
7710
啥?小胖连 JVM 对锁做了那些优化都不知道?真的菜!
来到多线程的第十五篇,对前十四篇感兴趣的请点文末底部的上、下一篇标签。这篇来聊聊 JVM 对 synchronized 做了那些优化?
JavaFish
2021/01/18
5150
啥?小胖连 JVM 对锁做了那些优化都不知道?真的菜!
面试官:什么是JIT、逃逸分析、锁消除、栈上分配和标量替换?
JIT、逃逸分析、锁消除、栈上分配和标量替换等都属于 JVM 的优化手段,JVM 优化手段是指在运行 Java 程序时,通过对字节码的编译和执行过程进行优化,以提升程序的性能和效率。
磊哥
2024/02/01
1550
JVM学习记录-线程安全与锁优化(二)
高效并发是程序员们写代码时一直所追求的,HotSpot虚拟机开发团队也为此付出了很多努力,为了在线程之间更高效地共享数据,以及解决竞争问题,HotSpot开发团队做出了各种锁的优化技术常见的有:自适应自旋锁(Adaptive Spinning)、锁消除(Lock Elimination)、锁粗化(Lock Coarsening)、轻量级锁(Lightweight Locking)和偏向锁(Biased Locking)等。
纪莫
2018/08/01
4400
JVM学习记录-线程安全与锁优化(二)
Java基础之Synchronized原理
思维导图svg: https://note.youdao.com/ynoteshare1/index.html?id=eb05fdceddd07759b8b82c5b9094021a&type=no
Ryan-Miao
2020/07/01
4580
JVM优化之逃逸分析与分配消除
在这期文章中,我们将要深入介绍一下逃逸分析(escape analysis)技术,这是JVM最有意思的优化手段之一。逃逸分析是JVM的一项自动分析变量作用域的技术,它可以用来实现某些特殊的优化,后续我们也会分析下这些优化。在开始之前,你只需要掌握一些HotSpot JVM的基本工作原理就可以了。
哲洛不闹
2019/08/09
7860
吃透synchronized实现原理
记得刚刚开始学习Java的时候,一遇到多线程情况就是synchronized。对于当时的我们来说,synchronized是如此的神奇且强大。我们赋予它一个名字“同步”,也成为我们解决多线程情况的良药,百试不爽。但是,随着学习的深入,我们知道synchronized是一个重量级锁,相对于Lock,它会显得那么笨重,以至于我们认为它不是那么的高效。随着Javs SE 1.6对synchronized进行各种优化后,synchronized不会显得那么重。
码哥字节
2021/07/27
4800
JVM-锁优化
​ 高效并发是JDK5升级到JDK6后一项重要的改进,HotSpot虚拟机开发团队在这个版本上花费了巨大的资源去实现各种锁优化。比如,自旋锁,自适应自旋锁,锁消除,锁膨胀,轻量级锁,偏向锁等。这些技术都是为了在线程之间更高效的共享数据及解决竞争问题。从而提高程序的运行效率。
程序员阿杜
2021/08/05
2530
synchronized 与多线程的哪些关系
JVM 实现的 synchronized JDK 实现的 ReentrantLock
BUG弄潮儿
2022/06/30
2640
synchronized 与多线程的哪些关系
简单理解JVM优化
所有正在运行的线程的栈上的引用变量。所有的全局变量。所有ClassLoader。。。
烂猪皮
2018/10/18
6450
简单理解JVM优化
相关推荐
详解synchronized和锁升级,以及偏向锁和轻量级锁的升级
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文