美文网首页
JVM 中的方法内联(Method Inlining)

JVM 中的方法内联(Method Inlining)

作者: bern85 | 来源:发表于2020-05-13 19:39 被阅读0次

    简介

    本章节,我们将研究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);
        }
    }
    

    大家可以自行运行上面的示例,得到的结果肯定会让大家感到惊讶。

    相关文章

      网友评论

          本文标题:JVM 中的方法内联(Method Inlining)

          本文链接:https://www.haomeiwen.com/subject/weponhtx.html