通过看字节码指令扒光 Kotlin 内联函数的底裤

通过看字节码指令扒光 Kotlin 内联函数的底裤

网上非常多讲 Kotlin 内联函数的文章,绝大部分都是告诉你结论,用了 xxx 关键字就怎么怎么样,过一段时间就又忘了,本篇文章带你从 JVM 字节码来一点一点带你分析它的原理,在开始之前你需要有基础的 JVM 字节码指令的相关知识,如果没有可以参考我之前的文章 JVM 字节码。如果有忘记了的字节码指令可以查看这个文档 字节码指令

普通函数调用

我定义了这样一个类:

Kotlin 复制代码
package com.tans.test

object Main {

    @JvmStatic
    fun main(args: Array<String>) {
        foo1()
    }

    fun foo1() {
        val data: Int = 1
        val returnData = foo2(data) {
            println("Callback: do something")
        }
        println("Foo1: returnData=$returnData")
    }

    fun <T> foo2(data: T, callback: () -> Unit): T {
        println("Foo2: do something")
        callback()
        return data
    }
}

代码很简单,我们主要分析函数 foo1()foo2(),查看字节码用的工具是 jclasslib,废话不多说直接上字节码。

foo1() 函数字节码指令:

less 复制代码
 0 iconst_1
 1 istore_1
 2 aload_0
 3 iload_1
 4 invokestatic #35 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
 7 getstatic #40 <com/tans/test/Main$foo1$returnData$1.INSTANCE : Lcom/tans/test/Main$foo1$returnData$1;>
10 checkcast #42 <kotlin/jvm/functions/Function0>
13 invokevirtual #46 <com/tans/test/Main.foo2 : (Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;>
16 checkcast #48 <java/lang/Number>
19 invokevirtual #52 <java/lang/Number.intValue : ()I>
22 istore_2
23 ldc #54 <Foo1: returnData=>
25 iload_2
26 invokestatic #35 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
29 invokestatic #58 <kotlin/jvm/internal/Intrinsics.stringPlus : (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String;>
32 astore_3
33 iconst_0
34 istore 4
36 getstatic #64 <java/lang/System.out : Ljava/io/PrintStream;>
39 aload_3
40 invokevirtual #70 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
43 return

我分析一些我自认为有价值的字节码:

vbnet 复制代码
4 invokestatic #35 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>

在入参前会通过 Integer.valueOf() 的静态方法把,int 变量转换成 Integer 对象,这也就是我们常说的装箱。

less 复制代码
7 getstatic #40 <com/tans/test/Main$foo1$returnData$1.INSTANCE : Lcom/tans/test/Main$foo1$returnData$1;>
10 checkcast #42 <kotlin/jvm/functions/Function0>
13 invokevirtual #46 <com/tans/test/Main.foo2 : (Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;>
16 checkcast #48 <java/lang/Number>
19 invokevirtual #52 <java/lang/Number.intValue : ()I>
22 istore_2

