Last weekend, I joined the amazing JSpirit unconference. What a venue—a working distillery! We discussed Java, surrounded by barrels and aromatic (and presumably slightly alcoholic) vapors. Here is what I learned about Graal.
A great strength of Java is the virtual machine. The just-in-time compiler monitors the running program and optimizes the “hot spots” that actually execute a lot. Because the VM knows about all classes that have been loaded, it can make optimizations that a regular ahead-of-time compiler cannot make. For example, if a method is never overridden (such as 99% of your boring getFoo
methods), the body of the method can be inlined. In the unlikely event that a class is loaded that does override the method, the inlining can be undone.
But the just-in-time compiling increase startup time. The JIT needs to “warm up” and run some code in slow, interpreted mode, before it can figure out what to compile into machine code. When an app server runs for months on end, the warm up doesn't matter, but it's not so great when a Java program only runs for a brief time, as is common these days with “serverless” functions.
That's where Graal comes it. It can be used to call between Java and other programming languages—hence the holy grail reference. But I think most people care more about the fact that Graal can also produce native code. Of course, a native executable starts quickly. The biggest limitation: no dynamic class loading. You couldn't produce an app server with Graal, but in these days of microservices, who cares.
You can download Graal from graalvm.org
, but the installation directions for the free “community edition” are less than stellar. this blog helped me out.
On my Linux machine, I downloaded the graalvm-ce-java11-linux-amd64-version.tar.gz
fine from the Github release page. Mac and Windows releases are on the same page.
Uncompress somewhere—the home directory is fine for getting started. Then run
path/to/graalvm/bin/gu install native-image
Now put a Hello, World program somewhere and run
path/to/graalvm/bin/javac HelloWorldApp.java path/to/graalvm/bin/native-image HelloWorldApp
You will notice that the second step takes a good long time. The result is an executable file helloworldapp
. You can run it as
./helloworldapp
It starts instantly 😁
Also, it's 6.7MB in size.
The Hello, World class file is 255 bytes. Of course, that doesn't really count. When it runs, there is a whole virtual machine. And the shared library libjvm.so
is 21.7MB.
What about other languages? I tried the Hello, 世界 program in Go. It came in at 2MB. I checked with readelf that the executable was statically linked. Apparently, Go programmers are shocked, shocked. But of course, Go has a nontrivial execution environment that provides services such as garbage collection and coroutines, so that is only to be expected.
And Rust did no better, with the “Hello, World!” program at 2.5MB.
With C, statically linked, it was 825KB.
So, I suppose 6.7MB is bigger, but not an order of magnitude bigger than Go and Rust. Presumably the Graal Java runtime works harder.
When I first heard about Graal, someone said that it doesn't handle reflection. I didn't quite understand. When an object is in memory, surely one could instrument it with sufficient information to make reflection work. Maybe not for every object, but perhaps one could specify at compile-time which classes, methods, or fields should be so instrumented?
And that's exactly how it works.
Here are instructions for configuring the native compiler. I tried it out with the following example:
import java.time.*; import java.beans.*; import java.lang.reflect.*; public class BeanTest { public static void main(String[] args) throws ReflectiveOperationException, IntrospectionException { LocalTime now = LocalTime.now(); BeanInfo info = Introspector.getBeanInfo(LocalTime.class, Object.class); for (PropertyDescriptor pd : info.getPropertyDescriptors()) { Method getter = pd.getReadMethod(); System.out.println("Property " + pd.getName() + " has value " + getter.invoke(now)); } } }
When you run the program with java BeanTest
, you get an output such as
Property hour has value 18 Property minute has value 40 Property nano has value 658866000 Property second has value 15
Of course, the Introspector.getBeanInfo
method must enumerate all public methods of the given class to find out which method names start with get
.
When compiling and running natively, I was surprised to get no output at all. From reading through this article, I expected an exception.
Then I made a file reflect-config.json
with this content:
[ { "name" : "java.time.LocalTime", "allDeclaredConstructors" : true, "allPublicConstructors" : true, "allDeclaredMethods" : true, "allPublicMethods" : true, "allDeclaredClasses" : true, "allPublicClasses" : true } ]
and compiled with
/path/to/graalvm/bin/native-image -H:ReflectionConfigurationFiles=reflect-config.json BeanTest
Now the resulting executable worked correctly.
The Graal docs advertise a tracing agent that can produce this configuration file automatically. Unfortunately, that didn't work for me—I got an empty JSON file for reflection.
So, that's what a project such as Quarkus needs to do—figure out where reflection is needed and instruct the native compiler.
The native beantest
, by the way, was 9.6MB.
In this list of limitations, you can see that lots of features are supported. But serialization is not one of them. It ominously says: “Java serialization is currently not supported with native image. Thus classes implementing java.io.Serializable or using java serialization primitives eg : Object[Input|Output]Stream.[read|write]Object will report an error”
I didn't understand this because obviously classes such as LocalTime
or String
implement Serializable
. And I don't think the wording is right. I was able to run the following program:
public class SerializableTest { public static void main(String[] args) { Person fred = new Person("Fred"); System.out.println("Hello, " + fred.getName()); } } class Person implements java.io.Serializable { private String name; public Person(String name) { this.name = name; } public String getName() { return name; } }
Of course, when you try to serialize the object,
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("fred.ser")); out.writeObject(fred);
then you get an exception:
Exception in thread "main" com.oracle.svm.core.jdk.UnsupportedFeatureError: ObjectOutputStream.writeObject() at com.oracle.svm.core.util.VMError.unsupportedFeature(VMError.java:101) at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:68) at SerializableTest.main(SerializableTest.java:7)
This appears to be a seriously cool technology. Why should I mess with a toy language 😁 when I can actually use Java—ok, most of Java. I'll have to spend more time understanding it all, and start applying it to real projects.
The other cool thing, to me, is the unconference format. I went to JSpirit and boldly put up a sticky tag saying “teach me how Graal works”. There was no one expert, but of the people who came, more than half knew something, and collectively they knew a lot, and I learned a lot.—“It is not the answer that enlightens, but the question.”
Comments powered by Talkyard.