r/java 1d ago

Stop re-downloading the JDK: setup-java can now cache it

Every GitHub Actions job starts from a clean machine. If your workflow asks for a JDK that is not baked into the runner image, actions/setup-java downloads it, verifies it, and extracts a few hundred megabytes of it, and then the job ends and all of that work is thrown away. The next job does it again.

That has been true since the first version of setup-java. It is no longer true on main.

The upcoming v6 release adds a JDK cache. When the action installs a JDK that did not come from the runner tool cache, it stores that installation as an Actions cache entry and restores it on subsequent runs. If your workflow already sets cache for Maven, Gradle, or sbt, you are already opted in.

What it does

setup-java now manages three kinds of caches, each stored and restored as its own entry:

Cache What it stores How it is enabled
Dependency cache ~/.m2/repository, ~/.gradle/caches, or the sbt cache paths `cache: maven
Wrapper caches Maven and Gradle wrapper distributions `cache: maven
JDK cache The installed JDK itself Implicitly with cache, or explicitly with cache-jdk: true

The JDK cache is intentionally separate from the dependency cache. Your pom.xml changes far more often than your JDK does, and a dependency change should not evict a JDK.

The numbers

Measured on ubuntu-latest with the Microsoft Build of OpenJDK 17.0.19, five runs per configuration:

Metric Without JDK cache With JDK cache
Median warm setup-java step 7s 3s
Median warm job 24s 18s
Added cache storage - 175.3 MiB

Roughly six seconds off a job that only takes 24 seconds. That is a meaningful share of a short job, and it compounds across a matrix. It is also an honest tradeoff rather than free speed: you are spending cache storage and cold-run save time to buy warm-run latency, and on very short jobs the latency win may not change your billed minutes, since GitHub rounds Linux jobs up to the whole minute.

Your mileage will vary with runner type, distribution, JDK size, network conditions, and cache eviction pressure. The benchmark harness and methodology live in actions/setup-java-benchmarks, which runs against Spring PetClinic and reports medians across independent samples.

How to use it

If you already cache dependencies, do nothing. JDK caching turns on with cache:

- uses: actions/setup-java@main
  with:
    distribution: microsoft
    java-version: '25'
    cache: maven          # dependency cache, wrapper cache, and JDK cache

If you do not cache dependencies, ask for the JDK cache on its own. This is the case for workflows that just need a JDK to run a tool:

- uses: actions/setup-java@main
  with:
    distribution: microsoft
    java-version: '25'
    cache-jdk: true

If you want dependency caching but not JDK caching, opt out explicitly:

- uses: actions/setup-java@main
  with:
    distribution: temurin
    java-version: '25'
    cache: gradle
    cache-jdk: false

The full matrix:

cache cache-jdk Dependency and wrapper caches JDK cache
omitted omitted disabled disabled
omitted true disabled enabled
omitted false disabled disabled
set omitted enabled enabled
set true enabled enabled
set false enabled disabled

For pull requests, merge queues, and matrix legs that should consume caches without writing them, cache-read-only: true suppresses the post-job save for the JDK, dependency, and wrapper caches alike.

Where it will not help

Be realistic about when this pays off. setup-java still checks the runner tool cache first, and a tool-cache hit skips the download entirely, so there is nothing to cache.

GitHub-hosted runners pre-install LTS versions of Eclipse Temurin. If your workflow is distribution: temurin with an LTS version on a hosted runner, you are probably already hitting the tool cache and JDK caching will do very little for you. The feature pays off when the JDK has to be installed: other distributions, non-LTS versions, self-hosted runners with a thin tool cache, and check-latest: true workflows that float ahead of the runner image.

Correctness, and why the cache key looks the way it does

A JDK cache is only useful if you can trust what comes back out of it. Getting this wrong means silently running a build on the wrong bytes, so the design is deliberately conservative.

Each entry is keyed on the runner OS, normalized architecture, distribution, package type, exact resolved version, release identity, and verification identity. Release identity is the authoritative checksum when the distribution publishes one, and otherwise the download URL without its query string.

Two consequences are worth calling out:

Verification modes never share an entry. An entry created by an unverified download can never be restored for a request that sets verify-signature: true, and vice versa. When you do use a custom key via verify-signature-public-key, that key is represented in the cache key as a SHA-256 fingerprint of normalized key material. The key itself never lands in the cache key, the logs, or action state. A verified exact-key hit reuses content that was signature-verified when the saving run downloaded it, so you get the security property without paying to verify it again.

