三四、Java 性能调优工具箱|JIT编译器:概述

0 CPU 使用率

使用 vmstat 命令查看 CPU 使用率,即 us\sy\id 三个参数,用户、系统、空闲使用 CPU 的时间。

  1. 检查应用性能时,首先应该审查 CPU 时间(尤其是多线程,CPU 的上下文切换报告)
  2. 优化代码的目的是提升而不是降低(更短时间段内的)CPU 使用率。
  3. 在试图深入优化应用前,应该先弄清楚为何 CPU 使用率低。

1 JIT 编译器

java文件->编译->class字节码文件->JVM编译解释成平台相关的二进制文件。
而 JIT 编译器属于最后的 JVM 编译过程,也可以称为后端编译器,这样便于理解
Java 应用汇被编译——但不是编译成特定 CPU 所专用的二进制代码,而是被编译成一种理想化的汇编语言(即 .class 字节码文件),它专用于 JVM 所执行。这个编译时在程序执行时进行的,即编译同时执行,(C 这种编译语言会先编译成 .o 或者 .obj 再执行),而 Java 是直接执行编译代码(JVM 执行)。Java 是一种半编译半解释语言(先编译成 .class,再让 JVM 解释成特定 CPU 的指令),而 Java 的表面直接执行其实内部 JVM 帮我们做了编译解释,不像 C 用户手动编译再执行,因为 C 的编译后的 .o 文件是针对特定的 CPU ,也许在下个 CPU 就需要重新编译了。参见:https://www.zhihu.com/question/21486706

由于编译成 .class 这个行为是在程序执行的时候进行的,因为被称为“即时编译”(即JIT,just in time)。你也可以先 javac 编译后,再 java 命令 执行。

1.热点编译

官方的 java 实现是 Oracle 的 HotSpot JVM。HotSpot 的名字来与它看待代码编译的方式。对于程序来说,通常只有一部分代码被经常执行,而应用的性能就取决于这些代码执行得有多快。这些关键代码段被称为应用的热点,代码执行得越多就被认为是越热。

因此 JVM 执行代码时,并不会立即编译代码。原因1:如果代码只执行一次,那编译完全就是浪费精力。对于只执行一次的代码,解释执行 Java 字节码比先编译然后执行的速度快。原因2:JVM 执行特定方法或者循环的次数越多,它就会越了解这段代码,使得 JVM 可以在编译代码时进行大量优化。

例,equals() 方法,存在每个 Java 对象中,并且经常被子类重写。当解释器遇到 b = obj1.equals(obj2) 语句时,为了知道该执行哪个 equals() ,必须先查找 obj1 的类。这个动态查找的过程有点消耗时间。

寄存器和主内存:

1
2
3
4
5
6
7
8
9
>public class RegisterTest {
> private int sum;
> public void calculateSum(int n) {
> for(int i = 0; i < n; i++) {
> sum += i;
> }
> }
>}
>

实例变量如果一直存在主内存中,但是从主内存获取数据是非常昂贵的操作,需要花费多个时钟周期才能完成,这样性能就会比较低,编译器就不会这么做,它会将 sum 的初始值装入寄存器,用寄存器中的值执行循环,然后(某个不确定时刻)将最终的结果从寄存器写回主内存。
使用寄存器是编译器普遍采用的优化方法,当开启逃逸分析(escape analysis)时,寄存器的使用更为频繁(详见本章尾)。

比如,随着时间流逝, JVM 发现每次执行这条语句时,obj1 的类型都是 java.lang.String。于是 JVM 就可以生成直接调用 String.equals() 的编译代码。现在代码更快乐,不仅是因为被编译,也是因为跳过了查找该调用哪个方法的步骤。
不过没那么简单,下次执行代码时,obj1 完全有可能是别的类型而不是 String ,所以 JVM 必须生成编译代码处理这种可能,尽管如此,由于跳过了方法查找的步骤,这里的编译代码整体性能仍然要快(至少和 obj1 一直是 String时同样快)。这种优化只有在代码运行过一段时间观察它如何做之后才能使用:这是为何 JIT 编译器等待代码编译的第二个原因。

2 调优入门:选择编译器类型(client/server或两者同用)

有两种 JIT 编译器,client 和 server。两者编译器的最主要的差别在于编译代码的时机不同。
client 编译器开启编译比 server 编译器要早。意味着在代码执行的开始阶段,client 编译器比 server 编译器要快,因为它编译代码相比 server 编译器而言要多。
server 编译器等待编译的时候是否还能做更有价值的事:server 编译器在编译代码时可以更好地进行优化。最终,server 编译器生成的代码要比 client 编译器快。
此处的问题:为什么需要人来做这种选择?为什么 JVM 不能在启动用 client 编译器,然后随着代码变热使用 server 编译器?这种技术被称为分层编译。java7 的分层编译容易超出 JVM 代码缓存的大小,默认关闭。在 java8 分层编译默认为开启。
即时应用永远运行, server 编译器也不可能编译它的所有代码,但是任何程序都有一小部分代码很少执行,最好是编译这些代码——即便编译不是最好的方法——而不是以解释模式运行。

