简介

在本教程中,我们将研究Java虚拟机中的内联方法及其工作方式。我们还将看到如何从JVM获取和读取与内联相关的信息,以及如何使用此信息来优化我们的代码。

内联方法是什么

基本上,内联是通过将最常执行的方法的调用替换为其主体来优化运行时已编译源代码的方法。

尽管涉及到编译,但是它不是由传统的javac编译器执行,而是由JVM本身执行。更准确地说,这是实时(JIT)编译器的责任, 它是JVM的一部分。javac只产生一个字节码,然后让JIT发挥作用并优化源代码。

这种方法最重要的意义之一是,如果我们使用旧的Java编译代码,则相同。类文件在较新的JVM上将更快。这样,我们不需要重新编译源代码,而只需更新Java。

JIT如何做到

本质上,JIT编译器尝试内联我们经常调用的方法,以便我们可以避免方法调用的开销。 在决定是否内联方法时,要考虑两点。

首先,它使用计数器来跟踪我们调用该方法的次数。当该方法被调用超过特定次数时,它将变为“热”。默认情况下,此阈值设置为10000,但是我们可以在Java启动期间通过JVM标志对其进行配置。我们绝对不希望内联所有内容,因为这将很耗时并且会产生巨大的字节码。

我们应该记住,只有当我们达到稳定状态时才会进行内联。这意味着我们将需要重复执行几次,以为JIT编译器提供足够的配置信息。

此外,“热”并不能保证该方法会被内联。如果太大,则JIT不会内联它。可接受的大小受-XX:FreqInlineSize=flag限制,该标志指定要内联方法的最大字节数。

但是,强烈建议不要更改此标志的默认值,除非我们绝对并确定知道它会产生什么影响。默认值取决于平台 – 对于64位Linux,为325。

JIT通常内联静态、私有或最终方法。尽管公共方法也是内联的候选对象,但并非每种公共方法都必须内联。JVM需要确定这种方法只有一个实现。 任何其他子类都将阻止内联,并且性能将不可避免地下降。

寻找热方法

我们当然不想猜测JIT在做什么。因此,我们需要某种方式来查看内联或未内联的方法。通过在启动过程中设置一些其他JVM标志,我们可以轻松实现并将所有这些信息记录到标准输出中:

-XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining

JIT编译发生时,第一个标志将记录。第二个标志启用其他标志,包括-XX:+PrintInlining,这些标志将打印要内联哪些方法以及在何处内联。

这将以树的形式向我们展示内联方法。叶子被注释并标记有以下选项之一:

  • inline (hot) – 此方法标记为热并内联
  • oo big - 该方法不是很热,但是它生成的字节码也太大,因此没有内联
  • hot method too big - 这是一个热方法,但是由于字节码太大,所以未内联

我们应注意第三个值,并尝试优化标记为“hot method too big”的方法。

通常,如果我们发现带有非常复杂的条件语句的热方法,则应尝试分离if语句的内容并增加粒度,以便JIT可以优化代码。switch和for-loop语句也是如此。

我们可以得出结论,为了优化我们的代码,不需要手动进行方法内联。JVM可以更有效地执行它,我们可能会使代码冗长且难以遵循(follow)。

范例

现在让我们看一下如何在实践中进行检查。我们首先创建一个简单的类,该类计算前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 = 0; 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);
}

在第一次运行中,我们将其运行1000次(小于上述阈值10000)。如果我们在输出中搜索calculateSum()方法,将找不到它。这是可以预期的,因为我们没有足够多次地调用它。

如果现在将迭代次数更改为15000,然后再次搜索输出,我们将看到:

664 262 % com.baeldung.inlining.InliningExample::main @ 2 (21 bytes)
  @ 10   com.baeldung.inlining.InliningExample::calculateSum (12 bytes)   inline (hot)

我们可以看到,这一次该方法满足了内联的条件,并且JVM对其进行了内联。

再次值得一提的是,如果方法太大,那么无论迭代次数如何,JIT都不会内联它。我们可以通过在运行应用程序时添加另一个标志来检查此情况:

-XX:FreqInlineSize=10

正如我们在前面的输出中看到的,我们的方法的大小为12个字节。 -XX:FreqInlineSize标志将可进行内联的方法大小限制为10个字节。因此,这次不应该进行内联。实际上,我们可以通过再次查看输出来确认这一点:

330 266 % com.baeldung.inlining.InliningExample::main @ 2 (21 bytes)
  @ 10   com.baeldung.inlining.InliningExample::calculateSum (12 bytes)   hot method too big

尽管我们出于说明目的已在此处更改了标志值,但必须强调建议除非绝对必要,否则不要更改-XX:FreqInlineSize标志的默认值。

总结

在本文中,我们看到了JVM中的内联方法以及JIT如何做到的。我们描述了如何检查我们的方法是否适合进行内联,并建议了如何通过尝试减小太大而无法内联的长方法的大小来利用此信息。

最后,我们说明了如何在实践中确定热方法。

Method Inlining in the JVM