深入理解JVM(六)——从字节码来看Lambda、泛型与协程

在上一篇文章熟悉了字节码的基础指令之后,一些以往难懂的、被编译器隐藏的“黑盒子”知识点就可以尝试通过字节码去分析了。下面尝试分析 Java 8 中 Lambda 表达式与 Kotlin 中 Lambda 表达式的区别、Java 和 Kotlin 中的泛型来加深对 Java 、Kotlin 语言的理解。

Lambda表达式原理

Java 在 1.8 版本中引用了一个重要特性——Lambda表达式。该特性允许我们将符合 SAM(Single Abstract Method)格式的接口转换为更优雅的 Lambda 表达式,同时也支持在代码中直接使用 Lambda 表达式来替代以往的 SAM 接口。

同样的,Kotlin在创作时就明确了会支持函数式编程(Functional Programming),同时也对 Lambda 表达式的支持更加灵活,下面就来对比一下这两种语言中对于 Lambda 表达式的实现有何异同。

Java 中的 Lambda

Java 8 中只允许将 SAM 接口转换为 Lambda 表达式,例如 Runnable:

SAM转换为Lambda

对于左边的情况相信大家已经熟悉了,javac 会将匿名内部类编译为一个单独的class文件,并且名字也是由规律的:OuterClassName$n。例如该例中会生成一个名为 JavaLambda$1.class 的文件。

而右边采用了JDK 8 中的新特性 Lambda 表达式,直觉告诉我们跟匿名内部类肯定不会是一回事。观察字节码实际上生成了两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=1
0: invokedynamic #2, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
5: astore_1
6: aload_1
7: invokeinterface #3, 1 // InterfaceMethod java/lang/Runnable.run:()V
12: return

private static void lambda$main$0();
descriptor: ()V
flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #5 // String 123
5: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return

在上一章详细讨论了 invokedynamic 指令,来看看常量池第 2 项:

1
2
3
4
5
Constant pool:
#1 = Methodref #8.#25 // java/lang/Object."<init>":()V
#2 = InvokeDynamic #0:#30 // #0:run:()Ljava/lang/Runnable;
··· ···
#30 = NameAndType #42:#43 // run:()Ljava/lang/Runnable;

#0是一个特殊查找,指向 BootstrapMethods 中第0行:

1
2
3
4
5
6
BootstrapMethods:
0: #27 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#28 ()V
#29 invokestatic JavaLambda.lambda$main$0:()V
#28 ()V

可以看到这是对静态方法 LambdaMetaFactory.metafactory() 的调用,这个类定义在 rt.jar 包中,方法签名如下:

1
2
3
4
5
6
7
public static CallSite metafactory(MethodHandles.Lookup caller,
String invokedName,
MethodType invokedType,
MethodType samMethodType,
MethodHandle implMethod,
MethodType instantiatedMethodType)
throws LambdaConversionException

其中:

  • caller:MethodHandles.Lookup对象,即 JVM 提供的查找上下文。
  • invokedName:调用函数名,本例中为“run”。
  • samMethodType:SAM 函数方法签名,本例中为“()void”。
  • implMethod:SAM 函数的具体实现方法,即我们编写在 lambda 表达式中,本例中为 invokestatic JavaLambda.lambda$main$0:()V。
  • instantiatedMethodType:一般和 samMethodType 相同,或是特例,本例中为“()void”。

metafactory 方法是整个 Lambda 表达式实现最核心和最复杂的地方。它的源码如下:

