JDK 21 shipped three weeks ago, which finally puts a date on something that has been unanswerable for two years: when free use of JDK 17 stops. This is about a license whose expiry is computed from a release calendar rather than written down, about what the paid subscription started counting in January, and about why the expensive half of leaving Java 8 is forty libraries moving across their own breaking changes.
9 October 2023·7 min read·languagesdependencies
Ask a room of engineers which JDK their production systems run and watch what happens. Somebody says 11. Somebody else says 8, and is right about a different service. Almost nobody names a vendor, because for about fifteen years there was only one answer worth naming and it came preinstalled. Run java -version on any of those machines and the second and third lines tell you who built the runtime and under what license, and those two lines are the ones nobody has read.
Not knowing was perfectly rational for most of that time, because the question had no consequences attached and habits that never form do not appear on any list of things you lack. The consequences arrived with JDK 11 on the twenty-fifth of September 2018, when Oracle shipped its build under a license requiring a paid subscription for production use. What followed was a scramble: the same code, compiled and tested by a dozen other organizations and published free, and a set of teams discovering that "which JDK" now had an invoice behind it. Some answered the question. Rather more stayed on 8.
Then Oracle reversed. JDK 17 shipped in September 2021 under a No-Fee Terms and Conditions license permitting commercial and production use at no charge, and JDK 21 arrived three weeks ago on the same terms. That is genuinely good and it puts nothing back the way it was, because the free period has a defined end and the end is computed rather than stated. The vendor of your runtime stopped being a default and became a decision with a review date. And while everybody argued about that, the part of the move off Java 8 that actually consumes a schedule sat untouched, somewhere else entirely.
They are the same source. Oracle, Eclipse, Amazon, Microsoft, Red Hat, SAP, Azul, BellSoft, IBM and Alibaba all publish builds compiled from the OpenJDK repository, and for a long-term release they track the same updates project. Vendor differences are build configuration, patch backports and packaging, applied on top of a tree everybody shares. That is not a claim about quality; it is a statement about provenance, and it is why swapping vendors is not a migration.
Compatibility is a test suite, not a promise. The Java SE Technology Compatibility Kit turns "compatible" from marketing into something with a pass or a fail. Eclipse states that every Temurin release passes the Oracle Java SE TCK as well as its own AQAvit quality suite, and it now publishes builds across the long-term releases from 8 forward. That is the sentence to look for on any vendor's page, and its absence is the only interesting finding available there.
What differs is the calendar and the packaging. Vendors publish different end-of-support dates for the same version, differing by years rather than months, and they differ in container images, installer formats and which architectures exist. Neither of those changes how your program runs and both change how your pipeline works. So picking a build is picking how long somebody keeps issuing security updates and how much friction your images inherit, which is a procurement question with a calendar answer.
The No-Fee Terms and Conditions license permits use of Oracle's own build for commercial and production purposes at no cost, which is the thing that was not true for JDK 11 through 16. The important detail was never that it is free. It is how the free period is bounded: Oracle defines it as running a year past the arrival of the following long-term release, rather than as a fixed date. For two years that made the end of free JDK 17 use genuinely unknowable, because the event it hung on had not happened.
It has now. JDK 21 landed three weeks ago, so the arithmetic resolves, and free production use of JDK 17 ends around September of next year. That is the sentence I would put in front of anyone still treating this as settled, because a great many organizations moved to 17 specifically on the grounds that it was free and have not looked at it since. Nothing was hidden and nothing was withdrawn early. The date simply did not exist at the moment the decision was made, and it does now.
Which is the shape worth naming, because it is unusual. A contract with an end date can be diarized by anyone who reads it once. A window that closes some period after an event that has not happened yet cannot be diarized at all, so the review has to be recurring and owned by a person, and nobody sets that up for something they obtained by downloading it. This is also the third license posture on the same artifact from the same vendor inside five years, which I read less as bad faith than as evidence about what kind of thing a runtime license is.
The paid side moved too, and it moved in a way that matters more than the price. In January Oracle replaced the old subscription with one metered per employee - counted across the whole organization rather than across the people or machines using Java. Whatever you think of the number attached to it, the meter no longer has anything to do with your usage, so the two questions engineering can normally answer, how many installs and how many cores, stopped being the questions being asked.
How much any of it costs you still turns on something unrelated to Java. If everything runs in containers you build from a base image you chose, this is twenty minutes of reading and a line in a policy document. If there is Java on employee laptops, inside a build server somebody set up in 2016, and embedded in three vendor appliances whose installers laid down a runtime without telling anyone, then it is an inventory problem, and inventory is the part that costs money. The license is cheap. Finding out is not.
The platform got modular even though your code did not. Nothing forces an application onto the module system, and this is the most common misreading of the whole transition. The breakage does not come from adopting modules; it comes from the fact that the JDK itself is now modular, so the internals your dependencies used to reach into are behind a boundary that is enforced. You are not being asked to modularize. You are being told that the platform did, and that the consequences are yours.
The Java EE modules left, and they left quietly. JEP 320 removed the bundled Java EE and CORBA modules in JDK 11, which means anything calling into javax.xml.bind or its neighbors now needs those artifacts declared as ordinary dependencies. The failure is a NoClassDefFoundError at runtime rather than a compile error, so it surfaces in whichever code path runs least often, which in practice is an error handler or a nightly job.
Strong encapsulation stopped being negotiable. JDK 16 made denial the default for reflective access into JDK internals, and JEP 403 in JDK 17 removed the command-line option that let you relax it. Libraries that reach into internals - serialization frameworks, mocking tools, bytecode generators - need versions that stopped doing so. The --add-opens escape hatch still exists per package, and every use of it is a debt you should record somewhere a person will read, because it is a list of things that will break again.
The garbage collector changed underneath you. G1 became the default in JDK 9, replacing the parallel collector Java 8 used, and the concurrent mark sweep collector was removed outright in JDK 14. So a service carrying heap flags tuned in 2016 does not fail to start. It starts, ignores the flags it no longer recognizes with a warning nobody reads, and runs with a different pause and throughput profile than the one your capacity planning assumed. That is a performance regression with no error attached to it.
The version string changed shape in JDK 9. Java 8 reported itself as 1.8.0_292 and everything since reports a plain major version, and there is a startling amount of code in the world that parses the old form. It is rarely in the application. It is in a shell script that gates a deployment, a monitoring agent, an installer check, or a library's feature detection, and it is always owned by somebody who has left. This one is trivial to fix and hard to find, which is the worst combination for a migration plan.
Java 17 produces class files at major version 61, and every tool that reads or writes bytecode has to understand that number before anything using it will run. That set is larger than it sounds: the mocking framework, the object-relational mapper's proxy generator, the dependency injection container, the coverage tool, the aspect weaver, and whatever the build plugin chain drags in transitively. Each needs a version that supports 17, and several of them changed their own APIs to get there.
Then the things that attach to a running JVM. Profilers, application performance agents and runtime security agents hook into internals by design, they are all version-coupled, and they are exactly the tools you would want pointed at a freshly upgraded service. The ordering is uncomfortable: observability is usually the last thing certified for a new release, so the period when you most need to see inside the process is the period when you can see least.
So the honest description of an 8-to-17 project is that it is mostly a dependency upgrade. You are not writing much Java; you are moving forty libraries across their own breaking changes, which means the schedule is set by the least well maintained thing in your tree rather than by anything about the language. Whether you stop at 11 on the way barely matters, because the breakage is cumulative either way. Teams estimate the whole thing as a runtime change and are then surprised by a quarter of library archaeology, which is the one prediction in this piece I would make with confidence.
The last five years read differently depending on where you stood. Oracle funds an enormous amount of the work that produces the thing everybody ships, the 2018 change was a legitimate attempt to charge for something with real costs behind it, and two long-term releases in a row under a no-fee license is a straightforwardly good outcome for everyone downloading a runtime. What the episode changed is not who to trust. It is that a property of the platform which used to be constant is now something with a version number and a review date.
The habit worth keeping out of it is smaller than any of the arguments. For fifteen years the answer to "which JDK are you running" was so obvious that nobody built the reflex to check, and the cost of not having that reflex was paid in the two years afterward, by people reading license terms under time pressure while trying to work out what a nightly job in a subsidiary had been started with. The license is free for now, and now has a date on it. The inventory is still the thing you do not have.