State of Kotlin targeting different JVMs

Sergei Rybalkin
Oct 29 · 4 min read
© pusher blog

Researching Kotlin compiler, I found a question about targeting different JVMs:

Does Kotlin only target Java 6?
> No. Kotlin lets you choose between generating Java 6 and Java 8 compatible bytecode. More optimal byte code may be generated for higher versions of the platform.
Kotlin FAQ

This post is to clarify what is may be in Kotlin 1.3.50 (23 Oct 2019)

TL;DR

  • kotlinc produces almost the same code, targeting JVM 1.6 and 1.8, however there are few differences in: Default interface methods, Static interface methods, Inline functions and classes, Codegen
  • kotlinc does not use invokedynamic for lambdas.
  • There is no observable difference targetin JVM 1.8 comparing to 9–12.

Choosing JVM target in kotlinc

kotlinc -jvmTarget <target version>
Target version could be one of 1.6, 1.8, 9, 10, 11, 12.
Default is 1.6.

Default interface methods

Let’s have a look at code Example1.kt

interface Foo {
fun bar(): Int = 42
}
class Baz: Foofun main() {
println(Baz().bar())
}

Foo::bar has a default implementation.

Let’s compile them using JVM targets 1.6 and 1.8 and compare results.

> kotlinc -jvm-target 1.6 Example1.kt (6)

> kotlinc -jvm-target 1.8 Example1.kt (8)

As we can see two sets of class files are identical modulo major version and MD5 checksum.

Java 8 introduced default interface methods.
In Kotlin you need to annotate method with @JvmDefault to achieve this. In our example:

@JvmDefault fun bar(): Int = 42

Compiling:

> kotlinc -jvm-target 1.8 -Xjvm-default=enable Example1.kt        (8-default)

Let’s now compare (6) and (8-default).
In (6) we have class Bar invoking kt/Foo$DefaultImpls.bar:(Lkt/Foo;)I from synthetic class using invokestatic instruction.

public final class kt.Bar implements kt.Foo {

public int bar();
Code:
0: aload_0
1: invokestatic #19 // Method kt/Foo$DefaultImpls.bar:(Lkt/Foo;)I
4: ireturn
}

public final class kt.Foo$DefaultImpls {
public static int bar(kt.Foo);
Code:
0: bipush 42
2: ireturn
}
public interface kt.Foo {
public abstract int bar();
}

However, (8-default) introduces different approach: Bar.class does not have anything except <init>, but Foo.class will have all the information about default method

public final class kt.Bar implements kt.Foo {
...
}
public interface kt.Foo {
public int bar();
Code:
0: bipush 42
2: ireturn
}

Static interface methods

Java 8 introduced static interface methods:

interface Foo {
static int bar() { return 42; }
}

The closest analogue in Kotlin is an interface with companion object Example2.kt:

interface Foo {
companion object {
fun bar(): Int = 42
}
}

Again, let’s compile and check diff

> kotlinc -jvm-target 1.6 Example1.kt (6)

> kotlinc -jvm-target 1.8 Example1.kt (8)
> diff -ar (6) (8)

And, as with default methods, two sets of class files are identical modulo major version and MD5 checksum.

Let’s now annotate bar() with @JvmStatic

@JvmStatic fun bar(): Int = 42     (8-static)

We do not need any other kotlinc arguments to compile it to (8-static).

Both in (6) and (8-static) kotlinc generates Foo$Companion.class: (8-static) contains everything same as (6) and metadata overhead (one constant pool member and one instruction) to store extra runtime annotation (@JvmStatic) contains.

Getting to the main difference in Foo.class. (8-static) has an extra static function proxying calls to Companion.

static {};
...
public static int bar();
descriptor: ()I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: getstatic #26 // Field Companion:Lkt/Foo$Companion;
3: invokevirtual #30 // Method kt/Foo2$Companion.bar:()I
6: ireturn
RuntimeVisibleAnnotations:
0: #24()
}

Inline functions

Kotlin has inline functions. It’s a strait-forward inline of code block (practically lambda).

Let’s have a look at Example3, it works pretty good when we compile these two files using the same JVM target.

// file Example3.kt 
package e3
fun main() {
foo()
}
// file InlineUtil.kt
package e3
inline fun foo() {
println(42)
}

However, compiling with different JVM targets

> kotlinc -jvm-target 1.8 InlineUtil.kt> kotlinc -jvm-target 1.6 -cp . Example3.kt

leads to a reasonable error, saying kotlinc can not inline bytecode of higher JVM target version.

Hashcode

There are two cases when Kotlin generates different hashCode() methods depending on JVM target version.

Data class

Let’s have a look at Example4 with data class.

data class Example4(val long: Long)

And again

> kotlinc -jvm-target 1.6 Example4.kt     (6)

> kotlinc -jvm-target 1.8 Example4.kt (8)

(6):

public int hashCode();
Code:
0: aload_0
1: getfield #10 // Field long:J
4: dup2
5: bipush 32
7: lushr
8: lxor
9: l2i
10: ireturn

(8):

public int hashCode();
Code:
0: aload_0
1: getfield #10 // Field long:J
4: invokestatic #52 // Method java/lang/Long.hashCode:(J)I
7: ireturn

As you can see, since 1.8 kotlinc uses invokestatic to get hashCode forLong.
There are similar differences for Double, Float, Byte, Short, Char and Int.

Inline class

Inline class is a very new Kotlin feature, introduced in 1.3, but still experimental.

Let’s have a look at Example5.kt

inline class Example5(val d: Double)

Similar to data classes, an amount of code is generated to support each inline class:

Compiled from “Example5.kt”
public final class kt.e5.Example5 {
public final int getInt();
...
public static int constructor-impl(int);
...
public static final kt.e5.Example5 box-impl(int);
...
public static java.lang.String toString-impl(int);
...
public static int hashCode-impl(int);
...
public static boolean equals-impl(int, java.lang.Object);
...
public static final boolean equals-impl0(int, int);
...
public final int unbox-impl();
...
public java.lang.String toString();
...
public int hashCode();
...
public boolean equals(java.lang.Object);
...
}

Comparing resulting bytecode for (6) and (8) we will see an additional invokestatic in (8)

public static int hashCode-impl(int);
Code:
0: iload_0
+++ 1: invokestatic #51 // Method java/lang/Integer.hashCode:(I)I
4: ireturn

Same differences for Double, Float, Byte, Short, Char and Long.

Source code

All examples are available here: https://github.com/rybalkinsd/state-of-kotlin-examples

PS

This list is potentially not comprehensive, so feedback is very welcome.

Sergei Rybalkin

Written by

Kotlin, JVM, Serverless, Java, Spring. Kotlin http DSL client https://github.com/rybalkinsd/kohttp

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade