关于java:这波性能优化太炸裂了

98次阅读

共计 7760 个字符,预计需要花费 20 分钟才能阅读完成。

你好呀,我是 why。

不是,这不是我。我还年老,也比他帅。

这是明天文章的主人公。

他叫做 Brett Wooldridge,你应该是不意识的。

然而我把他的 github 截图给你看看,你肯定晓得他写的开源我的项目:

看到了吗?

他就是赫赫有名的 HikariCP 的爸爸啊。

而且你看他的 github 的简介,写的很有感觉:

Father of an angel who fell to Earth and somehow into my life.

一个落到地球上的天使的父亲,她人不知; 鬼不觉地进入了我的生存。

图片应该是就他的孩子,他在旁边露出了老父亲般慈爱的微笑。

文章最开始的那一张图,是我从这个报道中找到的:

https://blog.jooq.org/2017/02…

其中问的第一个问题是这个:

你创立了 Java 中最风行的连接池之一,HikariCP。那么请问是什么让你的库如此受欢迎呢?

上面一大节,我就用第一人称的角度给大家讲述一下这个老哥是怎么答复这个问题的。

为啥写 HikariCP?

啥,你问我为啥要写 HikariCP?

哎呀妈呀,还不是因为没有找到趁手的家伙什嘛。

我几年前写代码的时候,须要用到一个数据库的连接池,于是就像大多数开发者一样,面向浏览器编程,在网上找了一个开源的池子,拿来就用。

你还别说,看起来如同还不错。

然而起初在对我的项目做性能测试的时候,就缓缓的发现这个池子不太行了,老是会碰到死锁、连贯状态不正确的问题。

我寻思这玩意不是坑爹吗?

然而过后用的连接池是开源的嘛,本着开源的精力,我就想着把代码拉下来看看嘛,能不能帮忙给修复一下。

后果我关上代码的时候,好家伙,代码量之大,至多比我预期的要多出个几千行来。

代码多就算了,忍忍就能读上来。

神奇的是代码逻辑。

我是去排查死锁问题的,后果我发现锁是一个套一个。

有的时候在一个办法外面获取到锁了,我硬是找不到开释的中央。

最初在隔着十万八千里的中央,看到了开释锁的中央。

我过后大略是这样的:

因为我晓得,我曾经没有方法找到死锁埋伏在代码的哪个角落了。

就算我解决了以后的问题,依照我的项目这样的写法,迟早也会碰到其余的问题。

于是我毅然决然,决定 …

在网上再找一个。

这次我学乖了,找到新的连接池之后,我先看了它的代码。

因为被死锁搞怕了,所以特地是关注了对于锁的局部。

新找到的连接池锁的语义的确更清晰了,然而代码量依然是我预期的二倍多。

除此之外,我钻研过的所有的链接池,都在以各种各样的形式违反 JDBC 的合约。

比方,我发现的最常见的一个问题是这样的。

当一个链接被用完了,放回池子外面的时候。某些池子并没有把这个链接外面的音讯清理洁净,比方主动提交、事务隔离级别等等,导致下个消费者再次拿到这个链接的时候,是一个“脏”链接。

我过后就在想:

Really?这就是 Java 生态中的连接池的现状?不行了,我要亲自出手了。于是出于须要和挫折感,我创立了 HikariCP。

回到最开始的问题。

如上所述,在我写 HikariCP 之前,其实曾经有很多成熟的连接池了,那么 HikariCP 是如何变得风行的呢?

再我看来,如果我主打正确性和可靠性,其实不算一个好的卖点,因为我认为这是必须所具备的货色。

所以我专一于推广性能。在我的各个社交媒体下来进行推广。

在 2015 年的某个时候,Wix 工程团队写了一篇对于应用 HikariCP 的博客。

这一波我就间接弹射腾飞了。HikariCP 也算是走进了大家的视线。

最初,我的确心愿随着工夫的推移,更多的用户会对正确性和可靠性给予等同的器重,没有这些性能就没有意义。