这里会拿到一个 Main$foo1$returnData$1 静态单例对象,其实就是我们的 labmda 表达式对象,然后强制转换成 Function0 对象,然后入栈,然后调用 foo2() 函数,我们仔细看看 foo2() 这个函数的签名,com/tans/test/Main.foo2 : (Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;,传入一个 ObjectFuction0 (在 Kotlin 中他就表示无参数的 lambda),返回值也是 Object,我们明明定义的是传入一个范形,返回也是一个范形,这怎么就用 Object 代替了呢?其实这就是常说的范形擦除,JVM 中的范形是伪范形,运行时方法中的范形都是通过强转来实现的,这也解释了普通方法的范形是不可以通过 T::class 去拿他的 class 对象的,就算拿到的也是 Objectclass 对象。最后返回值会被强制转换成 Number 对象,然后调用其 intValue() 方法完成拆箱操作。

vbnet 复制代码
23 ldc #54 <Foo1: returnData=>
25 iload_2
26 invokestatic #35 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
29 invokestatic #58 <kotlin/jvm/internal/Intrinsics.stringPlus : (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String;>
32 astore_3
33 iconst_0
34 istore 4
36 getstatic #64 <java/lang/System.out : Ljava/io/PrintStream;>
39 aload_3
40 invokevirtual #70 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
43 return

这里就是通过 Intrinsics#stringPlus 方法组合 "Foo1: returnData="foo2() 方法的返回值构成一个新的 String 对象,然后调用 System.out#println() 方法打印到控制台。

我们再看看上面说到的 lambda 对象,他的类名是 com/tans/test/Main$foo1$returnData$1,我们来看看他的 invoke() 方法的字节码指令:

bash 复制代码
 0 ldc #17 <Callback: do something>
 2 astore_1
 3 iconst_0
 4 istore_2
 5 getstatic #23 <java/lang/System.out : Ljava/io/PrintStream;>
 8 aload_1
 9 invokevirtual #29 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
12 return

这个字节码指令非常简单,直接从常量池中加载 Callback: do something 然后调用 System.out#println() 方法打印到控制台。

我们再来看看 foo2() 方法的字节码指令:

less 复制代码
 0 aload_2
 1 ldc #76 <callback>
 3 invokestatic #22 <kotlin/jvm/internal/Intrinsics.checkNotNullParameter : (Ljava/lang/Object;Ljava/lang/String;)V>
 6 ldc #78 <Foo2: do something>
 8 astore_3
 9 iconst_0
10 istore 4
12 getstatic #64 <java/lang/System.out : Ljava/io/PrintStream;>
15 aload_3
16 invokevirtual #70 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
19 aload_2
20 invokeinterface #82 <kotlin/jvm/functions/Function0.invoke : ()Ljava/lang/Object;> count 1
25 pop
26 aload_1
27 areturn

方法首先调用 System.out#println() 方法打印 Foo2: do something,然后调用第二个参数 Function0#invoke() 方法,也就是调用 lambda 对象,最后将输入的第一个参数当返回值返回。

普通函数调用的字节码指令分析就结束了,我们从字节码指令的角度看了 int 的装箱和拆箱、Kotlinlambda 实现和范形的擦除。

内联函数

普通内联函数

Kotlin 中想要让函数为内联函数添加一个 inline 关键字就好了,我们把上面的 foo2() 函数改造成内联函数:

Kotlin 复制代码
    // ...
    inline fun <T> foo2(data: T, callback: () -> Unit): T {
        println("Foo2: do something")
        callback()
        return data
    }
    // ...

然后我们再来看看 foo1() 方法的字节码指令:

bash 复制代码
 0 iconst_1
 1 istore_1
 2 aload_0
 3 astore_3
 4 iload_1
 5 istore 4
 7 iconst_0
 8 istore 5
10 ldc #31 <Foo2: do something>
12 astore 6
14 iconst_0
15 istore 7
17 getstatic #37 <java/lang/System.out : Ljava/io/PrintStream;>
20 aload 6
22 invokevirtual #43 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
25 iconst_0
26 istore 8
28 ldc #45 <Callback: do something>
30 astore 9
32 iconst_0
33 istore 10
35 getstatic #37 <java/lang/System.out : Ljava/io/PrintStream;>
38 aload 9
40 invokevirtual #43 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
43 nop
44 iload 4
46 istore_2
47 ldc #47 <Foo1: returnData=>
49 iload_2
50 invokestatic #53 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
53 invokestatic #57 <kotlin/jvm/internal/Intrinsics.stringPlus : (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String;>
56 astore_3
57 iconst_0
58 istore 4
60 getstatic #37 <java/lang/System.out : Ljava/io/PrintStream;>
63 aload_3
64 invokevirtual #43 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
67 return

简单浏览一下这个字节码,其中没有调用 foo2() 方法了,也没有构建 labmda 对象了,我们再来简单分析他的流程,由于该方法的本地变量表变得有些复杂,我把它的表中的内容贴一下。

Slot
0 this(Main 对象)
1 1
2 -
3 this(Main 对象)
4 1
5 0
6 "Foo2: do something"
7 0
8 0
9 "Callback: do something"
10 0

我刚开始看到这个本地变量表的时候也懵了,发现其中存放了很多对象都是重复的,而且 Slot 2 还是空的,我不清楚这是 Kotlin 编译器没有优化好 inline 函数,还是由于别的原因故意为之。

一点一点来看看字节码都做了啥。

bash 复制代码
10 ldc #31 <Foo2: do something>
12 astore 6
14 iconst_0
15 istore 7
17 getstatic #37 <java/lang/System.out : Ljava/io/PrintStream;>
20 aload 6
22 invokevirtual #43 <java/io/PrintStream.println : (Ljava/lang/Object;)V>

打印 Foo2: do something 到控制台。

bash 复制代码
25 iconst_0
26 istore 8
28 ldc #45 <Callback: do something>
30 astore 9
32 iconst_0
33 istore 10
35 getstatic #37 <java/lang/System.out : Ljava/io/PrintStream;>
38 aload 9
40 invokevirtual #43 <java/io/PrintStream.println : (Ljava/lang/Object;)V>

打印 Callback: do something 到控制台。

vbnet 复制代码
47 ldc #47 <Foo1: returnData=>
49 iload_2
50 invokestatic #53 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
53 invokestatic #57 <kotlin/jvm/internal/Intrinsics.stringPlus : (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String;>
56 astore_3
57 iconst_0
58 istore 4
60 getstatic #37 <java/lang/System.out : Ljava/io/PrintStream;>
63 aload_3
64 invokevirtual #43 <java/io/PrintStream.println : (Ljava/lang/Object;)V>

打印 Foo1: returnData= + 1 到控制台。

以上的字节码等于以下源码:

Kotlin 复制代码
    // ...
    fun foo1() {
        val data: Int = 1
        println("Foo2: do something")
        println("Callback: do something")
        println("Foo1: returnData=$data")
    }
    // ...

根据以上结果对内联函数做一个总结,他会把内联函数中的字节码直接移动到当前函数中执行,也包括 lambda 中的指令。这样做可以在运行时减少方法栈帧的创建,减少 lambda 对象的创建,在一定条件下可以提高程序性能;但是坏处也非常明显,如果调用的地方非常多,同时内联函数的逻辑比较复杂,那它会导致 class 所占用的空间明显增大,因为他的字节码要被复制到所有的调用的方法中。

为范形添加 reified 关键字

这里直接说结论添加不添加 reified 关键字,编译出来的字节码指令都是完全一样的,哈哈,我也没想到,reified 关键字标记后的范形是可以直接在内联函数中直接获取范形对象的 class 对象。

那我们把 foo2() 函数修改成以下:

Kotlin 复制代码
    // ...
    inline fun <reified T> foo2(data: T, callback: () -> Unit): T {
        val clazz = data!!::class.java
        println("Foo2: do something")
        callback()
        return data
    }
    // ...

这里如果没有 reified 关键字, data!!::class.java 是编译过不了的。

看看字节码:

less 复制代码
 0 iconst_1
 1 istore_1
 2 aload_0
 3 astore_3
 4 iload_1
 5 invokestatic #35 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
 8 astore 4
10 iconst_0
11 istore 5
13 aload 4
15 invokevirtual #39 <java/lang/Object.getClass : ()Ljava/lang/Class;>
18 astore 6
20 ldc #41 <Foo2: do something>
22 astore 7
24 iconst_0
25 istore 8
27 getstatic #47 <java/lang/System.out : Ljava/io/PrintStream;>
30 aload 7
32 invokevirtual #53 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
35 iconst_0
36 istore 9
38 ldc #55 <Callback: do something>
40 astore 10
42 iconst_0
43 istore 11
45 getstatic #47 <java/lang/System.out : Ljava/io/PrintStream;>
48 aload 10
50 invokevirtual #53 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
53 nop
54 aload 4
56 checkcast #57 <java/lang/Number>
59 invokevirtual #61 <java/lang/Number.intValue : ()I>
62 istore_2
63 ldc #63 <Foo1: returnData=>
65 iload_2
66 invokestatic #35 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
69 invokestatic #67 <kotlin/jvm/internal/Intrinsics.stringPlus : (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String;>
72 astore_3
73 iconst_0
74 istore 4
76 getstatic #47 <java/lang/System.out : Ljava/io/PrintStream;>
79 aload_3
80 invokevirtual #53 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
83 return

和上面普通的内联函数没有什么特殊的操作,只是增加了获取 class 对象的逻辑,如下:

vbnet 复制代码
 5 invokestatic #35 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
 8 astore 4
10 iconst_0
11 istore 5
13 aload 4
15 invokevirtual #39 <java/lang/Object.getClass : ()Ljava/lang/Class;>
18 astore 6

直接调用 Integer 对象的 getClass() 方法获取 class 对象。

目前看上去 reified 关键字在字节码指令中没有什么特殊作用,我猜测 reified 只是一个标记作用,可能和字节码优化有关。

为 lambda 参数添加 crossinline 关键字

这里直接给结论 crossinline 是不会修改字节码指令和 reified 关键字一样,惊不惊喜,意不意外,哈哈哈哈哈。那它是用来干嘛的呢?这个关键字是用来控制 lambda 中的 return 的。

假如我在上面的 lambda 中用了 return,如以下代码:

Kotlin 复制代码
    // ...
    fun foo1() {
        val data: Int = 1
        val returnData = foo2(data) {
            println("Callback: do something")
            return
        }
        println("Foo1: returnData=$returnData")
    }

    inline fun <T> foo2(data: T,  callback: () -> Unit): T {
        println("Foo2: do something")
        callback()
        return data
    }
    // ...

由于内联的特性上面的方法就会直接返回 foo1() 函数,如果是要返回 lambda 就得这么用 return@foo2;如果不是内联函数是禁止在 foo2()lambda 中返回 foo1() 函数。

crossinline 它就是用来限制上面例子的返回方式,不能够在 foo2()labmda 中返回 foo1() 函数。

假如我加了一个 foo3() 函数如下:

Kotlin 复制代码
    // ...
        fun foo1() {
        val data: Int = 1
        val returnData = foo2(data) {
            println("Callback: do something")
        }
        println("Foo1: returnData=$returnData")
    }

    inline fun <T> foo2(data: T, callback: () -> Unit): T {
        foo3(callback)
        println("Foo2: do something")
        callback()
        return data
    }

    inline fun foo3(crossinline callback: () -> Unit) {
        
    }
    // ...

其实上面的代码是编译无法通过的,因为 foo2() 中没有添加 crossinline,而 foo3() 中有添加 crossinline,但是 foo2() 又把 lambda 传给 foo3() 了,那 foo2()foo3() 的定义就冲突了,一个允许 return,一个不允许 return,所以 foo2()foo3() 必须同时没有 crossinline 或者同时有 crossinline

为 lambda 参数添加 noinline 关键字

我把普通内联函数章节中的代码修改为如下:

Kotlin 复制代码
    // ...
    inline fun <T> foo2(data: T, noinline callback: () -> Unit): T {
        println("Foo2: do something")
        callback()
        return data
    }
    // ...

这次对应的字节码有改变了,参考以下:

less 复制代码
 0 iconst_1
 1 istore_1
 2 aload_0
 3 astore_3
 4 iload_1
 5 istore 4
 7 getstatic #34 <com/tans/test/Main$foo1$returnData$1.INSTANCE : Lcom/tans/test/Main$foo1$returnData$1;>
10 checkcast #36 <kotlin/jvm/functions/Function0>
13 astore 5
15 iconst_0
16 istore 6
18 ldc #38 <Foo2: do something>
20 astore 7
22 iconst_0
23 istore 8
25 getstatic #44 <java/lang/System.out : Ljava/io/PrintStream;>
28 aload 7
30 invokevirtual #50 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
33 aload 5
35 invokeinterface #54 <kotlin/jvm/functions/Function0.invoke : ()Ljava/lang/Object;> count 1
40 pop
41 iload 4
43 istore_2
44 ldc #56 <Foo1: returnData=>
46 iload_2
47 invokestatic #62 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
50 invokestatic #66 <kotlin/jvm/internal/Intrinsics.stringPlus : (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String;>
53 astore_3
54 iconst_0
55 istore 4
57 getstatic #44 <java/lang/System.out : Ljava/io/PrintStream;>
60 aload_3
61 invokevirtual #50 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
64 return

加了 noinline 以后会禁止 lambda 内联化,和普通的 lambda 一样会是一个对象。

在某些情况下,我们不能让 lambda 内联化,而是需要一个 lambda 对象,例如以下代码:

Kotlin 复制代码
    // ...
    inline fun <T> foo2(data: T, callback: () -> Unit): T {
        foo3(callback)
        println("Foo2: do something")
        callback()
        return data
    }

    fun foo3(callback: () -> Unit) {

    }
    // ...

以上代码是无法编译的,foo2() 是一个内联函数,而 foo3() 不是,foo2() 会把 lambda 内联化,而 foo3() 需要的 lambda 必须是一个对象,为了编译能够给通过就可以通过在 foo2() 中的 callback 加上 noinline 来阻止其内联化,这样 foo2()foo3() 都是非内联的 lambda 对象,这样就可以通过编译了。

总结

inline

函数前加 inline 关键字能让函数内联化,包括其中的 lambda 参数,其中的字节码指令会等效地移动到调用的函数中。优点是一定程度上能够提升程序的运行效率;缺点是会导致字节码文件变大。要合理使用。

reified

在内联函数中的范形中添加,它可以让添加后的范形对象能够直接拿到 Class 对象,这个关键字不会修改字节码指令。

crossinline

在内联函数的 lambda 参数中添加,禁止内联后的 lambda 来返回上一个层级函数的方法,如果内联函数把这个 crossinlinelambda 当参数传递给另外的内联函数当参数,那么这个新调用内联函数中的 lambda 参数也必须是 crossinline 的。读上去有点绕,反正 IDEA 会提示你的。它也会不修改字节码指令。

noinline

在内联函数的 lambda 参数中添加,禁止 lambda 参数内联,当某些情况需要对象化的 lambda 对象时使用,比如一个内联函数中想要把他的 lambda 参数传递给普通的函数时,就需要禁止内联。

相关推荐
A_Tai23333333 小时前
JVM方法区
jvm
划水哥~21 小时前
Kotlin函数式API
java·开发语言·kotlin
鲨鱼 Fish21 小时前
JVM运行时数据区域-附面试题
java·开发语言·jvm·面试
讓丄帝愛伱2 天前
jinfo命令详解
jvm
西岭千秋雪_2 天前
彻底理解JVM常量池
java·jvm
讓丄帝愛伱2 天前
jvisualvm工具使用
开发语言·jvm
天天向上杰2 天前
简识JVM中并发垃圾回收器和多线程并行垃圾回收器的区别
java·jvm·算法
java1234_小锋2 天前
JVM对象分配内存如何保证线程安全?
java·开发语言·jvm
鳗漪2 天前
JVM--类加载器
jvm
鳗漪2 天前
jvm--类的生命周期
jvm·类的声明周期