通过看字节码指令扒光 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 参数传递给普通的函数时,就需要禁止内联。

相关推荐
一丝晨光13 分钟前
Java、PHP、ASP、JSP、Kotlin、.NET、Go
java·kotlin·go·php·.net·jsp·asp
Ray Wang5 小时前
3.JVM
jvm
500了10 小时前
Kotlin基本知识
android·开发语言·kotlin
java66666888821 小时前
Java中的对象生命周期管理:从Spring Bean到JVM对象的深度解析
java·jvm·spring
生产队队长21 小时前
JVM(HotSpot):字符串常量池(StringTable)
jvm
Yuan_o_1 天前
JVM(Java Virtual Machine) 详解
jvm
派大星-?1 天前
JVM内存回收机制
jvm
陈亦康1 天前
Armeria gPRC 高级特性 - 装饰器、无框架请求、阻塞处理器、Nacos集成、负载均衡、rpc异常处理、文档服务......
kotlin·grpc·armeria
G丶AEOM1 天前
Hotspot是什么?
jvm·hotspot
奋斗的小鹰2 天前
kotlin 委托
android·开发语言·kotlin