就我而言,我打算多写一些对于 HikariCP 的这些方面的文章。

性能为啥牛逼?

后面说了,HikariCP 的卖点是强悍的性能。

那么它的性能为啥这么牛逼呢?

其实答案就写在 HikariCP 的 github 主页上:

https://github.com/brettwoold…

在进入 how we do it here 之前,先简略说说这个我的项目的名称。

能够看到一个大大汉字:光。

对于这个名字的起源,其实在后面提到的报道中也提到过:

HikariCP,被翻译为“光”,在英语中,在 HikariCP 的上下文中,它是一个双关语。在这个我的项目外面,” 光 ” 不只示意速度快,也是指代码量很少。

Hikari 的发音是 Hi-ka-lee。

这个大家记一下。

我记得有一次面试,有个面试者提到了这个连接池,然而他不晓得怎么读。

他说:就是 H 开明 CP 结尾的那个连接池,咋读的我遗记了。

然而我过后也一下就反馈过去了。

我说:嗯,我晓得你说的哪个连接池,你持续说。

其实,过后我也不晓得怎么读,就很难堪。

好了,接下来,就一起看看性能为啥这么牛逼。

答案,作者都在 github 外面写着呢:

https://github.com/brettwoold…

首先,这个文章的题目就很有意思:

wath mean is Down the Rabbit Hole?

直译过去是“在兔子洞里”。

我觉事件没有这么简略,于是我去查了一下:

哦,down the rabbit hole 原来是是冒险进入未知世界的隐喻。出自驰名的《爱丽丝梦游仙境》一书中。

个别咱们用 down the rabbit hole 用来形容陷入一个愈发奇怪、令人摸不着头脑或出乎意料的情况,而且一件事件促使另一件事件的产生,接连不断,因而越陷越深、无从脱身的场景。

一个英语的小俚语,送给大家。

晓得题目的含意后,等你看完作者写的文章之后,你再次扫视这个“兔子洞”的题目,你就会发现:真特么贴切啊。

全文读完,了解之后,我发现作者想表白的为什么这么快的起因有四个:

  • 字节码级别的优化 - 尽量的利用 JIT 的内联伎俩
  • 字节码级别的优化 - 利用更容易被 JVM 优化的指令
  • 代码级别的优化 - 利用革新后的 FastList 代替 ArrayList
  • 代码级别的优化 - 利用无锁的 ConcurrentBag

咱们一个个的看。

字节码级别的优化

文章的结尾,作者就说了:我这波操作在字节码,就问你牛不牛逼。

简略的翻译一下要害的中央:

  • 为了使 HikariCP 变得更快,我进行了字节码级别的优化。
  • 我拿出了我所晓得的所有技巧来利用 JIT 优化,从而帮忙到你。
  • 我钻研了编译器的字节码输入,甚至是 JIT 的汇编输入,以限度要害的程序小于 JIT 的 inline-threshold。

这个中央作者提到了 JIT 的内联优化。

啥是内联?

内联其实是一个动作。

选定某个被调用的办法,将其内容复制到被调用的中央。

举个简略的例子,假如代码是这样的:

int result = add(a,b);

private int add(int x,int y){return x+y;}

那么通过 JIT 的内联优化之后,代码就会变成这样:

int result= a + b;

这样,节约了调用 add 办法的开销。

内联,也被称为优化子母,它为其余的优化伎俩建设了十分好的根底,所以除了下面写的那个例子之外,还有很多的更加进阶的体现形式,比方逃逸剖析、循环展开、锁打消:

.png)

那么一个调用的开销到底有哪些呢?

我想无外乎就是这几步:

  • 首先要设置办法调用须要传递的参数,对吧?
  • 有了参数,是不是还得查问具体调用哪个办法,对吧?
  • 而后如果有相似于局部变量,或者求值这样的办法,还得创立新的调用栈帧,创立新的运行时数据结构,对吧?
  • 最初,还有可能须要给调用方返回一个后果,对吧?

有的敌人就就会说了,至于吗?这个开销看着也不大啊?

