GraalVM, wat heb je eraan?

Java en native performance is al sinds het begin een discussiepunt. De JVM is in de loop der jaren weliswaar een heel stuk sneller geworden en kan voor veel doeleinden aardig concurreren met andere talen en platformen, toch zijn er nog wel een paar punten waar het nog een achterstand heeft op native applicaties. Met name als het om opstarttijd en geheugengebruik gaat, dan is de JVM relatief langzaam en gulzig.

Auteurs: Koen Hengsdijk & Thomas Zeeman 

In dit artikel willen we een alternatief bespreken dat sinds enige tijd door Oracle wordt ontwikkeld en beschikbaar gesteld: GraalVM [1].

GraalVM is meerdere dingen in een. Het project omschrijft zichzelf als een high-performance runtime, ideaal voor microservices. Naast Java ondersteunt het ook nog een aantal andere talen als JavaScript, C, C++, Ruby en Python. Ook kan het bijvoorbeeld in Oracle DB draaien.

Allemaal hartstikke interessant, maar voor dit artikel beperken we ons tot Java en de onderdelen die het high-performance maken. Dat houdt in dat we gaan kijken naar GraalVM als alternatief voor de standaard Hotspot compiler en naar de native image optie.

De eerste optie, GraalVM als alternatieve JIT-compiler, is de minst ingrijpende vorm. Deze gebruikt nog steeds de JVM en je compileert je project nog steeds naar byte-code. Je hoeft alleen een aantal opties aan te zetten voor de runtime:

  -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler

Dit werkt officieel vanaf Java 10, onofficieel vanaf 9.

De tweede optie, waarbij de code naar een native image wordt omgezet, is een stuk ingrijpender. Al is het maar omdat de machine waarop je de build uitvoert een aangeraden minimum van 32GB RAM nodig heeft. Tijdens vroege testen met een oude laptop met 8GB bleek dit geen loze kreet. De machine was tijdens de build onbruikbaar en geregeld mislukte deze. Een nieuwere machine met 16GB ging al beter, maar ook hier ging het nog wel eens mis.

Naast deze build requirements zijn er nog andere restricties. Bepaalde technieken als reflectie en dynamische code injectie werken niet. Er zijn voor de meeste technieken inmiddels wel mogelijkheden om deze toch te gebruiken, maar dat kan betekenen dat je het een en ander moet configureren.

Ondersteuning in populaire frameworks

Voor het ontwikkelen van moderne native Java applicaties met GraalVM zijn er meerdere frameworks geschikt. De frameworks Quarkus en Micronaut zijn allebei relatief jonge frameworks en richten zich beide op het ontwikkelen van container first microservices.

Zowel Micronaut als Quarkus ondersteunen het compileren van native images out of the box, met een grote hoeveelheid aan extensions om het leven van een developer makkelijker te maken. Denk hierbij aan standaard functies van een service zoals de interactie met databases en het ontwikkelen van een REST API. Maar naast standaard functionaliteiten bieden deze frameworks ook erg interessante functionaliteiten aan voor developers gewend aan het Spring Boot framework. Quarkus en Micronaut bieden namelijk extensions aan die een subset van Spring en Spring Boot functionaliteit beschikbaar maken in Quarkus en Micronaut. De frameworks mappen de Spring annotaties naar de alternatieven van Quarkus of Micronaut waardoor een Spring developer zich snel kan aanpassen aan deze nieuwe omgeving, mits de ondersteunde subset voldoende is voor de te bouwen applicatie.

Spring is ook bezig met het ondersteunen van GraalVM. Op dit moment biedt Spring het experimentele project Spring-graalvm-native aan met het doel om het compileren van native images out-of-the-box te laten werken voor Spring Boot applicaties. Voor nu is er echter alleen nog maar een alpha versie beschikbaar en is het project nog niet geschikt voor gebruik in productie. Het is zeker mogelijk om simpele Spring Boot apps te compileren naar native images met behulp van dit project en handmatige configuratie, maar het is een lastig proces wat nog lastiger wordt naarmate de applicatie ingewikkelder wordt.

GraalVM: de cijfers

Om een en ander te vergelijken, is er een simpel domein model uitgewerkt en op basis daarvan zijn REST based services gemaakt in Micronaut, Quarkus en Spring Boot.
Vervolgens hebben we over een aantal assen gemeten wat de invloed van GraalVM als JIT-compiler en met native-image is. Zie [3] voor de gebruikte code. Als assen zijn opstarttijd, geheugengebruik en grootte van de container inclusief applicatie gekozen. De achterliggende reden is dat we wilden zien welke voordelen dit voor een cloud-deployment zou kunnen hebben.

