这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战
上一篇我们聊了预热转发的高级特性,那么,为什么刚发布的服务需要预热呢?预热可以起到什么作用呢?
Part1 什么是JIT优化
都说C++快,Java慢,都是高级语言,是什么导致了运行速度的差别呢?
这个涉及到了两种执行方式:解释执行 和 编译执行。
相对于C++直接将代码编译成机器码运行的方式,Java为了实现跨平台、高度抽象等特性,增加了虚拟机层来实现Java代码到机器码的转换,Java程序先是被编译成符合虚拟机规范的.class字节码逐条将字节码翻译成机器码然后执行,所以,速度上就慢一些。
虽然,JVM的加入,给Java的运行速度增加了不少损耗,但是好处也很多,除了跨平台,还为我们实现了诸如内存管理、垃圾回收等容器级通用功能,让研发人员可以更加聚焦业务。
不过,Java也是要面子的,我允许自己慢,但我不允许自己慢那么多!
怎么办呢?遵循二八原则,是不是可以找寻程序当中的贡献了大部分调用量的核心代码,把这部分编译成机器码,提升其速度,不就把整体的速度提上去了么,JVM也是这么做的~
所以,JVM兼容了解释执行和编译执行两种方式,也就是我们常说的_即时编译_。
前面的问题到这里其实就可以回答了。为什么需要预热转发呢?是为了用小流量对程序进行预热,目的是为了让核心代码进行及时编译,提高峰值运行速率,提升服务响应~
下面让我们详细看下JIT。
1.1即时编译器
为了权衡编译时间和执行效率,JVM设置了多种即时编译器:
-
C1(Client 编译器):基于字节码完成部分优化,如方法内联、常量传递,相对于C2,速度快,但性能稍差。
-
C2(Server 编译器):耗时较长的全局优化,如无用代码消除、重排序、循环展开、公共子表达式替代、常量传播等等。
-
Graal(新的JIT编译器):侧重于性能和语言操作性。在一些负载上提供比传统编译器更好的峰值性能;用 Graal 执行的语言可以互相调用,可以使用来自其他语言的库。
1.2 JIT优化触发条件
前面我们说过,JVM其实是希望找到承担更多调用请求的代码块进行优化,那,怎么来确认哪些代码时优化目标呢?--热点探测
基于采样的热点探测:
周期采样,检测各线程栈顶方法,经常出现的方法即为热点方法。好处是简单高效,缺点是不精确,容易受线程运行状态的影响。
基于计数的热点探测:
(包括方法调用计数器和回边计数器)每个方法建立计数器,用来统计调用次数。如果该方法执行次数超过阈值,则该方法被认定为热点方法。好处是足够精确。缺点是空间损耗大,且实现较难。
另外,可以通过如XX:CompileThreshold等参数来修改阈值,不过,没有绝对把握,还是不要动为好。
Part2 JIT指导代码优化
2.1方法内联
为什么我们在刚写代码的时候,总是被建议不要写很大的方法体?方法内联的JIT优化策略就是其中一个重要的原因。(还有GC友好等原因)
JVM内的每一次方法调用,都是栈帧在内存中出栈入栈的过程,方法多了性能损耗自然大,所以要进行方法内联,即把方法执行逻辑直接复制到调用方内部,避免方法调用。
但是,方法内联是有方法大小限制的,超过了一定大小的方法,没法做内联优化。所以,平常应该注意,尽量避免写很大很冗长的方法。
让我们来举个栗子实际感受一下~
两种书写风格的大数相加
如上图所示,两个字符串型整数相加,都能实现功能,前一种写法,把中间过程全都拆开,罗列在的方法内,整个方法虽然理解起来稍微方便些,但整体显得冗长;第二种方法,把各个条件都囊括在了for循环条件内,三行代码完成整体操作。
如果要去评价,我觉得大部分人都会说第二种写的好,但是,第二种的好难道真的局限于优雅么?
//添加JVM启动参数,用于打印代码执行过程中的编译详情
//-XX:+PrintCompilation
String num1 = "12345";
String num2 = "23456";
//循环15000次,因为1.8分层编译下,各层阈值不一样,我们取最大阈值
for (int i=0;i<15001;i++) {
rejectionLB1.stringAdd(num1, num2);
//rejectionLB1.stringAdd2(num1, num2);
}
执行15000次写法1
(图中编译层次这一列中,3代表C1编译,4代表C2编译)
我们看到,随着代码的执行次数的增加,一些方法,进行了C1编译,如我们的主方法_stringAdd_,而少数方法,从C1编译提升到了C2编译,如AbstractStringBuilder::append方法。
执行15000次写法2
我们看到了什么,stringAdd2 居然在进行到运行后期执行了C2编译,而且很明显,方法二的C2编译的方法,比方法一要多不少。所以,平常写代码该注意些什么,是不是显而易见了。。。
2.2其他优化
方法内联虽然只是一种简单优化,但是,是后续其他优化的基石。
而JVM的分层优化涉及的点非常多[1]:
局部优化:关注局部数据流分析,数组越界检查消除;寄存器优化,优化跳转、循环、异常处理等;代码简化,如公共表达式提取等等等。
控制流优化:专注于代码重排序、循环缩减、循环展开、异常定位优化等等等。
全局优化:主要关注冗余消除,如方法调用、锁;逃逸分析;GC和内存分配优化等等等。
Part3 总结
本篇从RPC的预热转发功能,引出了其背后的理论依据--JIT优化。阐述了JIT的基本概念,并用一个实例说明了代码编写风格对JIT优化的实际影响。
JIT相关的优化实现起来非常难,不过其原理和作用对我们普通研发也不是特别难理解,学习JIT优化的目的,在于了解JVM底层的运行逻辑和实现,让我们可以更加信任托管,聚焦业务逻辑,同时在编写代码时,尽量用JVM友好的方式进行,从而达到更好看、更高效的目的。
推荐阅读:
参考资料
[1]
JIT 编译器如何优化代码: "www.ibm.com/docs/zh/sdk…"