对于长时间运行的应用来说,应该一直使用 server 编译器,最好配合分层编译器。

3 Java 和 JIT 编译器版本

JIT 编译器有 3 种版本:

  1. 32位 client 编译器(-client)
  2. 32位 server 编译器(-server)
  3. 64位 server 编译器(-d64)

4 编译器中级调优

1.调优代码缓存

JVM 编译代码时,会在代码缓存中保存编译之后的汇编语言指令集。代码缓存一旦填满,JVM 就不能编译更多代码了(只能解释执行其余代码了)。
但是,如果设置过多,例如设置代码缓存为 1GB,JVM 就会保留 1GB 的本地内存空间。然后这部分内存在需要时才会分配,但它仍然是保留的,这意味着为了满足保留内存,你的机器必须有足够的虚拟内存。
此外,如果是 32位 JVM,则进程占用的总内存不能超过 4GB。这包括 Java堆、JVM 自身所有嗲吗占用的空间(包括它的本地库和线程栈)、分配给应用的本地内存(或者 NIO 库的直接内存),当然还有代码缓存。
代码缓存: -XX:ReservedCodeCacheSize=N ,可以设置代码缓存的最大值。

2.编译阈值

编译时基于两种 JVM 计数器的:方法调用计数器和方法中的循环回边计数器。回边实际上可看作是循环完成执行的次数。
JVM 执行某个 Java 方法时,会检查该方法的两种计数器总数,然后判定该方法是否适合编译。如果适合就进入编译队列。被称为标准编译。
如果循环真的很长——或因包含所有程序逻辑而永远不退出,JVM 不等方法被调用就会编译循环。所以循环每完成一轮,回边计数器就会增加并被检测。如果循环的回边计数器超过阈值,那这个循环(不是整个方法)就可以被编译。被称为栈上替换(On-Stack Replacement,OSR)。

标准编译由: -XX:CompileThreshold=N 标志触发。

5 高级编译器调优

前面说道,当方法(或循环)适合编译时,就会进入到编译队列。队列则由一个或多个后台线程处理。这是件好事,意味着编译过程是异步的,这使得即便是代码正在编译的时候,程序也能持续执行。如果是用标准编译所编译的方法,那下次调用该方法时就会执行编译后的方法;如果是用 OSR 编译的循环,那下次循环迭代时就会执行编译后的代码。

编译队列并不严格遵守先进先出的原则:调用次数多的方法有更高的优先级(非公平更好使)。

5.1 逃逸分析

开启逃逸分析: -XX:DoEscapeAnalysis,默认为 true。server 编译器将会执行一些非常激进的优化措施,例如, for 循环中的新建变量,如果对象只在循环中引用,JVM 会毫不犹豫地对这个对象进行一系列优化。
包括,锁去除,值存储在寄存器而不是内存中,甚至不需要分配实际的对象,可以只追踪这个对象的个别字段。

6 逆优化

有两种逆优化的情形:代码状态分别为“made not entrant”(代码被丢弃)和“made zombie”(产生僵尸代码)时。

6.1 代码被丢弃

当一个接口有多重实现,在使用switch 进行工厂模式的创建时,可能上次的编译器内联,在下次就必须使用解释执行了,因为对象变了,要开始新的编译,而上次的编译代码就属于丢弃代码。
另一种情况就是,分层编译。先使用 client 编译,再使用 server 编译,那么在第二次编译时,第一次编译的一些代码就要被丢弃,属于丢弃代码。

6.2 逆优化僵尸代码

  1. 逆优化使得编译器可以回到之前版本的编译代码。
  2. 先前的优化不再有效时(例,所设计的对象类型发生了更改),才会发生代码逆优化。
  3. 代码逆优化时,会对性能产生一些小而短暂的影响。

7 小结

  1. 不用担心小方法,特别是 getter 和 setter ,因为它们很容易内联。编译器会修复这些问题。
  2. 需要编译的代码在编译队列中。队列中代码越多,程序达到最佳性能的时间越久。
  3. 虽然代码缓存的大小(也应该)调整,但它仍然是有限的资源。
  4. 代码越简单,优化越多。分析反馈和逃逸分析可以使代码更快,但复杂的循环结果和大方法限制了它的有效性。