Moving along through our detailed Java Exception Handling series, today we'll be taking a closer look at the UnsupportedClassVersionError. The UnsupportedClassVersionError
can be rather confusing, but the simplest explanation is that it occurs when the Java Virtual Machine (JVM
) attempts to access a class file that was compiled using a different Java version than is currently supported.
In this article we'll dig into what might cause a particular version to be unsupported, where UnsupportedClassVersionErrors
fit into the larger Java Exception Hierarchy, and how changing the way in which classes are compiled can sometimes work around these potential issues. Let's get going!
All Java errors implement the java.lang.Throwable
interface, or are extended from another inherited class therein. The full exception hierarchy of this error is:
java.lang.Object
java.lang.Throwable
java.lang.Error
java.lang.LinkageError
java.lang.ClassFormatError
UnsupportedClassVersionError
Unlike most of our Java Exception Handling articles thus far, this one won't include a great deal of functional source code. Since the UnsupportedClassVersionError
is thrown due to compilation and versioning issues, as opposed to most errors that are due to problems in the source code, we'll forego a lot of code in this post and focus more on the explanation and nuances of compilation issues within Java.
As previously mentioned, an UnsupportedClassVersionError
is thrown when the JVM
attempts to use a class that was compiled using an incompatible or unsupported Java version than what is currently running. For example, the most recent stable version of Java is 8
, and there have been about ten major versions since the beta release way back in 1994. Each new major version brings a great deal of features and syntactic improvements, just like with most languages, which dramatically improve the flexibility and ease of use for developers that are able to upgrade to the new platform.
However, migrating an existing application to the latest and greatest major Java version isn't always feasible. Many mission-critical systems have been built over the years using Java. Google, Amazon, YouTube, eBay... the list of massive companies and projects that rely on Java goes on and on. In many cases, it simply isn't feasible to upgrade existing, functional systems to the newest releases, given the potential of introducing massive, unforeseen issues when doing so.
This conundrum may invariably lead to a scenario in which a Java application that was compiled using one (typically older) version of Java, must be executed on a different version. For the most part, backward compatibility is supported by newer versions, meaning that older applications can be run on newer versions of the JVM
. However, some situations may arise where there are incompatibilities between the version the application (or class
) was compiled on, and the version that is attempting to execute said application. It is these situations where an UnsupportedClassVersionError
may be thrown.
For example, consider the following (extremely simple) Main.java
file:
public class Main {
public static void main(String[] args) {
System.out.println(String.format("Main.class running on %s", System.getProperty("java.version")));
}
}
We've got almost nothing going on in this code. When the main(String[] args)
method is executed, we print out a confirmation message that also includes the java.version
property value, which will indicate the currently executing Java version. Thus, on my own system that is running Java 8, executing the main(String[] args)
method produces the following output:
Main.class running on 1.8.0_131
This means that I'm running on the latest stable major version 1.8
(aka 8
), with a minor version of 131
. I happen to have a the unstable Java 9
installed, which I'm able to verify by running the update-alternatives
command from the terminal:
$ sudo update-alternatives --config java
There are 4 choices for the alternative java (providing /usr/bin/java).
Selection Path Priority Status
------------------------------------------------------------
* 0 /usr/lib/jvm/oracle_jdk8/jre/bin/java 2000 auto mode
1 /usr/lib/jvm/java-7-openjdk-amd64/jre/bin/java 1071 manual mode
2 /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java 1081 manual mode
3 /usr/lib/jvm/java-9-openjdk-amd64/bin/java 1091 manual mode
4 /usr/lib/jvm/oracle_jdk8/jre/bin/java 2000 manual mode
I can use the unstable Java 9
version by selecting it from the prompt. After doing so, let's execute the Main
class again and see what we get:
$ java Main
Main.class running on 9-internal
Cool! Since it's preferable to use the stable release in most cases, I'll switch back to Java 8
for now.
Now, what if we want to compile an application using a specific version of Java? We can accomplish that by providing a few options flags to the javac
compiler.
For example, if we wanted to compile the Main
class that was written in version 1.7
we'd use the -source 1.7
flag:
$ javac -source 1.7 Main.java
warning: [options] bootstrap class path not set in conjunction with -source 1.7
1 warning
As you can see, we're given a warning in indicating that we need to specify the bootstrap path. The bootstrap
classes are a collection of classes packaged up into an rt.jar
file, which is included with each JRE
or JDK
installation. This file essentially houses all the runtime classes that comprise the core Java API. Thus, the javac
compiler needs to know where it can find the rt.jar
file that is from the matching Java version we're compiling from.
Let's now try adding the -bootclasspath
option and tell the compiler where to look. Since no output is typically shown when successfully compiling, we also added the -verbose
flag to see what's going on:
$ javac -source 1.7 -verbose -bootclasspath /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar Main.java
[parsing started RegularFileObject[Main.java]]
[parsing completed 11ms]
[search path for source files: .]
[search path for class files: /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar,/usr/lib/jvm/oracle_jdk8/jre/lib/ext/cldrdata.jar,/usr/lib/jvm/oracle_jdk8/jre/lib/ext/dnsns.jar,/usr/lib/jvm/oracle_jdk8/jre/lib/ext/jaccess.jar,/usr/lib/jvm/oracle_jdk8/jre/lib/ext/jfxrt.jar,/usr/lib/jvm/oracle_jdk8/jre/lib/ext/localedata.jar,/usr/lib/jvm/oracle_jdk8/jre/lib/ext/nashorn.jar,/usr/lib/jvm/oracle_jdk8/jre/lib/ext/sunec.jar,/usr/lib/jvm/oracle_jdk8/jre/lib/ext/sunjce_provider.jar,/usr/lib/jvm/oracle_jdk8/jre/lib/ext/sunpkcs11.jar,/usr/lib/jvm/oracle_jdk8/jre/lib/ext/zipfs.jar,.]
[loading ZipFileIndexFileObject[/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar(java/lang/Object.class)]]
[loading ZipFileIndexFileObject[/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar(java/lang/String.class)]]
[checking Main]
[loading ZipFileIndexFileObject[/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar(java/io/Serializable.class)]]
[loading ZipFileIndexFileObject[/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar(java/lang/AutoCloseable.class)]]
[loading ZipFileIndexFileObject[/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar(java/lang/System.class)]]
[loading ZipFileIndexFileObject[/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar(java/lang/Comparable.class)]]
[loading ZipFileIndexFileObject[/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar(java/lang/CharSequence.class)]]
[loading ZipFileIndexFileObject[/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar(java/util/Locale.class)]]
[loading ZipFileIndexFileObject[/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar(java/io/PrintStream.class)]]
[loading ZipFileIndexFileObject[/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar(java/lang/Appendable.class)]]
[loading ZipFileIndexFileObject[/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar(java/io/Closeable.class)]]
[loading ZipFileIndexFileObject[/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar(java/io/FilterOutputStream.class)]]
[loading ZipFileIndexFileObject[/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar(java/io/OutputStream.class)]]
[loading ZipFileIndexFileObject[/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar(java/io/Flushable.class)]]
[wrote RegularFileObject[Main.class]]
[total 127ms]
This confirms that our Main.class
was successfully compiled, so let's try executing it:
$ java Main
Main.class running on 1.8.0_131
Interesting. So, even though we explicitly sourced this compile using version 1.7
, it was still able to execute just fine on our version 1.8
installation. As previously mentioned, this is expected due to backward compatibility of most newer Java versions.
Let's change our JVM
to run on version 1.7
, and try executing the Main
class again:
update-alternatives: using /usr/lib/jvm/java-7-openjdk-amd64/jre/bin/java to provide /usr/bin/java (java) in manual mode
$ java Main
Main.class running on 1.7.0_95
As expected, we're able to run our script sourced and targeting version 1.7
in a JVM
using Java 7
(i.e. 1.7
). Now, let's try compiling from one version, but targeting a different version. We can do this by adding the -target
flag to the javac
command:
$ javac -source 1.7 -target 1.8 -verbose -bootclasspath /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar
Main.java
[parsing started RegularFileObject[Main.java]]
[parsing completed 16ms]
[search path for source files: .]
[search path for class files: /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar,/usr/lib/jvm/oracle_jdk8/jre/lib/ext/cldrdata.jar,/usr/lib/jvm/oracle_jdk8/jre/lib/ext/dnsns.jar,/usr/lib/jvm/oracle_jdk8/jre/lib/ext/jaccess.jar,/usr/lib/jvm/oracle_jdk8/jre/lib/ext/jfxrt.jar,/usr/lib/jvm/oracle_jdk8/jre/lib/ext/localedata.jar,/usr/lib/jvm/oracle_jdk8/jre/lib/ext/nashorn.jar,/usr/lib/jvm/oracle_jdk8/jre/lib/ext/sunec.jar,/usr/lib/jvm/oracle_jdk8/jre/lib/ext/sunjce_provider.jar,/usr/lib/jvm/oracle_jdk8/jre/lib/ext/sunpkcs11.jar,/usr/lib/jvm/oracle_jdk8/jre/lib/ext/zipfs.jar,.]
[loading ZipFileIndexFileObject[/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar(java/lang/Object.class)]]
[loading ZipFileIndexFileObject[/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar(java/lang/String.class)]]
[checking Main]
[loading ZipFileIndexFileObject[/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar(java/io/Serializable.class)]]
[loading ZipFileIndexFileObject[/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar(java/lang/AutoCloseable.class)]]
[loading ZipFileIndexFileObject[/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar(java/lang/System.class)]]
[loading ZipFileIndexFileObject[/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar(java/lang/Comparable.class)]]
[loading ZipFileIndexFileObject[/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar(java/lang/CharSequence.class)]]
[loading ZipFileIndexFileObject[/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar(java/util/Locale.class)]]
[loading ZipFileIndexFileObject[/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar(java/io/PrintStream.class)]]
[loading ZipFileIndexFileObject[/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar(java/lang/Appendable.class)]]
[loading ZipFileIndexFileObject[/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar(java/io/Closeable.class)]]
[loading ZipFileIndexFileObject[/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar(java/io/FilterOutputStream.class)]]
[loading ZipFileIndexFileObject[/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar(java/io/OutputStream.class)]]
[loading ZipFileIndexFileObject[/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar(java/io/Flushable.class)]]
[wrote RegularFileObject[Main.class]]
[total 145ms]
Here we're sourcing
from version 1.7
, but targeting
version 1.8
. Given that we're still running our JVM
on version 1.7
, what happens now when we try to execute the Main
class?
$ java Main
Exception in thread "main" java.lang.UnsupportedClassVersionError: Main : Unsupported major.minor version 52.0
Lo and behold, our friend the UnsupportedClassVersionError
finally rears its ugly head. As the error message indicates, the Main.class
file is targeting a major.minor version
that is not supported. Obviously the number 52.0
that is provided doesn't look anything like the 1.7
and 1.8
versions we've been seeing up to now. This is merely because of how the actual bytecode of a Java .class
file is setup. Specifically, the major
version of 52
is equivalent to Java SE 8
, whereas a version of 51
would be Java SE 7
. In other words, the error is telling us Java 8
(1.8
) is unsupported, because its a newer version than the 1.7
we're currently running on, and because the Main.class
was compiled as 1.7
.
The Airbrake-Java library provides real-time error monitoring and automatic exception reporting for all your Java-based projects. Tight integration with Airbrake's state of the art web dashboard ensures that Airbrake-Java
gives you round-the-clock status updates on your application's health and error rates. Airbrake-Java
easily integrates with all the latest Java frameworks and platforms like Spring
, Maven
, log4j
, Struts
, Kotlin
, Grails
, Groovy
, and many more. Plus, Airbrake-Java
allows you to easily customize exception parameters and gives you full, configurable filter capabilities so you only gather the errors that matter most.
Check out all the amazing features Airbrake-Java has to offer and see for yourself why so many of the world's best engineering teams are using Airbrake to revolutionize their exception handling practices! Try Airbrake free for 30 days.