Last week was the release of Oracle’s GraalVM. As stated on the website:
High-performance polyglot VM
GraalVM is a universal virtual machine for running applications written in JavaScript, Python 3, Ruby, R, JVM-based languages like Java, Scala, Kotlin, and LLVM-based languages such as C and C++.
GraalVM removes the isolation between programming languages and enables interoperability in a shared runtime. It can run either standalone or in the context of OpenJDK, Node.js, Oracle Database, or MySQL.
There are several factors that could make one want to switch from a regular JRE to Graal VM:
- One of them could be the improved performance that it claims
- Another one could be the polyglot feature, to transparently mix and match supported languages
- The final one is a blend of the former: with native support, one could ship Java apps as native code
As a patented geek, I wanted to have first look real quick. Here are my first impressions.
Enterprise Edition or not?
The first step is to download Graal VM itself. It comes into two flavors:
- Community Edition
-
- All open-source license
- Free for production use
- Enterprise Edition
-
- Free for evaluation and other non-production uses
- For commercial use and support options, the sales team should be contacted
First surprise: the CE edition is only available for Linux operating systems. For OSX, one should get the EE version.
There’s no edition for Windows (yet?) |
Graal VM structure
The structure is similar to the one of traditional pre-9 Java JDK.
Hence, GraalVM can be a drop-in replacement for any standard JDK.
Running java -version
returns the following output:
java version "1.8.0_161" Java(TM) SE Runtime Environment (build 1.8.0_161-b12) GraalVM 1.0.0-rc1 (build 25.71-b01-internal-jvmci-0.42, mixed mode)
As of now, GraalVM is limited to Java 8 capabilities |
Some performance benchmarks
The next step was to check whether there was an improvement in performance. I used the JMH framework: it’s dedicated to that.
I used the following code:
public class MyBenchmark {
@Benchmark
public void testMethod() {
List<String> randomStrings = Stream.generate(() -> RandomStringUtils
.randomAlphabetic(25))
.limit(100_000)
.collect(Collectors.toList());
randomStrings.sort(String::compareToIgnoreCase);
}
}
It was tested on 3 different JREs using the java -jar target/benchmarks.jar
command-line.
# JMH version: 1.20 # VM version: JDK 1.8.0_161, VM 25.71-b01-internal-jvmci-0.42 # VM invoker: /usr/local/graalvm-1.0.0-rc1/Contents/Home/jre/bin/java # VM options: <none> # Warmup: 20 iterations, 1 s each # Measurement: 20 iterations, 1 s each # Timeout: 10 min per iteration # Threads: 1 thread, will synchronize iterations # Benchmark mode: Throughput, ops/time # Benchmark: ch.frankel.blog.MyBenchmark.testMethod Result "ch.frankel.blog.MyBenchmark.testMethod": 6.795 ±(99.9%) 0.016 ops/s [Average] (min, avg, max) = (6.477, 6.795, 6.967), stdev = 0.068 CI (99.9%): [6.778, 6.811] (assumes normal distribution) # Run complete. Total time: 00:06:59 Benchmark Mode Cnt Score Error Units MyBenchmark.testMethod thrpt 200 6.795 ± 0.016 ops/s
# JMH version: 1.20 # VM version: JDK 1.8.0_92, VM 25.92-b14 # VM invoker: /Library/Java/JavaVirtualMachines/jdk1.8.0_92.jdk/Contents/Home/jre/bin/java # VM options: <none> # Warmup: 20 iterations, 1 s each # Measurement: 20 iterations, 1 s each # Timeout: 10 min per iteration # Threads: 1 thread, will synchronize iterations # Benchmark mode: Throughput, ops/time # Benchmark: ch.frankel.blog.MyBenchmark.testMethod Result "ch.frankel.blog.MyBenchmark.testMethod": 6.727 ±(99.9%) 0.017 ops/s [Average] (min, avg, max) = (6.466, 6.727, 6.899), stdev = 0.070 CI (99.9%): [6.710, 6.743] (assumes normal distribution) # Run complete. Total time: 00:07:00 Benchmark Mode Cnt Score Error Units MyBenchmark.testMethod thrpt 200 6.727 ± 0.017 ops/s
# JMH version: 1.20 # VM version: JDK 9.0.4, VM 9.0.4+11 # VM invoker: /Library/Java/JavaVirtualMachines/jdk-9.0.4.jdk/Contents/Home/bin/java # VM options: <none> # Warmup: 20 iterations, 1 s each # Measurement: 20 iterations, 1 s each # Timeout: 10 min per iteration # Threads: 1 thread, will synchronize iterations # Benchmark mode: Throughput, ops/time # Benchmark: ch.frankel.blog.MyBenchmark.testMethod Result "ch.frankel.blog.MyBenchmark.testMethod": 7,136 ±(99.9%) 0,026 ops/s [Average] (min, avg, max) = (6,464, 7,136, 7,443), stdev = 0,111 CI (99.9%): [7,110, 7,162] (assumes normal distribution) # Run complete. Total time: 00:07:26 Benchmark Mode Cnt Score Error Units MyBenchmark.testMethod thrpt 200 7,136 ± 0,026 ops/s
Here’s the sum-up:
GraalVM | Oracle JDK 8 | Oracle JDK 9 | |
---|---|---|---|
Average ops/s |
6.795 ±(99.9%) 0.016 |
6.727 ±(99.9%) 0.017 |
7,136 ±(99.9%) 0,026 |
Min |
6.477 |
6.466 |
6,464 |
Max |
6.967 |
6.899 |
7,443 |
Std dev |
0.068 |
0.070 |
0,111 |
CI (99.9%) (assumes normal distribution) |
[6.778, 6.811] |
[6.710, 6.743] |
[7,110, 7,162] |
Numbers speak for themselves: performance-wise, the gap between Graal VM and Java 8 is not significant. There’s one between them and Java 9 though. Also, Java 9 has the highest standard deviation.
Going native
GraalVM is able to turn a JAR into a native executable, via the native-image
command-line.
I tried to do it with the created JAR.
native-image -H:+JNI -jar target/benchmarks.jar
However, when trying to run the newly created binary, I stumbled upon the following:
Exception in thread "main" java.lang.reflect.InvocationTargetException at java.lang.Throwable.<init>(Throwable.java:310) at java.lang.Exception.<init>(Exception.java:102) at java.lang.ReflectiveOperationException.<init>(ReflectiveOperationException.java:89) at java.lang.reflect.InvocationTargetException.<init>(InvocationTargetException.java:72) at com.oracle.svm.reflect.proxies.Proxy_3_Main_main.invoke(Unknown Source) at java.lang.reflect.Method.invoke(Method.java:498) at com.oracle.svm.core.JavaMainWrapper.run(JavaMainWrapper.java:199) at Lcom/oracle/svm/core/code/CEntryPointCallStubs; .com_002eoracle_002esvm_002ecore_002eJavaMainWrapper_002erun_0028int_002corg_002egraalvm_002enativeimage_002ec_002etype_002eCCharPointerPointer_0029(generated:0) Caused by: java.lang.IllegalArgumentException: class org.openjdk.jmh.runner.options.TimeValue is not a value type at java.lang.Throwable.<init>(Throwable.java:265) at java.lang.Exception.<init>(Exception.java:66) at java.lang.RuntimeException.<init>(RuntimeException.java:62) at java.lang.IllegalArgumentException.<init>(IllegalArgumentException.java:52) at joptsimple.internal.Reflection.findConverter(Reflection.java:66) at joptsimple.ArgumentAcceptingOptionSpec.ofType(ArgumentAcceptingOptionSpec.java:106) at org.openjdk.jmh.runner.options.CommandLineOptions.<init>(CommandLineOptions.java:109) at org.openjdk.jmh.Main.main(Main.java:41) ... 4 more
Worse, trying to create an image for the Spring Pet Clinic fails with the following:
error: unsupported features in 3 methods Detailed message: Error: com.oracle.graal.pointsto.constraints.UnsupportedFeatureException: Unsupported constructor java.lang.ClassLoader.<init>(ClassLoader) is reachable: The declaring class of this element has been substituted, but this element is not present in the substitution class To diagnose the issue, you can add the option -H:+ReportUnsupportedElementsAtRuntime. The unsupported element is then reported at run time when it is accessed the first time. Trace: at parsing java.security.SecureClassLoader.<init>(SecureClassLoader.java:76) Call path from entry point to java.security.SecureClassLoader.<init>(ClassLoader): at java.security.SecureClassLoader.<init>(SecureClassLoader.java:76) at java.net.URLClassLoader.<init>(URLClassLoader.java:100) at org.springframework.boot.loader.LaunchedURLClassLoader.<init>(LaunchedURLClassLoader.java:50) at org.springframework.boot.loader.Launcher.createClassLoader(Launcher.java:74) at org.springframework.boot.loader.Launcher.createClassLoader(Launcher.java:64) at org.springframework.boot.loader.Launcher.launch(Launcher.java:49) at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:51) at com.oracle.svm.reflect.proxies.Proxy_1_JarLauncher_main.invoke(Unknown Source) at java.lang.reflect.Method.invoke(Method.java:498) at com.oracle.svm.core.JavaMainWrapper.run(JavaMainWrapper.java:199) at Lcom/oracle/svm/core/code/CEntryPointCallStubs; .com_002eoracle_002esvm_002ecore_002eJavaMainWrapper_002erun_0028int_002corg_002egraalvm_002enativeimage_002ec_002etype_002eCCharPointerPointer_0029(generated:0) Error: com.oracle.graal.pointsto.constraints.UnsupportedFeatureException: Unsupported field java.net.URL.handlers is reachable To diagnose the issue, you can add the option -H:+ReportUnsupportedElementsAtRuntime. The unsupported element is then reported at run time when it is accessed the first time. Trace: at parsing java.net.URL.setURLStreamHandlerFactory(URL.java:1118) Call path from entry point to java.net.URL.setURLStreamHandlerFactory(URLStreamHandlerFactory): at java.net.URL.setURLStreamHandlerFactory(URL.java:1110) at org.springframework.boot.loader.jar.JarFile.resetCachedUrlHandlers(JarFile.java:383) at org.springframework.boot.loader.jar.JarFile.registerUrlProtocolHandler(JarFile.java:373) at org.springframework.boot.loader.Launcher.launch(Launcher.java:48) at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:51) at com.oracle.svm.reflect.proxies.Proxy_1_JarLauncher_main.invoke(Unknown Source) at java.lang.reflect.Method.invoke(Method.java:498) at com.oracle.svm.core.JavaMainWrapper.run(JavaMainWrapper.java:199) at Lcom/oracle/svm/core/code/CEntryPointCallStubs; .com_002eoracle_002esvm_002ecore_002eJavaMainWrapper_002erun_0028int_002corg_002egraalvm_002enativeimage_002ec_002etype_002eCCharPointerPointer_0029(generated:0) Error: com.oracle.graal.pointsto.constraints.UnsupportedFeatureException: Unsupported method java.security.ProtectionDomain.getCodeSource() is reachable: The declaring class of this element has been substituted, but this element is not present in the substitution class To diagnose the issue, you can add the option -H:+ReportUnsupportedElementsAtRuntime. The unsupported element is then reported at run time when it is accessed the first time. Trace: at parsing org.springframework.boot.loader.Launcher.createArchive(Launcher.java:118) Call path from entry point to org.springframework.boot.loader.Launcher.createArchive(): at org.springframework.boot.loader.Launcher.createArchive(Launcher.java:117) at org.springframework.boot.loader.ExecutableArchiveLauncher.<init>(ExecutableArchiveLauncher.java:38) at org.springframework.boot.loader.JarLauncher.<init>(JarLauncher.java:35) at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:51) at com.oracle.svm.reflect.proxies.Proxy_1_JarLauncher_main.invoke(Unknown Source) at java.lang.reflect.Method.invoke(Method.java:498) at com.oracle.svm.core.JavaMainWrapper.run(JavaMainWrapper.java:199) at Lcom/oracle/svm/core/code/CEntryPointCallStubs; .com_002eoracle_002esvm_002ecore_002eJavaMainWrapper_002erun_0028int_002corg_002egraalvm_002enativeimage_002ec_002etype_002eCCharPointerPointer_0029(generated:0) Error: Processing image build request failed
Dynamic class-loading just doesn’t work with GraalVM.
I tried with the H2 and HSQLDB standalone JARs… to no avail. In both cases, I got the following stack:
java.lang.NullPointerException at com.oracle.graal.pointsto.ObjectScanner.scanField(ObjectScanner.java:113) at com.oracle.graal.pointsto.ObjectScanner.doScan(ObjectScanner.java:263) at com.oracle.graal.pointsto.ObjectScanner.finish(ObjectScanner.java:307) at com.oracle.graal.pointsto.ObjectScanner.scanBootImageHeapRoots(ObjectScanner.java:78) at com.oracle.graal.pointsto.ObjectScanner.scanBootImageHeapRoots(ObjectScanner.java:60) at com.oracle.graal.pointsto.BigBang.checkObjectGraph(BigBang.java:581) at com.oracle.graal.pointsto.BigBang.finish(BigBang.java:552) at com.oracle.svm.hosted.NativeImageGenerator.doRun(NativeImageGenerator.java:653) at com.oracle.svm.hosted.NativeImageGenerator.lambda$run$0(NativeImageGenerator.java:381) at java.util.concurrent.ForkJoinTask$AdaptedRunnableAction.exec(ForkJoinTask.java:1386) at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289) at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056) at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692) at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)----
Conclusion
GraalVM has many promising features, including polyglot, even though I’m not much interested in that one by now. However, from my limited tire-kicking, it still has room for improvement. Performance-wise, it’s outperformed by Java 9. Also, the native image creation is too limited in its current state to be useful. IMHO, it’s in a MVP state at the moment. I’m eagerly waiting the next version, and hope they will improve on the above issues.