Zoals uit alle drie de grafieken wel blijkt, kan het gebruik van native-image een enorme winst opleveren. Dat is bij alledrie de projecten wel de trend. De verschillen zijn verder klein tussen de drie frameworks, al lijkt Quarkus bij alledrie de testen als zuinigste uit de bus te komen. Daarnaast is ook het gebruik van GraalVM als Jit-compiler al een interessante optie. Zeker het geheugengebruik gaat er al een stuk op vooruit.

Hoe werkt dat dan en wat kunnen we daar nu mee?

Zoals te zien, levert Graal in beide varianten een verbetering op ten opzicht van de default JVM, maar hoe komt dat dan? Bij GraalVM als JIT-compiler zit de winst in een verbeterde analyse van je code. Met name Partial Escape Analysis (PAE, [2]) is verantwoordelijk daarvoor, maar ook meer inlining enkele andere optimalisaties helpen mee. Samen zorgt dit ervoor dat er meer objecten op de stack geplaatst kunnen worden in plaats van op de heap. Dat is een stuk goedkoper in geheugen toewijzing en, later, garbage collection. Dat verklaart vooral het lagere geheugengebruik, maar ook een deel van de performanceverbeteringen.

Wel merken we dat de verbeteringen minder groot zijn, dan sommige van de voorbeelden die op de GraalVM site staan of in de praatjes van bijvoorbeeld Twitter of de Nederlandse Politie naar voren komen. Dat komt mogelijk doordat de laatste twee organisaties veel gebruik maken van Scala en deze door haar manier van geheugengebruik meer uit de verbeteringen van GraalVM haalt, dan als je een zelfde applicatie in Java zou draaien.

Verder dient opgemerkt te worden dat GraalVM, omdat het in Java geschreven is, ook een stukje heap in gebruik neemt. Rond de 40 MB lijkt dat momenteel in beslag te nemen. Als je applicatie klein genoeg is, dan kan dat genoeg zijn om de positieve effecten van GraalVM teniet te doen.

Waar je wel spectaculaire verbeteringen ziet is bij de native-image compilatie. De opstarttijden zijn al gauw in de sub-milliseconde en ook het geheugengebruik en de image groottes zijn fors lager. Hoewel ook hier de compiler de verbeteringen gebruikt die GraalVM als JIT-compiler ook gebruikt, is iets anders hier datgene wat de doorslag geeft. Voor de native-image compilatie wordt namelijk gekeken welke code er allemaal daadwerkelijk gebruikt wordt. Dat kan de vele tientallen zo niet een paar honderd megabyte aan libraries en JVM code al snel reduceren tot enkele megabytes.
Dat heeft zowel zijn weerslag in de grootte van de binary, de opstarttijd en het geheugengebruik. Wat er niet is, hoeft immers ook niet opgeslagen, geladen of gecompileerd te worden. Aanvullend is daar dan nog dat de GraalVM compiler een initiële heap opzet. Daarin worden zoveel mogelijk van de objecten alvast geïnitialiseerd opgenomen, zodat dat niet bij het opstarten hoeft te gebeuren.

Met name de eerste techniek zorgt voor een aantal uitdagingen. AOP, reflectie en andere dynamische technieken kun je vaak niet goed van te voren voorspellen. Met hints is dat te ondervangen, zodat bepaalde code alsnog gecompileerd wordt en in het native-image terecht komt. Zeker bij Micronaut en Quarkus is daar al een hoop voor je gedaan voor de standaard onderdelen. Het Spring framework heeft nog het een en ander te doen daar, al is het experimentele Spring Graal native project hard aan de weg aan het timmeren om developers te ondersteunen.

Onder de streep valt te concluderen dat GraalVM een interessante ontwikkeling is. De JIT-compiler is al experimenteel aanwezig in de JVM en zal ongetwijfeld een keer C2 vervangen als standaard. Tot die tijd is het nog uitproberen of het voor jouw use case al interessant is.

De route met native-images lijkt voor nu nog een brug te ver voor veel use cases. Al is het maar omdat er nog heel wat ontwikkeling nodig is om allerhande libraries geschikt te maken. Heb je echter een use case, bijvoorbeeld serverless of REST-based microservices, waarbij Micronaut of Quarkus een mogelijkheid is en waar de andere restricties niet zo’n grote rol spelen, dan is het zeker de overweging waard.

Links:

[1] https://www.graalvm.org/

[2] http://www.ssw.uni-linz.ac.at/Research/Papers/Stadler14/Stadler2014-CGO-PEA.pdf

[3] https://github.com/khengsdijk/test-projecten-graalvm

Bio’s:

Koen Hengsdijk is Java developer bij DPG media en voormalig afstudeerder bij Trifork. Voor zijn scriptie bij Trifork hield hij zich bezig met onderzoek naar de bruikbaarheid van GraalVM native images.

Thomas Zeeman is software architect bij Trifork. Naast software ontwikkelen houdt hij zich ook bezig met begeleiden van afstudeerders. Daarbij komt ook geregeld zijn interesse in nieuwe ontwikkelingen op het gebied van software ontwikkeling aan de orde.