简介
本章节,我们将研究Java虚拟机中的方法内联及其工作原理。 我们将学习如何从JVM中获取和读取与内联相关的信息,以及如何使用此信息来优化我们的代码。
什么是方法内联
基本上, 内联是一种优化已编译源源码的方式,通常将最常执行的方法调用(也称之为热点),在运行时替换为方法主体,以便减少调用成本. 尽管涉及到编译, 但是它不是由传统的 javac 编译器执行, 而是由 JVM 本身执行. 更准确地说, 这是实时编译器 Just-In-Time (JIT) 的责任, 它是 JVM的一部分; javac 只是生成字节码, 然后让 JIT 发挥作用并优化源代码.
JIT 的工作原理
本质上, JIT 编译器尝试内联我们经常调用的方法,以便我们可以避免方法调用的开销. 在决定是否内联方法时,需要考虑两点. 首先, 它使用计数器来记录我们调用该方法的次数. 当该方法被调用超过特定次数时, 它将变为“hot”. 默认情况下, 此阈值被设置为 10,000 , 但是我们可以在启动时通过JVM参数设置来改变它. 我们绝对不希望内联所有内容, 因为这将很耗时并且会产生庞大的字节码. 我们应该知道,只有当我们达到稳定状态时才会内联. 这句话的意思是,我们需要重复执行几次,才能为JIT编译器提供足够的信息来判断. 其次, “hot” 并不能保证方法一定会被内联如果方法太大, JIT也不会对其进行内联. 具体大小可以通过 -XX:FreqInlineSize= size 设置, 该值为方法内联的最大字节码指令数. 但是, 强烈建议不要更改默认值,除非我们能绝对确定知道它会产生的具体影响。 默认值取决于平台 – 对于64位 Linux, 默认值是 325. JIT 通常会内联 static, private, 或 final 方法. 虽然 public 方法也可能被内联, 但是并非每一个 public 方法都能被内联. JVM 需要确定public的方法只有一个实现. 任何其他子类都将阻止内联, 并且性能不可避免地会下降.
定位Hot方法
我们不能想当然的猜测JIT在做什么. 因此, 我们需要某种方式来查看哪些方法被内联或未被内联. 通过在启动的时候设置一些额外的JVM参数,我们可以获取这些记录信息,并输出到标准输出中:
-XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining
第一个参数-XX:+PrintCompilation 表示打开编译日志,当JVM对方法进行编译的时候,都会打印一行信息,什么方法被编译了. 第二个参数表示启用其他参数,即-XX:+PrintInlining,第三个参数表示将打印哪些方法被内联以及在何处内联。这将以树的形式向我们展示内联方法. 叶子被注释并标记以下选项之一:
- inline (hot) – 该方法被标记为hot并且被内联
- too big – 该方法不是很 hot, 同时它生成的字节码太大, 所以没有被内联
- hot method too big – 这是一个 hot 方法, 但是因为字节码太大,所以未被内联
我们应该多加关注第三种情况,尝试去优化被标记未“hot method too big”的方法. 通常, 如果存在带有非常复杂的条件语句的热门方法,我们应该尝试分离 if-语句的内容,以便JIT可以优化代码. switch和for-loop语句也是如此. 因此,我们可以得出结论,我们无需去配置方法内联,JVM会自动有效的去帮助我们完成方法内联。
示例
让我们通过一个示例证实我们上面的理论. 我们首先创建一个简单的类,该类计算前N个连续的正整数之和:
public class ConsecutiveNumbersSum {
private long totalSum;
private int totalNumbers;
public ConsecutiveNumbersSum(int totalNumbers) {
this.totalNumbers = totalNumbers;
}
public long getTotalSum() {
totalSum = 0;
for (int i = 1; i <= totalNumbers; i++) {
totalSum += i;
}
return totalSum;
}
}
接下来, 一个简单的方法将利用该类来执行计算:
private static long calculateSum(int n) {
return new ConsecutiveNumbersSum(n).getTotalSum();
}
最后, 我们将多次调用该方法, 然后看看会发生什么:
for (int i = 1; i < NUMBERS_OF_ITERATIONS; i++) {
calculateSum(i);
}
第一次运行, 我们将NUMBERS_OF_ITERATIONS设置1000,我们将calculateSum方法运行1,000次 (小于上述阈值10,000). 我们在output中搜索方法的关键字 getTotalSum 如下所示:
139 35 4 com.bern.inlining.ConsecutiveNumbersSum::getTotalSum (37 bytes)
如果现在将迭代次数更改为15,000 ,然后再次搜索, 我们将看到:
158 44 4 com.bern.inlining.InliningExample::calculateSum (12 bytes)
@ 5 com.bern.inlining.ConsecutiveNumbersSum::<init> (10 bytes) inline (hot)
@ 1 java.lang.Object::<init> (1 bytes) inline (hot)
@ 8 com.bern.inlining.ConsecutiveNumbersSum::getTotalSum (37 bytes) inline (hot)
我们可以看到, 这一次该方法满足了内联的条件, 并且JVM对其进行了内联. (不同的JVM,或者不同的版本,不同的平台输出都可能不一样,仅供参考)。 再次需要重提的是,如果方法太大,则无论迭代多少次,JIT都不会对它进行内联. 我们可以在运行时通过设置另一个参数来进行验证:
-XX:FreqInlineSize=10
正如我们在前面的输出中看到的,getTotalSum方法的大小为37 bytes. 参数 -XX:FreqInlineSize 将可进行内联的方法大小限制为10 bytes. 因此, 这次不会对方法进行内联. 实际上, 我们可以通过再次查看输出来确认这一点:
134 43 4 com.bern.inlining.InliningExample::calculateSum (12 bytes)
@ 5 com.bern.inlining.ConsecutiveNumbersSum::<init> (10 bytes) inline (hot)
@ 1 java.lang.Object::<init> (1 bytes) inline (hot)
@ 8 com.bern.inlining.ConsecutiveNumbersSum::getTotalSum (37 bytes) too big
尽管我们出于说明目的已在此处更改了参数值, 但必须强调除非绝对必要, 否则不要更改-XX:FreqInlineSize 参数的默认值.
结论
在本文中, 我们了解了JVM中哪些方法可以内联以及JIT如何工作的. 我们介绍了如何检查我们的方法是否被内联,并建议通过尝试减小太大的方法而使得JIT有助于对方法进行内联,减少方法调用,提升性能. 最后, 我们通过实践说明了如何确定热门方法。
所有代码都已经上传至 GitHub.
附录
package com.bern.inlining;
import java.util.Random;
public class InlineExamping {
private static final int COUNT = 2000000000;
public static void main(String[] args) {
System.out.println(arrayCompute() + " " + virtualCompute() + " " + interfaceCompute());
}
static long arrayCompute() {
InliningInterface[] array = new InliningInterface[4];
array[0] = (x,y) -> x + y;
array[1] = (x,y) -> x + x + y;
array[2] = (x,y) -> x + y + y;
array[3] = (x,y) -> x - y;
long start = System.currentTimeMillis();
Random r = new Random(start);
int x = r.nextInt(10);
int y = r.nextInt(10);
for (int i = 0; i < COUNT; i++) {
for (InliningInterface item : array) {
item.compute(x, y);
}
}
return System.currentTimeMillis() - start;
}
static long virtualCompute() {
InliningInterface A = (x,y) -> x + y;
InliningInterface B = (x,y) -> x + x + y;
InliningInterface C = (x,y) -> x + y + y;
InliningInterface D = (x,y) -> x - y;
long start = System.currentTimeMillis();
Random r = new Random(start);
int x = r.nextInt(10);
int y = r.nextInt(10);
for (int i = 0; i < COUNT; i++) {
A.compute(x, y);
B.compute(x, y);
C.compute(x, y);
D.compute(x, y);
}
return System.currentTimeMillis() - start;
}
static long interfaceCompute() {
InliningInterface[] array = new InliningInterface[4];
array[0] = (x,y) -> x + y;
array[1] = (x,y) -> x + x + y;
array[2] = (x,y) -> x + y + y;
array[3] = (x,y) -> x - y;
long start = System.currentTimeMillis();
Random r = new Random(start);
int x = r.nextInt(10);
int y = r.nextInt(10);
for (int i = 0; i < COUNT; i++) {
array[0].compute(x, y);
array[1].compute(x, y);
array[2].compute(x, y);
array[3].compute(x, y);
}
return System.currentTimeMillis() - start;
}
interface InliningInterface {
int compute(int x, int y);
}
}
大家可以自行运行上面的示例,得到的结果肯定会让大家感到惊讶。
网友评论