Managing JVM Versions

By default, Mill uses the same JVM that it itself is running on to compile/test/run Java/Scala/Kotlin modules, defaulting to zulu:21 unless you have specified some other mill-jvm-version explicitly in your Build Header Config, or configured a def jvmVersion on individual modules. Mill manages these JVMs automatically for you, downloading and caching them on demand, so you generally never need to worry about installing a JVM globally when working with a Mill project.

Using Mill’s Built-in JVM

Mill comes with a managed JVM (default zulu:21) that it uses for itself that also acts as the default JVM for any modules you define. You can use this JVM yourself via the builtin command ./mill java, javac, javap, jps, jstack, etc.:

> ./mill java -version
openjdk version "21..." ...

> ./mill javac -version
javac 21...

> ./mill javap -version
21...

> ./mill jps

> ./mill jstack --help
Usage:
    jstack [-l][-e] <pid>

For most dev workflows you don’t need to use the JVM binaries yourself, as Mill automatically uses the right one when you run compile, run, or test. However, it can sometimes be handy to invoke these binaries manually for workflows outside of Mill, e.g. running ./mill jps or ./mill jstack on a stuck process, and so Mill exposes them for you to use.

Customizing Mill’s JVM Version

build.mill (download, browse)
//| mill-jvm-version: 19

Mill’s JVM can be configured via the //| mill-jvm-version key in your build header.

> ./mill java -version
openjdk version "19..." ...

Mill’s managed JVMs are useful for three reasons:

  • It provides your project using ./mill to have access to a JVM it can use, even if run in environments without a JVM installed

  • It provides a JVM with a fixed version regardless of where your project is being run, ensuring it behaves consistently without the problems that too-new or too-old versions may bring

  • It makes it super easy to try out different JVMs: just change the //| mill-jvm-version and all the downloading/caching/installation is handled automatically!

Setting the JVM Version of a JavaModule

Configuring custom JVMs is done by setting the def jvmVersion of any JavaModule, ScalaModule, or KotlinModule

The jvmVersion string has the form:

"{name}:{version}"

To see what Jvms are available for download look at the index for your os and architecture here.

build.mill (download, browse)
import mill.*, javalib.*
import mill.api.ModuleRef

object foo extends JavaModule {
  def jvmVersion = "temurin:11.0.21"

  object test extends JavaTests, TestModule.Junit4
}
> ./mill foo.run
Foo running on Java 11.0.21

> ./mill foo.test
Testing with JVM version: 11.0.21
Test foo.FooTest.testSimple finished...

Selecting a custom JVM via jvmVersion means that JVM is used for compiling, testing, and running that module via Mill. Note that .assembly is not affected, as JVM assembly jars do not bundle a JVM and have to be run using a JVM installed on the target host machine.

For any module foo with a custom jvmVersion, you can also access java and other commands on the module’s JVM via foo.java, foo.javap, etc. For example, can be useful if you want to test a module’s assembly jar using the same java version with which it was built:

> ./mill foo.java -version
openjdk version "11..." ...

> ./mill show foo.assembly
".../out/foo/assembly.dest/out.jar"

> ./mill foo.java -jar out/foo/assembly.dest/out.jar
Foo running on Java 11.0.21

Selecting JVM Index Versions

By default, Mill comes bundled with a version of the JVM index that was published when each version of Mill is released. This ensures that the JVM versions you pick are stable, but means that the latest JVM versions may not be available. You can pass in the JVM index version explicitly via def jvmIndexVersion below, choosing a published index version from the Maven Central:

Or alternatively pass in "latest.release" to pick the latest JVM index available, although that might mean the JVM version is no longer stable and might change over time as new releases are published:

import scalalib.*

object bar extends ScalaModule {
  def scalaVersion = "2.13.16"
  def jvmVersion = "temurin:23.0.1"
  def jvmIndexVersion = "latest.release"
}
> ./mill bar.run
Bar running on Java 23.0.1

Explicit JVM Download URLs

You can also pass in the JVM download URL explicitly. Note that if you do so, you need to ensure yourself that you are downloading the appropriate JVM distribution for your operating system and CPU architecture. In the example below we switch between Mac/ARM and Linux/X64, but you may have additional cases if you need to support Windows or other OS/CPU combinations

import kotlinlib.*

object qux extends KotlinModule {
  def kotlinVersion = "2.0.20"
  def jvmVersion =
    if (sys.props("os.name") == "Mac OS X") {
      "https://github.com/adoptium/temurin22-binaries/releases/download/jdk-22.0.2%2B9/OpenJDK22U-jdk_aarch64_mac_hotspot_22.0.2_9.tar.gz"
    } else {
      "https://github.com/adoptium/temurin22-binaries/releases/download/jdk-22.0.2%2B9/OpenJDK22U-jdk_x64_linux_hotspot_22.0.2_9.tar.gz"
    }

}
> ./mill qux.run
Qux running on Java 22.0.2

Locally-Installed JVMs

Lastly, you can point Mill at any JVM distribution installed locally on disk via:

object baz extends JavaModule {
  def javaHome = Some(PathRef(os.Path("/my/java/home"), quick = true))
}

Choosing a JVM Version

Note that when publishing artifacts to Maven repositories, the JVM version used to build the artifacts will be the minimum version that downstream projects will need to use in order to use your artifacts.

  • For libraries is best to set your mill-jvm-version to the lowest version possible, e.g. mill-jvm-version: 17 in this example which is the lowest version supported by Mill.

  • For applications without downstream projects, using the highest available JVM version is usually preferred to let your application take advantage of the latest JVM improvements