JVM 中的方法内联(Method Inlining)

简介

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,922评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,591评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,546评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,467评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,553评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,580评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,588评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,334评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,780评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,092评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,270评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,925评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,573评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,194评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,437评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,154评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352