点关注,不迷路;持续更新Java架构相关技术及资讯热文!!!java
JVM 在对代码执行的优化可分为运行时(runtime)优化和即时编译器(JIT)优化。运行时优化主要是解释执行和动态编译通用的一些机制,好比说锁机制(如偏斜锁)、内存分配机制(如 TLAB)等。除此以外,还有一些专门用于优化解释执行效率的,好比说模版解释器、内联缓存(inline cache,用于优化虚方法调用的动态绑定)。面试
JVM 的即时编译器优化是指将热点代码以方法为单位转换成机器码,直接运行在底层硬件之上。它采用了多种优化方式,包括静态编译器可使用的如方法内联、逃逸分析,也包括基于程序运行 profile 的投机性优化(speculative/optimistic optimization)。这个怎么理解呢?好比我有一条 instanceof 指令,在编译以前的执行过程当中,测试对象的类一直是同一个,那么即时编译器能够假设编译以后的执行过程当中还会是这一个类,而且根据这个类直接返回 instanceof 的结果。若是出现了其余类,那么就抛弃这段编译后的机器码,而且切换回解释执行。数组
固然,JVM 的优化方式仅仅做用在运行应用代码的时候。若是应用代码自己阻塞了,好比说并发时等待另外一线程的结果,这就不在 JVM 的优化范畴啦。缓存
今天这道面试题在专栏里有很多同窗问我,也是会在面试时被面试官刨根问底的一个知识点。安全
大多数 Java 工程师并非 JVM 工程师,知识点总归是要落地的,面试官颇有可能会从实践的角度探讨,例如,如何在生产实践中,与 JIT 等 JVM 模块进行交互,落实到如何真正进行实际调优。服务器
在今天这一讲,我会从 Java 工程师平常的角度出发,侧重于:架构
从总体去了解 Java 代码编译、执行的过程,目的是对基本机制和流程有个直观的认识,以保证可以理解调优选择背后的逻辑。并发
从生产系统调优的角度,谈谈将 JIT 的知识落实到实际工做中的可能思路。这里包括两部分:如何收集 JIT 相关的信息,以及具体的调优手段。工具
首先,咱们从总体的角度来看看 Java 代码的整个生命周期,你能够参考我提供的示意图测试
我已经提到过,Java 经过引入字节码这种中间表达方式,屏蔽了不一样硬件的差别,由 JVM 负责完成从字节码到机器码的转化。
一般所说的编译期,是指 javac 等编译器或者相关 API 等将源码转换成为字节码的过程,这个阶段也会进行少许相似常量折叠之类的优化,只要利用反编译工具,就能够直接查看细节。
java优化与 JVM 内部优化也存在关联,毕竟它负责了字节码的生成。例如,Java 9 中的字符串拼接,会被 javac 替换成对 StringConcatFactory 的调用,进而为 JVM 进行字符串拼接优化提供了统一的入口。在实际场景中,还能够经过不一样的策略选项来干预这个过程。
今天我要讲的重点是JVM 运行时的优化,在一般状况下,编译器和解释器是共同起做用的,具体流程能够参考下面的示意图
JVM 会根据统计信息,动态决定什么方法被编译,什么方法解释执行,即便是已经编译过的代码,也可能在不一样的运行阶段再也不是热点,JVM 有必要将这种代码从 Code Cache 中移除出去,毕竟其大小是有限的。
Intrinsic 机制,或者叫做内建方法,就是针对特别重要的基础方法,JDK 团队直接提供定制的实现,利用汇编或者编译器的中间表达方式编写,而后 JVM 会直接在运行时进行替换。
这么作的理由有不少,例如,不一样体系结构的 CPU 在指令等层面存在着差别,定制才能充分发挥出硬件的能力。咱们平常使用的典型字符串操做、数组拷贝等基础方法,Hotspot 都提供了内建实现。
而即时编译器(JIT),则是更多优化工做的承担者。JIT 对 Java 编译的基本单元是整个方法,经过对方法调用的计数统计,甄别出热点方法,编译为本地代码。另一个优化场景,则是最针对所谓热点循环代码,利用一般说的栈上替换技术(OSR,On-Stack Replacement,更加细节请参考R 大的文章),若是方法自己的调用频度还不够编译标准,可是内部有大的循环之类,则仍是会有进一步优化的价值。
从理论上来看,JIT 能够看做就是基于两个计数器实现,方法计数器和回边计数器提供给 JVM 统计数据,以定位到热点代码。实际中的 JIT 机制要复杂得多,郑博士提到了逃逸分析、循环展开、方法内联等,包括前面提到的 Intrinsic 等通用机制一样会在 JIT 阶段发生。
第二,有哪些手段能够探查这些优化的具体发生状况呢?
专栏中已经陆陆续续介绍了一些,我来简单总结一下并补充部分细节。
打印编译发生的细节。
输出更多编译的细节。
JVM 会生成一个 xml 形式的文件,另外, LogFile 选项是可选的,不指定则会输出到
具体格式能够参考 Ben Evans 提供的JitWatch工具和分析指南。
打印内联的发生,可利用下面的诊断选项,也须要明确解锁。
如何知晓 Code Cache 的使用状态呢?
不少工具都已经提供了具体的统计信息,好比,JMC、JConsole 之类,我也介绍过使用 NMT 监控其使用。
第三,咱们做为应用开发者,有哪些能够触手可及的调优角度和手段呢?
调整热点代码门限值
我曾经介绍过 JIT 的默认门限,server 模式默认 10000 次,client 是 1500 次。门限大小也存在着调优的可能,可使用下面的参数调整;与此同时,该参数还能够变相起到下降预热时间的做用。
不少人可能会产生疑问,既然是热点,不是迟早会达到门限次数吗?这个还真未必,由于 JVM 会周期性的对计数的数值进行衰减操做,致使调用计数器永远不能达到门限值,除了能够利用 CompileThreshold 适当调整大小,还有一个办法就是关闭计数器衰减。
若是你是利用 debug 版本的 JDK,还能够利用下面的参数进行试验,可是生产版本是不支持这个选项的。
调整 Code Cache 大小
咱们知道 JIT 编译的代码是存储在 Code Cache 中的,须要注意的是 Code Cache 是存在大小限制的,并且不会动态调整。这意味着,若是 Code Cache 过小,可能只有一小部分代码能够被 JIT 编译,其余的代码则没有选择,只能解释执行。因此,一个潜在的调优势就是调整其大小限制。
固然,也能够调整其初始大小。
注意,在相对较新版本的 Java 中,因为分层编译(Tiered-Compilation)的存在,Code Cache 的空间需求大大增长,其自己默认大小也被提升了。
调整编译器线程数,或者选择适当的编译器模式
JVM 的编译器线程数目与咱们选择的模式有关,选择 client 模式默认只有一个编译线程,而 server 模式则默认是两个,若是是当前最广泛的分层编译模式,则会根据 CPU 内核数目计算 C1 和 C2 的数值,你能够经过下面的参数指定的编译线程数。
在强劲的多处理器环境中,增大编译线程数,可能更加充分的利用 CPU 资源,让预热等过程更加快速;可是,反之也可能致使编译线程争抢过多资源,尤为是当系统很是繁忙时。例如,系统部署了多个 Java 应用实例的时候,那么减少编译线程数目,则是能够考虑的。
生产实践中,也有人推荐在服务器上关闭分层编译,直接使用 server 编译器,虽然会致使稍慢的预热速度,可是可能在特定工做负载上会有微小的吞吐量提升。
其余一些相对边界比较混淆的所谓“优化”
好比,减小进入安全点。严格说,它远远不仅是发生在动态编译的时候,GC 阶段发生的更加频繁,你能够利用下面选项诊断安全点的影响。
注意,在 JDK 9 以后,PrintGCApplicationStoppedTime 已经被移除了,你须要使用“-Xlog:safepoint”之类方式来指定。
不少优化阶段均可能和安全点相关,例如:
在 JIT 过程当中,逆优化等场景会须要插入安全点。
常规的锁优化阶段也可能发生,好比,偏斜锁的设计目的是为了不无竞争时的同步开销,可是当真的发生竞争时,撤销偏斜锁会触发安全点,是很重的操做。因此,在并发场景中偏斜锁的价值实际上是被质疑的,常常会明确建议关闭偏斜锁。
点关注,不迷路;持续更新Java架构相关技术及资讯热文!!!