1
2
3
4
5
6
mf = new InnerClassLambdaMetafactory(caller, invokedType,
invokedName, samMethodType,
implMethod, instantiatedMethodType,
false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
mf.validateMetafactoryArgs();
return mf.buildCallSite();

InnerClassLambdaMetafactory 中使用 ASM 技术动态生成字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public InnerClassLambdaMetafactory(MethodHandles.Lookup caller,
MethodType invokedType,
String samMethodName,
MethodType samMethodType,
MethodHandle implMethod,
MethodType instantiatedMethodType,
boolean isSerializable,
Class<?>[] markerInterfaces,
MethodType[] additionalBridges)
throws LambdaConversionException {
super(caller, invokedType, samMethodName, samMethodType,
implMethod, instantiatedMethodType,
isSerializable, markerInterfaces, additionalBridges);
implMethodClassName = implDefiningClass.getName().replace('.', '/');
implMethodName = implInfo.getName();
implMethodDesc = implMethodType.toMethodDescriptorString();
implMethodReturnClass = (implKind == MethodHandleInfo.REF_newInvokeSpecial)
? implDefiningClass
: implMethodType.returnType();
// 返回值类型
constructorType = invokedType.changeReturnType(Void.TYPE);
// 生成类的类名
lambdaClassName = targetClass.getName().replace('.', '/') + "$$Lambda$" + counter.incrementAndGet();
cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
int parameterCount = invokedType.parameterCount();
if (parameterCount > 0) {
argNames = new String[parameterCount];
argDescs = new String[parameterCount];
for (int i = 0; i < parameterCount; i++) {
argNames[i] = "arg$" + (i + 1);
argDescs[i] = BytecodeDescriptor.unparse(invokedType.parameterType(i));
}
} else {
argNames = argDescs = EMPTY_STRING_ARRAY;
}
}

可看到这里将生成类的类名定为:OuterClassName$$Lambda$n ,具体生成类的实现我们可以通过 java -Djdk.internal.lambda.dumpProxyClasses JavaLambda 来运行 JavaLambda 类,会发现其在运行期间生成了一个新的类,将其反编译后会发现其实现了 Runnable 接口,并且实现了run方法:

1
2
3
4
5
6
7
8
9
final class JavaLambda$$Lambda$1 implements java.lang.Runnable

public void run();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: invokestatic #17 // Method JavaLambda.lambda$main$0:()V
3: return

run 方法中调用了 JavaLambda.lambda$main$0 方法,从而执行了我们定义在Lambda表达式中的逻辑。

总结一下 Java 中的 Lambda表达式:

  1. Lambda 表达式声明的地方会生成一个 invokedynamic 指令,同时编译器生成一个对应的引导方法(Bootstrap Method)。
  2. 第一次执行 invokedynamic 指令时,会调用对应的引导方法,该引导方法会调用 LambdaMetafactory.metafactory 方法动态生成内部类。
  3. 引导方法会返回一个动态调用 CallSite 对象,这个CallSite 最终调用实现了 Runnable接口的内部类。
  4. Lambda 表达式中的代码会被编译成静态方法,动态生成的内部类会直接调用该静态方法。
  5. 真正执行 lambda 调用的还是 invokeinterface 指令。

Kotlin 中的 Lambda

Kotlin 中的 Lambda 表达式就比 Java 8 中的要强大得多了,几乎可以在任何地方使用 Lambda 表达式,下面同样来看一例子:

1
2
3
4
5
6
fun main() {
val sum = { a: Int, b: Int ->
a + b
}
println(sum(1, 2))
}

使用 javac 编译后生成了两个文件:

KotlinLambda

根据命名猜测到正是 Lambda 表达式生成的内部类,其命名规律为:OuterClassName$OuterFunctionName$LambdaExpressionName$n。先将其反编译,发现这个内部类继承自 kotlin.jvm.internal.Lambda,并且实现了 kotlin.jvm.functions.Function2 接口。

kotlin.jvm.internal.Lambda 类是 Kotlin 中所有 Lambda 表达式的抽象父类,其实现很简单:

1
2
3
abstract class Lambda<out R>(override val arity: Int) : FunctionBase<R>, Serializable {
override fun toString(): String = Reflection.renderLambdaToString(this)
}

熟悉 Kotlin 的话就知道 FunctionN 这类接口是 Kotlin 用于实现函数式编程而表示函数类型的接口,其签名如下:

1
2
3
4
public interface Function2<in P1, in P2, out R> : Function<R> {
/** Invokes the function with the specified arguments. */
public operator fun invoke(p1: P1, p2: P2): R
}

知道了这个内部类是 Function2 类型的话,那么具体实现只需要看 invoke 函数就行了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public java.lang.Object invoke(java.lang.Object, java.lang.Object);
descriptor: (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=3, locals=3, args_size=3
0: aload_0
1: aload_1
2: checkcast #11 // class java/lang/Number
5: invokevirtual #15 // Method java/lang/Number.intValue:()I
8: aload_2
9: checkcast #11 // class java/lang/Number
12: invokevirtual #15 // Method java/lang/Number.intValue:()I
15: invokevirtual #18 // Method invoke:(II)I
18: invokestatic #24 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
21: areturn

public final int invoke(int, int);
descriptor: (II)I
flags: ACC_PUBLIC, ACC_FINAL
Code:
stack=2, locals=3, args_size=3
0: iload_1
1: iload_2
2: iadd
3: ireturn

可以看到这两个 invoke 方法就是我们的 Lambda 表达式内部的内容了,将两个整型相加。那么是如何调用的呢,回过头来看 KotlinLambdaKt.class 的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static final void main();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
Code:
stack=3, locals=3, args_size=0
0: getstatic #15 // Field KotlinLambdaKt$main$sum$1.INSTANCE:LKotlinLambdaKt$main$sum$1;
3: checkcast #17 // class kotlin/jvm/functions/Function2
6: astore_0
7: aload_0
8: iconst_1
9: invokestatic #23 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
12: iconst_2
13: invokestatic #23 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
16: invokeinterface #27, 3 // InterfaceMethod kotlin/jvm/functions/Function2.invoke:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
21: checkcast #29 // class java/lang/Number
24: invokevirtual #33 // Method java/lang/Number.intValue:()I
27: istore_1
28: iconst_0
29: istore_2
30: getstatic #39 // Field java/lang/System.out:Ljava/io/PrintStream;
33: iload_1
34: invokevirtual #45 // Method java/io/PrintStream.println:(I)V
37: return

main 方法首先获取了 KotlinLambdaKt$main$sum$1 的静态单例,接着同样使用 invokeinterface 指令来执行接口 Function2中的 invoke 方法,执行 Lambda 表达式内部的操作。

可以看到 Kotlin 的 Lambda 表达式实现还是比较简单的,它采用了内部类的方式来代理 Lambda 表达式的内容,而 Java 8 则是利用了 invokedynamic 指令,通过 LambdaMetaFactory 来处理,这样做的好处是整个调用流程通过 LambdaMetaFactory 来完成,整个方法分派的流程从编译器转移到了运行时,后续只需调整 LambdaMetaFactory 里面的代码即可改变现有实现。

泛型与字节码

Java 中的泛型

Java 在 JDK 1.5 时引入了泛型(Generic Type)的概念,泛型存在使得我们可以更好的使用集合框架,避免了很多强制的类型转换,并且将很多错误暴露在了编译阶段。不过老鸟都知道 Java 为了兼容 1.5 之前的 JDK 版本,并没有真正引入泛型类型,而是采用一种叫类型擦除的机制,在编译时将泛型相关的全部擦除为基本类型。例如 List<String> 编译后会被擦除为 List 类型,这也导致了一些问题,例如无法使用泛型类型来做方法重载:

1
2
3
4
private void print(List<String> list) {
}
private void print(List<Integer> list){
}

当这样编写代码时会编译报错,提示 ”’print(List<String>)’ clashes with ‘print(List<Integer>)’; both methods have same erasure“,因为两个方法编译后的字节码是一模一样的。

除此之外,泛型还有无法实例化、无法判断泛型类型等缺点,如下面代码所示。

1
2
3
4
5
6
7
8
9
10
11
12
static <T> void genericMethod(T t) {
// 无法创建对象
T newInstance = new T();
// 无法创建数组类型
T[] array = new T[0];
// 无法获得Class对象
Class c = T.class;
List<T> list = new ArrayList<>();
// 无法使用instanceof判断类型
if (list instanceof List<String>) {
}
}

不过如果没有泛型的话,我们操作集合类就比较麻烦了,例如下列代码:

1
2
3
List strings = new ArrayList();
strings.add("Hello");
String value = (String) strings.get(0);

每次从 strings 中获取元素都需要显式的强转为 String 类型才能正常使用,如果使用泛型的话就不需要了,不过虚拟机还是会帮我们进行类型的转换:

1
2
3
4
5
6
7
List<String> strings = new ArrayList<>();
strings.add("Hello");
String value = strings.get(0);

// 字节码中依然需要强转
INVOKEINTERFACE java/util/List.get (I)Ljava/lang/Object; (itf)
CHECKCAST java/lang/String

字节码中留存的泛型信息

Javac 虽然会在编译时将所有泛型类型擦除,但是它依然在字节码中保留了一些关于泛型的信息,以下面代码为例:

1
2
3
4
5
6
7
8
public class SuperClass<T> {
}

class SubClass extends SuperClass<String> {
public List<Map<String, Integer>> getValue() {
return null;
}
}

使用 javap 查看 SubClass 的字节码,发现 getValue 方法相比普通无泛型方法多了一条 Signature 信息,而 Signature 中指向的常量池信息刚好是我们的泛型实际类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
public java.util.List<java.util.Map<java.lang.String, java.lang.Integer>> getValue();
descriptor: ()Ljava/util/List;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aconst_null
1: areturn
LineNumberTable:
line 11: 0
LocalVariableTable:
Start Length Slot Name Signature
0 2 0 this Lcom/sukaidev/fuckingaosp/SubClass;
Signature: #14 // ()Ljava/util/List<Ljava/util/Map<Ljava/lang/String;Ljava/lang/Integer;>;>;

Signature 指向常量池14号表项,值为 ()Ljava/util/List<Ljava/util/Map<Ljava/lang/String;Ljava/lang/Integer;>;>;。同样的,整个 Class 文件也存在一个 Signature:

1
Signature: #15                          // Lcom/sukaidev/fuckingaosp/SuperClass<Ljava/lang/String;>;

指向了常量池15号表项,内容为带泛型的父类实际类型。

现在我们知道字节码中通过附加的签名信息保留的泛型的实际类型,那么在运行时去获取泛型的实际类型就存在理论上的可能了。不过有一点要注意的是,签名可能会被混淆,注意keep住:

1
2
-keepattributes Signature
-keep class kotlin.Metadata {*;}

运行时获取泛型实际类型

注意!!!:以下分析是基于 Android SDK 30,源码可能会与主流 JDK(Oracle JDK、OpenJDK等)中有所出入,例如 Class 类中获取泛型签名的方法名就不同,不过原理是一样的。

如果经常使用 Gson 来作为序列化/反序列化 Json 工具的话,一定经常使用这样的代码:

1
2
Type type = new TypeToken<List<String>>() {}.getType();
List<String> list = new Gson().fromJson(json, type);

Type 是 Java 编程语言中所有类型的公共高级接口。它们包括原始类型、参数化类型、数组类型、类型变量和基本类型。如果将 type 打印的话,会发现它是我们传入 TypeToken 中的泛型实际类型:

1
java.util.List<? extends java.lang.String>

那么为何通过 TypeToken 可以拿到这个泛型类型呢?看看 TypeToken 的源码:

1
2
3
4
5
6
protected TypeToken() {
// 获取超类泛型实际类型
this.type = getSuperclassTypeParameter(getClass());
this.rawType = (Class<? super T>) $Gson$Types.getRawType(type);
this.hashCode = type.hashCode();
}

TypeToken 通过 getSuperclassTypeParameter 方法来获取父类的泛型参数。

1
2
3
4
5
6
7
8
9
10
11
static Type getSuperclassTypeParameter(Class<?> subclass) {
// 获取父类泛型类型
Type superclass = subclass.getGenericSuperclass();
if (superclass instanceof Class) {
throw new RuntimeException("Missing type parameter.");
}
// 强转为参数化类型,即泛型
ParameterizedType parameterized = (ParameterizedType) superclass;
// 返回第一个泛型实际类型
return $Gson$Types.canonicalize(parameterized.getActualTypeArguments()[0]);
}

重点在 getGenericSuperclass 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Type getGenericSuperclass() {
Type genericSuperclass = getSuperclass();
// 没有父类的话直接返回null,例如基本类型,接口,void,和Object
if (genericSuperclass == null) {
return null;
}

// getSignatureAttribute是个native方法,用于获取泛型签名
String annotationSignature = getSignatureAttribute();
if (annotationSignature != null) {
GenericSignatureParser parser = new GenericSignatureParser(getClassLoader());
parser.parseForClass(this, annotationSignature);
genericSuperclass = parser.superclassType;
}
return Types.getType(genericSuperclass);
}

getSignatureAttribute 是个 Native 方法最终通过 GenericSignatureParser 解析泛型签名而得到我们传给父类的泛型实际类型。

我们可以总结一下如何获取父类的泛型实际类型:

1
2
3
4
5
ParameterizedType genericType = (ParameterizedType) SubClass.class.getGenericSuperclass();
System.out.println(genericType.getActualTypeArguments()[0].getTypeName());

// 控制台输出
java.lang.String

同样我们也可以获取方法中的泛型实际类型:

1
2
3
4
5
6
7
8
9
try {
ParameterizedType methodType = (ParameterizedType) SubClass.class.getMethod("getValue").getGenericReturnType();
System.out.println(methodType.getActualTypeArguments()[0].getTypeName());
} catch (NoSuchMethodException e) {
e.printStackTrace();
}

// 控制台输出
java.util.Map<java.lang.String, java.lang.Integer>

Kotlin 中的泛型特化

Kotlin 作为一种 JVM 语言,其泛型同样会进行类型擦除。不过 Kotlin 中提供了一个关键字:reified,在内联函数中使用该关键字可以实现 Java 中实现不了的效果,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun main() {
reifiedGeneric("hello kotlin.")
}

inline fun <reified T> reifiedGeneric(t: T) {
// 可以获取Class对象,反射创建对象
val newInstance = T::class.java.newInstance()
// 可以创建数组
val arrayOfT = arrayOf<T>()
val list = ArrayList<T>()
// 依然无法使用instanceof判断类型,编译报红
if (list is List<String>){
}
}

反编译后 main 方法的字节码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static final void main();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
Code:
stack=1, locals=4, args_size=0
0: ldc #11 // String hello kotlin.
2: astore_0
3: iconst_0
4: istore_1
5: ldc #13 // class java/lang/String
7: invokevirtual #19 // Method java/lang/Class.newInstance:()Ljava/lang/Object;
10: astore_2
11: iconst_0
12: anewarray #13 // class java/lang/String
15: astore_3
16: nop
17: return

原本 main 方法应该直接调用 reifiedGeneric 方法才对,但实际上编译后 reifiedGeneric 方法中的代码完全被编译进了 main 方法内,这其实就是 inline(内联)的含义。

内联方法通过 kotlinc 编译器内联到调用处,其字节码完全合并到调用处中,这也是 kotlin 泛型特化能够实现的原因。内联函数的泛型在编译期编译器就可以通过上下文推导出其实际类型,因此可以获取其 Class 对象,也能进行实例化。

协程原理

此部分较为复杂,已发布文章《深入理解Kotlin协程(六)——从字节码角度理解协程》

参考