是的,的确不大,然而当再小的一个优化点,乘以一个微小的调用量之后,最终的后果都是很可观的。

我想这个情理大家都明确。

作者也在文章外面说了:

HikariCP 蕴含了许多宏观的优化,这些优化独自来看简直无奈掂量,但联合起来就能晋升整体性能。

甚至在数百万次的调用中,优化的级别是以毫秒的工夫来掂量的。

可能这就是大佬吧。

我想,谋求性能的极致,也就不过如此了。

接下来,说说另外一个字节码级别的优化:

invokevirtual vs invokestatic

这波优化我感觉几乎就是在大气层了。

作者举了个例子。

之前获取 Connection, Statement,ResultSet 的代理对象什么的都是通过单例工厂办法。

就相似于这样的:

ROXY_FACTORY 就是一个 static 的字段。

下面的代码的字节码大略是这样的:

通过字节码你能够看到,首先有一个 getstatic 调用,来取得动态字段 PROXY_FACTORY 的值。

还有一个 invokevirtual 指令的调用,对应的就是 ProxyFactory 实例的 getProxyPreparedStatement() 办法:

15: invokevirtual #69  // Method com/zaxxer/hikari/proxy/ProxyFactory.getProxyPreparedStatement:(Lcom/zaxxer/hikari/proxy/ConnectionProxy;Ljava/sql/PreparedStatement;)Ljava/sql/PreparedStatement;

这个中央有什么优化空间呢?

作者把代码批改成了这样:

其中 ProxyFactory 是通过 Javassist 生成的。

所以你去看 ProxyFactory 源码,全是空实现,

它真正的实现逻辑,是对应源代码的这个类,就不具体展现了,有趣味的能够下来看看:

com.zaxxer.hikari.util.JavassistProxyFactory

而后,把 getProxyPreparedStatement 办法做成了 static。

而后字节码就变成了这样:

神奇的事件就产生了:

  • getstatic 指令隐没了
  • invokevirtual 被替换成了 invokestatic 调用,这样更加容易被 JVM 优化。
  • 最初,可能第一眼没有留神到的是,堆栈大小从 5 缩小到 4。这是因为在 invokevirtual 的状况下,ProxyFactory 的实例被隐含地传递到了堆栈中(也就是 this 对象),而且在调用 getProxyPreparedStatement() 时,还有一个额定的从堆栈中弹出的操作。

第 1,3 点应该问题不大。大家都能明确是怎么回事。

然而这个第二点:invokevirtual 被替换成了 invokestatic 调用,这样更加容易被 JVM 优化。

说真的,我第一次看到的时候大略是这样的:

为啥啊?

invokevirtual 和 invokestatic 是干啥的我倒是还记得。

然而 invokestatic 的性能会更好一点吗?

于是我带着这个问题去翻了《深刻了解 JVM 虚拟机》,没有间接找到答案。

然而还是有意外播种的。就是写下了这篇文章:《报告!书里有个 BUG》

不然你感觉我为什么会忽然翻到书外面的这一部分,都是有契机的。

尽管,书外面没有间接把答案写进去,然而在相干局部有这样的一段话:

我了解一下就是 invokevirtual 指令,须要查问虚办法表能力确定办法的间接援用。

而 invokestatic 在类加载的时候,就能够从符号援用转成间接援用。

这样看来,invokestatic 的确是优于 invokevirtual 的。

那么问题又来了。

类加载的过程是什么?

加载、验证、筹备、解析、初始化。

invokestatic 是在哪个过程搞事件的?

必定是解析阶段哈,敌人们。

解析阶段,就是 JVM 将常量池内的符号援用替换为间接援用的过程。

扯远了,说回来。

下面只是我的一点猜想,我置信必定不止我一个人看了作者的“兔子洞”文章后对于 invokevirtual vs invokestatic 这一块有疑难。

于是,我去查了一圈。

果不其特么的然。(道歉爆粗了,然而我的确找了很久。)

找到了这个链接,链接的前半部分和我的问题截然不同:

