1. Overview
As a modern automation and build tool, Gradle makes it possible for us to configure the Kotlin compiler JVM bytecode version in our project.
This tutorial will explore several approaches to configuring the Kotlin JVM bytecode version with Gradle.
2. What Is Kotlin’s Bytecode?
As we know, Kotlin is a static language that targets the JVM, Android, JavaScript, Wasm, and Native. So if we target the JVM, then the Kotlin code compilation process will be translated into bytecode, which will later be run by JVM:
+----------------+ +------------+
[ Kotlin Code ] --> | Compilation | --> [ Bytecode (.class) ] --> | Run by JVM |
+----------------+ +------------+
If we’ve ever seen a .class file in a project, we should know it’s a file that contains JVM bytecode. All JVM languages, including Java, Kotlin, Scala, Clojure, Groovy, JRuby, and Jython, will generate .class files after compilation that contain instructions that require a JVM to be interpreted and run.
So when we say Kotlin Bytecode, we are referring to JVM bytecode generated by the Kotlin compiler, with Kotlin-specific features, such as null safety, extension functions, and Kotlin lambdas. So, Kotlin Bytecode is a subcategory of JVM bytecode that has special characteristics due to the transformation from Kotlin language to JVM bytecode.
2.1. Validating Bytecode Version
We can find out the bytecode version from the .class file. Inside the .class file there is a ClassFile structure that stores various information:
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
Then let’s use the javap command, which is useful for disassembling one or more .class files:
javap -verbose <class file name>
The javap -verbose command is a tool in the JDK (Java Development Kit) that is used to display in-depth details about Java bytecode .class files, including bytecode instructions, internal structures, and other meta information.
Suppose the .class is located at configure-bytecode/build/classes/kotlin/main/com/baeldung/Main.class.
Let’s try it right away:
javap -verbose configure-bytecode/build/classes/kotlin/main/com/baeldung/Main.class
The result will be something like this:
If we look at it, it is almost the same as Java bytecode. But from the structure of this class file format, we can see that this is the result of a compilation of Main.kt, which means that this source code is written in Kotlin.
Then let’s see, there is a major version: 55, which means JVM version 11.
2.2. Find Kotlin Version
Going deeper, we can look at the InnerClasses attribute section. We can see the Kotlin Metadata:
Here we can see metadata version mv=[2,0,0], indicating the metadata uses Kotlin version 2.0.0.
3. Kotlin and Java Version Compatibility
Before discussing further, it’s important to know about Kotlin and JVM version compatibility. So that we don’t choose the wrong JVM version for the Kotlin version we are compiling against.
We can actually determine the compatible bytecode versions from the Kotlin version we are using. Because by default, the Kotlin/JVM (since version 1.6.0) compiler produces Java 8-compatible bytecode. If we want to make use of optimizations available in newer versions of Java, we can explicitly specify the target Java version from 9 to 21. But starting with Kotlin 1.5, the compiler doesn’t support producing bytecode compatible with Java versions below 8.
Here are the JVM targets applicable to different Kotlin versions:
So we can target the Java version that is compatible with our Kotlin version. Knowing this, we shouldn’t target Java 6 if we are using Kotlin 2.0.0.
4. Configuring Bytecode Version in Gradle
If we don’t configure the bytecode version in build.gradle, then the generated bytecode will refer to the JVM version in the IDE settings, for example in .idea/gradle.xml (assuming we are using IntelliJ IDEA).
<project version="4">
<component name="GradleSettings">
<!-- ... -->
<option name="gradleJvm" value="azul-11" />
<!-- other option -->
<!-- ... -->
</component>
</project>
We can prove this by not defining any JVM version configuration in our build.gradle at all. Then run the clean task, then compile. Next, we can check the bytecode version with javap -verbose.
So to ensure that the JVM version targeted is reproducible and doesn’t depend on the IDE, it is better to add it in build.gradle.
4.1. Using jvmTarget
To set the JVM version in a Kotlin project, we can use jvmTarget inside compilerOptions in the compileKotlin task. The kotlin {} extension will apply this setting to both compileKotlin and compileKotlinTest:
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}
}
We should know that related tasks such as compileKotlin, compileJava, compileTestKotlin, and compileTestJava will all be checked for compatibility. Because they are part of the Gradle lifecycle, so they must all have the same target JVM.
If we don’t do it properly, it will cause an incompatibility error. For example, when in the compileKotlin task we try to use Java 17, while the default IDE settings still use Java 11. This will cause a build failure:
Execution failed for task ':compileKotlin'.
> 'compileJava' task (current target is 11) and 'compileKotlin' task (current target is 17) jvm target compatibility should be set to the same Java version.
Consider using JVM toolchain: https://kotl.in/gradle/jvm/toolchain
We can use the release flag for Java 10 and above or use targetCompatibility for below Java 10. As much as possible, do not use targetCompatibility if we are using Java 10 and above.
tasks.compileJava {
options.release.set(11)
}
The release flag ensures the specified language level is used. Gradle supports using the release flag from Java 10.
4.2. Using Toolchains
Alternatively, we can use a Java toolchain that is officially recommended by Kotlin to solve the incompatibility issue explained previously.
A Java toolchain is a set of tools to build and run Java projects, which is usually provided to the environment via local JRE or JDK installations.
We can use toolchains implicitly without thinking about the compatibility of related tasks because everything will be handled.
Can we also use it in Kotlin projects? Yes, of course. Note, we must be using Gradle version 6.7 and above, then we can define:
kotlin {
jvmToolchain(11)
}
With toolchain support, Gradle can autodetect local JDKs and install missing JDKs that Gradle requires for the build. Now Gradle itself can run on any JDK and still reuse the remote build cache feature for tasks that depend on a major JDK version. Gradle supports toolchains since version 6.7.
5.3. Configuring Test Source Only
We can also configure the bytecode version on a specific task. For example, on the compileTestKotlin and compileTestJava tasks:
tasks.compileTestKotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}
tasks.compileTestJava {
options.release.set(11)
}
The configuration is scoped to the compileTestKotlin and compileTestJava blocks, which specifically manage the compilation of test sources.
This means that the target JVM version 11 configuration is only applied when the project runs unit or integration tests, not for the main source code compilation.
4.4. Using configureEach to Configure All Tasks
When we use kotlin {} extension, it configures the compileKotlin and compileTestKotlin tasks. However, it will not configure any custom tasks.
So we can use configureEach() which is guaranteed to configure all tasks, including custom ones:
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
tasks.withType<KotlinCompile>().configureEach {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}
tasks.withType(JavaCompile::class).configureEach {
options.release.set(11)
}
By using tasks.withType
Then configureEach, Gradle sets the target JVM configuration for each Kotlin compilation task it finds.
5. Conclusion
In this article, we discussed Kotlin bytecode and how to target a JVM version with Gradle. The Kotlin compiler will compile Kotlin code into bytecode, and then the JVM will run it. To ensure a bytecode version compatible with a specific JVM version, we need to explicitly target it.
We should use jvmTarget with caution, due to compatibility issues, and prefer to use the toolchain approach, but this must be with Gradle 6.7 and above.
Kotlin JVM compatibility ranges must also be taken into consideration so that we don’t choose an incompatible JVM version for our Kotlin version.