Key Takeaways
- Since Java 8 a number of useful new language features have been introduced, along with new tooling, and performance improvements particularly for garbage collection.
- When choosing to upgrade the choice you face is whether to upgrade to the latest version of Java (12) and be prepared to upgrade every six months; or upgrade to the latest LTS (11) to give yourself up to three years to think about your next upgrade.
- Don’t be tempted to ignore compiler warnings. Deprecation is being taken much more seriously in this modern Java world, and both Java 10 and Java 11 removed APIs.
- One of the changes from Java 9 was that internal APIs (largely those classes in packages that started with sun.misc.*) were hidden from use. APIs that are not core to the JDK have also been removed in Java 11. These changes may impact your application but there is a clear path to avoid these problems.
- Once "over the hump" of this first upgrade, it's worth at least testing the application on the latest version of Java every 6 months, for example in CI.
Enterprises have traditionally been reluctant to upgrade to the latest version of Java until it has been fully proven. This is becoming increasingly challenging as we’ve had new versions of Java every six months since Java 9 came out in September of 2017. So even though Java 9 is less than two years old, the latest version is now Java 12.
This pace of change might be intimidating, and it can certainly seem like upgrading from Java 8, which the majority of applications are still running on, to Java 12 could be a lot of work. In this article we’re going to look at:
- The benefits of upgrading
- The potential issues with upgrading
- Some tips for upgrading
Why Upgrade?
Before diving into the details of how to upgrade, we should look seriously at why we want to.
Features
As developers, we’re most interested in the new language features or APIs that come in each update to the language. Since Java 8 we’ve had plenty of new features, and also some new tools that can change the way we work. I’ve picked out some key features developers have told me have made their lives easier when they’re working with the latest version of Java.
Local-variable type inference (var
) is a nice example of syntactic sugar that helps reduce boilerplate code. It’s not a fix-all for all readability problems, but using it at the right time can help the reader of the code focus on the business logic (whatit’s doing) rather than boilerplate (howit’s doing it).
Convenience Factory Methods for Collections make it significantly easier to create collections like lists, maps and sets. The factory methods also create unmodifiable collections, making them safer to use.
Collecting to unmodifiable collections is possible using the new Collector for Streams operations to put the results into an immutable collection.
Predicate::not provides an easy way to negate predicate lambdas or method references, again reducing the boilerplate in our code.
New methods on Optional give even more options for coding in a functional style when using an Optional instead of having to use clumsy ifstatements.
JShell is the REPL that allows us to run individual lines of code, or even scripts, in Java. It’s a nice way to experiment with new features, and we can use it locally on our development machines without having to adopt new versions of Java in our production application code.
An HttpClient built in to the JDK. Almost everyone is working with HTTP in some form or other, via web applications or REST services or something else. The built in client removes the need for external dependencies, and supports HTTP 1.1 and 2 with both synchronous and asynchronous programming models.
Multi release jar files is one of the tools library developers can use to support the needs of those using the latest versions of Java as well as those forced to use older versions.
JLink is a fantastic tool made possible by the Java Module System which lets us package and deploy only the sections of the JDK that we really need.
Performance
Generally speaking each new release performs better than the previous one. "Better" can take many forms, but in recent releases we've seen improvements in startup time; reduction in memory usage; and the use of specific CPU instructions resulting in code that uses fewer CPU cycles, among other things. Java 9, 10, 11 and 12 all came with significant changes and improvements to Garbage Collection, including changing the default Garbage Collector to G1, improvements to G1, and three new experimental Garbage Collectors (Epsilon and ZGC in Java 11, and Shenandoah in Java 12). Three might seem like overkill, but each collector optimises for different use cases, so now you have a choice of modern garbage collectors, one of which may have the profile that best suits your application.
Cost Reductions
The improvements in recent versions of Java could lead to a cost reduction. Not least of all tools like JLink reducing the size of the artifact we deploy, and recent improvements in memory usage could, for example, decrease our cloud computing costs.
There's always another potential benefit to consider in that using modern versions of a language also allows you to attract a wide range of developers, as many developers like to have the opportunity to learn new things and upgrade their skills. Using a modern version of Java has an impact on the retention and recruitment of developers, which ultimately impacts the cost of running your team.
Upgrade Concerns
Lots of things have changed since Java 8, and not just in terms of language features. Oracle started releasing two different builds, each with a different license (a commercial build which you need to pay to use in production and an open source OpenJDK build) and changed their update and support models. This has led to some confusion in the community, but ultimately means it has given us more choice over the JDK we use. We have to consider both what our options are and what we want from the JDK.
Which version?
It might seem like there’s a big choice of versions to upgrade to from Java 8 given that the latest version is Java 12. In fact the choice is a little simpler than this. Now we have releases every six months, each new release replaces the previous one. The exception is that every three years there’s a Long Term Support (LTS) release. This is to allow organisations to select an upgrade path that they’re comfortable with - either upgrading to the latest every six months, or doing a traditional-style large upgrade every three years to the next LTS release. Java 8 was an LTS, although Oracle stopped providing updates to Java 8 for (free) commercial use in January this year. Java 11 is the current LTS, and although Oracle are not providing free commercial updates for this now that Java 12 is out, there are plenty of JDK builds out there from organisations who will provide updates for at least three years.
The choice you face is: upgrade to the latest version of Java (12) and be prepared to upgrade every six months; or upgrade to the latest LTS (11) to give yourself up to three years to think about your next upgrade. Anecdotally we would expect larger organisations to jump from LTS release to LTS release, whilst start-ups may well update every six months. The great thing about the more rapid, predictable release cadence is that it supports both options.
Which build?
Just because Oracle aren’t providing free updates to their LTS releases doesn’t mean it’s not possible to run an LTS in production and get free upgrades. There are plenty of other organisations and vendors who offer builds of OpenJDK(the reference implementation of the JDK that even the Oracle commercial JDK is based on). Rather than enumerate them all here, feel free to check out this list which covers the providers of free OpenJDK builds, the dates that free public updates will be available until, and details of any commercial support.
This might seem more complicated than it used to be. There are more factors to consider, it’s true, but this is the side effect of having more choice. The Java Champions have prepared a much more complete discussion of the topic of licensing, support, updates and the different options open to us, and there’s also a Q&A session from QCon London 2019 on this topic.
Didn’t Java 9 break everything?
The main concern that many developers have when thinking about upgrading from Java 8 are the big changes that came in Java 9 and the fear that these changes may break applications. One of the changes was the encapsulation of internal APIs, which meant that some methods that used to be available in the JDK are no longer accessible. This, along with the removal of tools.jar and rt.jar seemed alarming at the time of the release of Java 9, but in hindsight it seems to have been more a problem for library, framework and language developers than for application developers.
Provided your application isn’t doing something it shouldn’t (like using internal APIs or deprecated methods), migrating to Java 9 or beyond is not as scary as it may seem. Many of the problems that faced the community have actually been addressed in the build tools and libraries that we use.
Pointers for Upgrading
Every upgrade process is going to be specific to the application being migrated. However there are some basic good practices that will help ease the process. These are outlined in the order that you should tackle them, and you’ll find for the first few steps you don’t even need to use an updated JDK.
Address compiler warnings
Warnings are there for a reason, and if you read them they often talk about features that may be going away in the future. If you're working with Java 8, on a Java 8 JDK, and you see deprecation warnings or warnings of features you shouldn't be using (see Figure 1), fix these warnings before trying to move to a newer JDK.
Figure 1 - Example compiler warnings from JDK 8
Note that deprecation is being taken much more seriously in this modern Java world, and both Java 10 and Java 11 removed APIs.
Check for use of Internal APIs
One of the changes that happened in Java 9 was that internal APIs (largely those classes in packages that started with sun.misc.*
) were hidden from use.
There’s a tool, jdeps, that’s part of the JDK that you can run with a flag of -jdkinternals
to check to see if a set of classes is using something it shouldn’t. For example, if I run the following command in the output directory of my project:
> $JAVA_HOME\bin\jdeps -jdkinternals .
Then I get the following output:
. -> /Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/rt.jar
com.mechanitis.demo.sense.twitter.connector.TwitterOAuth (.)
-> sun.misc.BASE64Encoder JDK internal API (rt.jar)
-> sun.misc.Unsafe JDK internal API (rt.jar)
Warning: JDK internal APIs are unsupported and private to JDK implementation that are subject to be removed or changed incompatibly and could break your application.
Please modify your code to eliminate dependency on any JDK internal APIs. For the most recent update on JDK internal API replacements, please check: https://wiki.openjdk.java.net/display/JDK8/Java+Dependency+Analysis+Tool
JDK Internal API Suggested Replacement
---------------- ---------------------
sun.misc.BASE64Encoder Use java.util.Base64 @since 1.8
sun.misc.Unsafe See http://openjdk.java.net/jeps/260
This tool not only identifies the classes that use the internal APIs, but provides a suggestion for what to use instead.
Upgrade your build tool
If you're using Maven or Gradle you need to upgrade.
Gradle
Gradle 5.0 introduced support for Java 11, so we should be using at least 5.0. The current version is 5.3.1 and was released last month.
Maven
We need to be running at least version 3.5.0 (the current version is 3.6.0), and we need to make sure the Maven compiler plugin is at least version 3.8:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<release>11</release> <!-- or 12 -->
</configuration>
</plugin>
Upgrade your dependencies
Some of the issues you may have heard about with regards to migrating to Java 9 or later are problems faced by (and potentially fixed by) libraries and frameworks. For example, many frameworks use reflection and internal APIs under the covers. In order for your application to have the best chance of continuing to function correctly, you should ensure all your dependencies are up to date. Many libraries have already been updated to work with Java 9 and beyond, and there’s an ongoing community effort to ensure this continues.
Some dependencies may need to be replaced. A number of libraries have moved to Byte Buddy for code generation and proxies, for example, as it works well with all modern versions of Java. When investigating a migration to Java 11, you must have a good understanding of your dependencies and whether they have been updated to support versions of Java later than 8.
Add dependencies for functionality that has moved
APIs that are not core to the JDK have been removed. This includes Java EE and Corba modules and JavaFX. This is usually straightforward to fix, by adding the correct library to your dependency management. For example, adding a dependency on JAXB to Gradle or Maven. JavaFX is a slightly more complex case, but there is good documentation at the OpenJFX site.
Run the application with the new JDK
You don't need to recompile your application to use the latest version of Java, that's part of the reason the language developers work so hard to maintain backwards compatibility. You may be able to run your application in your continuous integration environment (for example) using a new JDK without any code changes.
Compile with the new JDK
We could still be compiling our applications with Java 8 for all these previous steps. Only when you've done these other steps should you consider compiling against Java 11 or 12. Remember, we can compile our application with a lower language version if we don’t want to use new features so we remain open to rolling back to the old version. For example, in Maven:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<release>8</release>
</configuration>
</plugin>
Or in Gradle:
sourceCompatibility = JavaVersion.VERSION_1_8
Start using the new features
Only when everything works, all your tests run and everything looks good performance-wise, and maybe after some time running safely in production, should you consider using the new language features.
Also remember, although the Java 9 release was all about the Java Module System, applications don’t need to use it at all even if they have migrated to a version that supports it. However if you are interested in adopting the module system, we have a tutorial on that here at InfoQ.
In Summary
Lots of things have changed since Java 8: we get releases every 6 months; licensing, updates and support have changed; where we get our JDK from may have changed. On top of that, of course there are new language features, including the major changes that went into Java 9. But now that Java 11 has replaced Java 8 as the latest LTS, and now that major libraries, frameworks and build tools have adopted the latest versions of Java, it is a good time to migrate your application to Java 11 or 12.
Once "over the hump" of this first upgrade, it's worth at least testing the application on the latest version of Java every 6 months, for example in CI. In an ideal world, you might even be able to get on this six monthly release train, using the next version of Java every 6 months and being able to use new features as soon as they're available.
For more information on upgrading from Java 8 see the Life Beyond Java 8 presentation.
About the Author
Trisha Gee is a Java Champion and has developed Java applications for a range of industries, including finance, manufacturing, software and non-profit, for companies of all sizes. She has expertise in Java high performance systems, is passionate about enabling developer productivity, and dabbles with open source development. She’s the Java Developer Advocate for JetBrains, which is the perfect excuse to live on the bleeding edge of Java technology.