https://github.com/brettwoold…

作者的回复如下:

前面那一段 Additionally 很好了解。

就是后面说的,动态调用少一个堆栈,在运行时就少一个推 / 拉操作,这进一步提高了性能。

次要是后面这段,有亿点点难懂。

他说:简而言之,JVM 在做内联调用的时候,即便是单态的内联,它也必须装置一个 trap(陷阱),以防另一个实现呈现,并将调用转变为多态。

这个 trap 的设置和革除给调用减少了一点开销。

怎么样,懵不懵逼?

其实,他这句话,我集体了解,说的就是 Java 的动静分派,聊的就是 JVM 的 CHA(Class Hierarchy Analysis,类型继承关系剖析)技术。

答案就写在《深刻了解 Java 虚拟机 (第三版)》的 417 页,翻去吧:

你非要问我证据是什么,那么这两个单词响应上了,你说这事多巧?

invokevirtual 调用的是虚办法,依照书里的说法,后面提到的 trap 其实就是这里的“逃生门”:

而 This trap setting and clearing adds slightly more overhead to the invocation(这个 trap 的设置和革除给调用减少了一点开销)这句话,其实就是对应这里:

当初你晓得为什么对于 JVM 来说,invokestatic 比 invokevirtual 更容易优化了吧?

优化指的就是内联。

invokestatic 调用的是静态方法,对于非虚办法,JVM 能够间接进行内联,这种内联是有百分之百的平安保障的。

而 invokevirtual 调用的是虚办法,对于虚办法的内联,就得上 CHA 机制,设置逃生门这一套玩意。

尽管都是内联,这不得多耗费一点性能嘛。

内联曾经是性能优化了,让代码更好的内联,优化性能优化的优化。

这波操作,在大气层。

好了,下面就是字节码层面的优化了,接着咱们看代码层面的优化。

代码层面的优化

代码层面上最闻名的就是 FastList 替换 ArrayList 这个玩意了。

首先,我去看了我的项目的提交记录,在 2014 年 1 月 15 日的时候,作者进行了一次提交:

备注的后半局部咱们应该很相熟了,后面曾经讲过了。

后面就是用 FastList 替换 ArrayList 的那一次提交。

Java ArrayList 在每次调用 get(int index) 时都会进行范畴查看。在 HikariCP 我的项目中,能够保障 index 在正确的范畴内,所以这个查看没有意义,于是就去掉了:

再比方,ArrayList 的 remove(Object o) 办法是从头扫到尾。

假如要删除最初一个元素,须要遍历整个数组。

巧就巧在,比方 HikariCP 的 Statement,依照咱们的编码习惯,删除(敞开)应该先删除最初一个。

所以 FastList 优化了 remove(Object element) 办法,将查找程序变成了逆序查找:

整体来看,FastList 的优化点就下面说的 get 和 remove 办法。

接着,看看另外一个代码级别的优化:

作者列了几个点:

  • 一个无锁的设计
  • 线程本地缓存
  • 窃取队列
  • 间接交接的优化

作者介绍的很简略,其实这外面还是很有货色的。

一个重要的技巧是 ConcurrentBag 通过 ThreadLocal 做了一次连贯的预调配。

通过 ThreadLocal 肯定水平上防止了共享资源的竞争。

本人看代码的话次要看看 add(闲暇连贯退出队列)、borrow(获取连贯)、requite(开释连贯) 办法。

网上也有很多相应的文章去介绍,有趣味的能够去理解一下,我这就不写了。

哦,你不想看其余的文章,就想等着我给你讲呢?
好的,先欠着,欠着。
偷个懒,文章写太长了也没人看。

打起来了

在写文章的过程中,我还看到了这样的一个 issue,感觉有点意思,写一下。

https://github.com/brettwoold…

一个小哥说:

你好,我感觉你对 Java 数据库池的剖析很有参考价值。我碰巧遇到了阿里巴巴的这个 druid 线程池(号称是 Java 中最快的数据库池!)。从我的疾速浏览来看,它仿佛有一些很酷的性能。对此有什么想法吗。谢谢。