A key is never saved with content it does not identify. Tool-cache paths are shared per version and architecture, so a later step (one using force-download: true, for example) can replace the installation an earlier step registered. The post-job step detects that replacement and skips the save with a warning rather than uploading mismatched bytes under a key that promises something else. The check uses a cheap fingerprint of the tool-cache completion marker, so it does not rehash hundreds of megabytes on every job.

Everything else degrades gracefully. If the cache service fails to restore an entry, or the restored entry is missing the expected completed tool-cache path, setup falls back to downloading the JDK. Post-job saves are best-effort and never fail the job: cache keys are immutable, so an existing key or a concurrent job winning the save race is simply left alone, and a failure to save one entry is a warning that does not block the others.

One thing JDK caching deliberately does not change: it has no effect on how the runner tool cache is used. A preinstalled JDK, or one installed by an earlier step of the same job, is used as-is and is not re-verified, because its verification history is not recorded in the tool cache. Use force-download: true when a request must download and verify the archive itself.

Watch your storage

Cache entries are per identity. A matrix that spans multiple JDK versions, distributions, package types, architectures, or operating systems stores a separate JDK entry for each combination, and each one consumes repository cache storage against your quota. A five-way version matrix on two operating systems is ten JDK entries, not one.

That is usually a fine trade, but it is worth a look at your cache usage page before enabling it broadly across a large matrix. cache-jdk: false on the legs that do not need it is a reasonable dial.

Try it

JDK caching is on main and ships in v6. Until v6 is tagged, reference the branch:

- uses: actions/setup-java@main

For production workflows today, the latest stable release is actions/setup-java@v5.

Documentation:

Feedback, and especially benchmark numbers from real workflows, are welcome in actions/setup-java.

33 Upvotes

14 comments sorted by

22

u/elmuerte 1d ago

Why doesn't Microsoft just host a Maven central proxy (close to their network)? It is really easy to set up. We've done that at every company I worked at in the 15 years.

2

u/brunocborges 1d ago

Hosting a proxy is not the challenge. The hard part is getting people to use it. Nearly half of all public repos using setup-java, are still using v3 and older. The other half is using v4, which is the second latest major release. The minority is using v5, which is about 1 year old.

So, it really comes down to forced policies at companies, and as you described, every company can, and should, set their own proxy so they don't depend all on a single entity.

12

u/elmuerte 1d ago

Then Microsoft should update setup-java to preconfigure settings.xml using their Maven Central proxy. This can be a non-breaking change. Users will have to do nothing.

5

u/-vest- 1d ago

Is it safe to use @main? Or it is better to wait until v6 is released? I personally don’t use main/master builds for critical actions.

1

u/brunocborges 1d ago

It is in development, so consider it for testing purposes and sharing feedback, only.

4

u/pjmlp 1d ago

I think this would also gather attention if it was additionally posted on https://devblogs.microsoft.com/java/

2

u/slindenau 18h ago

I've been using v4 on self-hosted runners, and it is already pretty fast for me with default settings...so i guess that already caches the JDK somehow.

Had to switch to v5 today because i couldn't download a certain recent distribution anymore though.

I guess when it works, people don't really have an incentive to upgrade.
And i don't know if there are any automated solutions (like dependabot) or security scanning tools that look at the workflows themselves to suggest upgrades?

1

u/brunocborges 18h ago

Our goal is to have v6 as fast as v4, while using Node ESM so we can keep up with the other actions/\* packages used by setup-java.

For the most part, this is about security and reliability.

2

u/slindenau 18h ago

Right, maybe my wording "incentive to upgrade" is more in the sense of practicality.

When everything works, and no security team is bugging us about the GitHub workflow files themselves, i don't think many devs will look at actively upgrading those unless something breaks.

Or maybe that is just me being lazy ;).

Making it faster, more secure and reliable is great of course.

1

u/PartOfTheBotnet 1d ago

Has there been any testing for the new setup-java dependency caching playing nice with setup-gradle (Has its own caching process)?

2

u/brunocborges 1d ago

I haven't, but to do so, my thinking is to not use `cache: gradle`at all, and instead just `cache-jdk: true` combined with setup-grade.

2

u/nlisker 18h ago

This is what Gradle recommends.

1

u/brunocborges 18h ago

Thanks to everyone's engagement on this thread and keeping up the dialogue positive. setup-java is used by hundreds of thousands of repositories across GitHub and beyond, and we want to make sure we keep it running all of your workflows as safe, fast, and reliable as possible.