美文网首页
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)

    简介 本章节,我们将研究Java虚拟机中的方法内联及其工作原理。 我们将学习如何从JVM中获取和读取与内联相关的信...

  • JVM代码优化:方法内联(Method inlining)

    什么是方法内联 方法内联,是指JVM在运行时将调用次数达到一定阈值的方法调用替换为方法体本身,从而消除调用成本,并...

  • Kotlin - Inline Functions 1

    Inline Basics Inline or Inlining,我们更经常听到的词是方法内联或者内联函数。在大多...

  • MVC---Help Methods

    一. 创建自定义Help Method 1.1 创建内联(Inline)的Help Method 内联Help M...

  • Jvm优化技术

    Jvm优化技术有:逃逸分析、方法内联 一:Jvm优化技术之逃逸分析 1:概念 JVM的优化技术,可以有效减少Jav...

  • 《白话》--- jvm方法内联

    参考:https://time.geekbang.org/column/article/14575 什么是方法内联...

  • JVM的内存结构及GC机制

    JVM内存管理 根据JVM规范,JVM把内存划分成了如下几个区域: 方法区(Method Area) 堆区(Hea...

  • JVM内存直观描述

    JVM内存简单描述 java的JVM内存可分为3个区:堆(heap),栈(stack)和方法区(method)。 ...

  • jvm的内存结构

    内存结构 jvm和系统调用之间的关系 java堆(heap) 方法区(Method Area) 程序计数器 JVM...

  • Java基础(3)——JVM内存模型

    Java for android 基础知识。 JVM的内存结构分为: 方法区(method) 栈内存(stack)...

网友评论

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

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