HikariCP 的作者很快就进行了回复:

至多在他的基准测试中,Druid 在获取和返回连贯方面是最慢的,而在创立和敞开语句方面是第三快的。他们维基中的基准页面没有显示他们是用什么配置运行的,但我狐疑他们禁用了借来的测试。尽管我不会说这是 “ 舞弊 ”,但这不是我在生产上的应用形式。据我所知,他们也没有提供他们测试源代码。

这就有点意思了。

尽管我不会说这是 ” 舞弊 ”, 这话说的,就像是:有一句话我不知当讲不当讲。

而后就接着讲进去了。

接着,另外一个吃瓜网友说:

Druid 的设计实践是专一于监控和数据拜访行为的加强(如主动数据库切片)。它提供了一个 SQL 解析器来剖析用户的 SQL 查问,并收集了很多数据用来监控。因而,如果你须要一个 JDBC 监控解决方案,你能够试试 Druid。

HikariCP 的作者也示意这句话没故障,然而他强调了本人的 HikariCP 也是给监控留了口子的:

这是一个无效的观点。我想指出,HikariCP 也提供监控数据,但提供的指标是 “ 池级 “ 指标,并不具体到查问执行工夫等。

下面的对话,都是产生在 2015 年 1 月。

然而一年半后,2016 年 7 月 26 日,这个问题又被一个人激活了:

wenshao,来者何人?

此人正是 druid 的爸爸之一,江湖人称温少。

兴许你不意识温少,兴许你不晓得温少写的 druid,然而你肯定晓得温少的另外一个大作:

问题是多了一点,然而并不障碍他人是大神。

能够间接端茶:

首先温少说:如果你配置了 maxWait 属性,druid 会应用偏心锁,所以升高了性能。

至于为什么这样的,是因为在生产环境中遇到的一些问题,设计如此。

而后他接着提到了淘宝:

链接点进去,题目是这样的:

说的是 2015 年的天猫双 11。

题目翻译过去就是:

阿里巴巴团体在光棍节销售的前 90 分钟内销售了 50 亿美元。

我还在链接外面看到了好久没见的马爸爸:

我了解温少放这个链接的意思就是说,druid 在阿里外部应用,天猫双十一是一个十分牛逼的场景,druid 禁受住了这样场景的考验。

HikariCP 的作者并没有回复温少。

直到另外一个吃瓜大众的火上浇油:

HikariCP 的作者寻思,这是要进行数据量的 battle 了呀。

那我就不客气了。

HikariCP 是世界上应用最宽泛的连接池之一,被一些最大的公司应用,每天为数十亿的用户提供服务。

而对于 Druid,不好意思,我谈话有点直:在中国以外的中央很少见。

然而对于他的这个答复,很快就有人提出了质疑:

一些最大的公司都在应用,每天为数十亿用户服务?比如说呢?

要数据是吧?坐稳了:

  • wix.com 托管着超过 1.09 亿个网站,每天解决的申请超过 10 亿个。
  • Atlassian 的产品领有数百万的客户。
  • HikariCP 是 spring boot 的默认连接池。
  • HikariCP 每月从地方 maven 仓库解析超过 30 万次。

这些公司都在用:

这个答复之后,单方都没有谈话了。

两方之间的 battle 就算是完结了。

然而还有人在持续跟帖,我感觉这个哥们属于苏醒吃瓜:

另外一个老哥的答复就有意思了:

别吵了,别吵了。我特么来这里是学技术的,不是来看你们探讨 “ 资本主义 ” 工具和 “ 共产主义 ” 工具的区别的。

而我感觉,这场 battle 其实真的没有特地大的意义。

在技术选型上,没有最好的,只有适合的。

Druid 和 HikariCP 各有各的劣势。

最初说一句(求关注)

好了,看到了这里点个关注吧,周更很累的,须要一点正反馈。

感谢您的浏览,我保持原创,非常欢送并感谢您的关注。

正文完
 0