¡No te pierdas ninguna publicación! Suscríbete a The Softtek Blog
A finales del año pasado tuve la oportunidad de asistir al Commit en Madrid, donde participé en algunas charlas relacionadas con el mundo de Java en las que se mencionó, brevemente, el nombre de GraalVM seguido de una frase del tipo "está muy bien, tenéis que echarle un vistazo". Si no recuerdo mal, en una de ellas llegaron a decir "puede generar ejecutables a partir de código Java".
En esta serie vamos a ver de qué se trata y alguna de las características que más me han llamado la atención.
GraalVM es una nueva máquina virtual poliglota de alto rendimiento de Oracle que permite interactuar con lenguajes basados en la JVM (Java, Scala, Kotlin, Clojure…), y también con lenguajes de programación invitados escritos en JavaScript, Python, Ruby, R, C o C++ a través del framework Truffle. Por ello, el propio equipo de GraalVM lo define como One VM to Rule Them All (Una máquina virtual para gobernarlos a todos). Esta frase la podéis ver en la web en el título de uno de los videos, así como en alguna documentación que podéis encontrar al buscar información sobre el tema.
Ya solo con eso promete bastante, pero además tiene otras características como por ejemplo:
Y estas son solo algunas.
Durante esta serie, revisaremos las más llamativas como, por ejemplo, el de combinar los lenguajes o crear esos ejecutables.
Pero antes veamos cómo está montada la arquitectura de GraalVM y qué nos aporta cada una de sus capas.
Como digo, GraalVM es un producto compuesto por varias capas.
Tenemos el compilador de Graal, que se trata de un compilador Just-In-Time (JIT) escrito en Java. Al estar escrito en Java, permite que el equipo de GraalVM tenga mayor control, por ejemplo para la gestión de errores, y les permita usar herramientas actuales como IDEs e independencia del HotSpot de Java. Este compilador lo usaremos para los lenguajes basados en la JVM (Java, Scala, Kotlin, Clojure…), que se comunica con el HotSpot de Java a través del interfaz del compilador de la JVM (JVMCI).
Por encima tenemos el framework Truffle, que permite crear tu propio lenguaje a través de un árbol de sintaxis abstracto (AST). En la distribución de GraalVM ya podemos usar las implementaciones de JavaScript, Ruby, Python o R que se han realizado para este framework. Y como digo, podríamos crear nuestro propio lenguaje.
A su vez tenemos por encima Sulong, un intérprete de bitcode de la máquina virtual de bajo nivel (LLVM) que se usa para ejecutar C/C++ o Fortran.
Finalmente tenemos SubstrateVM por debajo del compilador de Graal. Se trata de un framework que permite compilación Ahead-Of-Time (AOT) con el que se crean los ejecutables nativos.
Acabamos de ver estos conceptos en la descripción de la arquitectura y convendría un pequeño repaso para tenerlos claros antes de empezar a usar GraalVM.
El bytecode que se genera tras la compilación no puede ser ejecutado por el sistema operativo y solo lo podría hacer a través de un intérprete. Ejecutarlo con un intérprete es más lento que ejecutar código nativo. Por ello la JVM realiza una compilación durante la propia ejecución (Just-In-Time) para pasar del bytecode a código nativo y que pueda ser ejecutado por el sistema.
La JVM tiene dos tipos de compiladores JIT, Cliente y Servidor (llamados C1 y C2 respectivamente). El compilador C1 se caracteriza por poder realizar la compilación rápidamente, para que la aplicación tenga un inicio rápido, pero genera un código poco optimizado. En cambio, el C2 es más lento ejecutándose pero genera un código mucho más optimizado. Deberíamos usar C1 para programas de corta duración y C2 para los de larga. Pero si no especificamos nada la JVM lanza inicialmente en un modo interpretado, monitorizando las llamadas a métodos y compilando con C1 aquellas que se realizan más frecuentemente. Y no queda ahí la cosa, ya que si el número de llamadas sobre esos métodos se incrementa recompilara de nuevo estos métodos, pero en este caso con la compilación C2.
De este modo el compilador va monitorizando la ejecución del programa y realiza un análisis de manera dinámica, aprendiendo del comportamiento de la aplicación durante su ejecución. Así puede realizar optimizaciones dinámicamente.
Algunas de estas optimizaciones son:
Aún con todas estas optimizaciones, la carga inicial de la aplicación es más lenta al tener que aplicar a una compilación en tiempo de ejecución para obtener el código nativo.
Por ello, de manera alternativa, podemos realizar una compilación anticipada (Ahead-Of-Time) para pasar el bytecode a código nativo una única vez. La carga inicial es más rápida, ya que hemos eliminado la compilación en tiempo de ejecución, pero al mismo tiempo el compilador no puede realizar todas las optimizaciones de las que sería capaz el JIT porque, al contrario que el JIT, el compilador AOT no tiene acceso al comportamiento dinámico de la aplicación ejecución.
Tras aclarar estos conceptos, ya podemos pensar que deberemos usar la compilación JIT cuando el proceso vaya a ser de larga ejecución, por ejemplo procesos batch o para aplicaciones ejecutándose en el servidor como puede ser un API REST. Y deberemos usar la compilación AOT para procesos cortos en los que nos interese un inicio rápido y en los que no dé tiempo a aplicar optimizaciones, por ejemplo procesos de línea de comandos o, aplicado a servicios en la nube, Funciones como servicio (FaaS).
Veremos en uno de los siguientes post cómo realizando pruebas vemos que el mismo programa se ejecuta más rápido con una u otra compilación, según la duración.
GraalVM está disponible para Linux y MacOS, y recientemente se ha añadido una versión incompleta para Windows.
Hay dos ediciones, Community (open source y gratis para uso en producción) y Enterprise.
Para instalarlo solo hay que descargarlo, descomprimirlo y añadirlo al path. Os dejo aquí el link donde se explica más detalladamente.
Yo, pese a usar un Mac, he optado por usar la imagen oficial de GraalVM para Docker, por eso de no cambiar nada en mi equipo y poder empezar a probarlo más rápidamente.
$ docker pull oracle/graalvm-ce
$ docker run -it -v $(pwd):/demo -w /demo oracle/graalvm-ce bash
Primero descargo la imagen de GraalVM con docker pull, en mi caso contiene la versión 19.0.0 de GraalVM que salió en Mayo, y, a continuación, lanzo el contenedor a partir de esa imagen con docker run.
Los argumentos que le paso al comando run son los siguientes:
Si accedemos a la carpeta bin de la instalación podremos ver todos los ejecutables disponibles:
bash-4.2# ls /opt/graalvm-ce-19.0.0/bin/
appletviewer javadoc jjs keytool rmid
clhsdb javah jmap lli rmiregistry
extcheck javap jps native2ascii schemagen
gu java-rmi.cgi jrunscript node serialver
hsdb jcmd js npm servertool
idlj jconsole jsadebugd orbd tnameserv
jar jdb jstack pack200 unpack200
jarsigner jdeps jstat policytool wsgen
java jhat jstatd polyglot wsimport
javac jinfo jvisualvm rmic xjc
Como podéis ver se listan entre otros: java, javac, java, gu, js, node o npm.
Pero no vemos Ruby, Python o R. En caso de necesitarlos tendríamos que usar el Graal Updater, podríamos lanzarlo usando el comando gu del directorio bin.
Vamos a aprovechar que durante esta serie de posts crearemos alguna imagen nativa y que la herramienta native-image no se encuentra en la distribución por defecto. Empecemos comprobando qué componentes hay disponibles para instalar con el subcomando available.
bash-4.2# gu available
Downloading: Component catalog from www.graalvm.org
ComponentId Version Component name Origin
--------------------------------------------------------------------------------
native-image 19.0.0 Native Image github.com
python 19.0.0 Graal.Python github.com
R 19.0.0 FastR github.com
ruby 19.0.0 TruffleRuby github.com
Podéis ver que native-image está en el listado. Por lo que podemos pasar a instalarlo, para ello usaremos el subcomando install:
bash-4.2# gu install native-image
Downloading: Component catalog from www.graalvm.org
Processing component archive: Native Image
Downloading: Component native-image: Native Image from github.com
Installing new component: Native Image licence files (org.graalvm.native-image, version 19.0.0)
Refreshed alternative links in /usr/bin/
Si quisiésemos desinstalarlo, usaríamos el subcomando uninstall.
bash-4.2# gu uninstall native-image
Ya con GraalVM instalado y añadido al path, vamos a probar unos *Hola Mundo*:
Java
bash-4.2# javac HelloWorld.java && java HelloWorld
Hello World!
JavaScript
bash-4.2# js
const sayHello = () => console.log('Hello World!')
sayHello()
Hello World!
bash-4.2# js HelloWorld.js
Hello World!
bash-4.2# node HelloWorld.js
Hello World!
Nada llamativo, funciona como esperábamos. Aunque hay que recordar que estamos usando la distribución de GraalVM y no las respectivas plataformas.
En este post hemos visto las características principales de GraalVM, los distintos tipos de compilación, cómo instalar GraalVM y cómo ejecutar nuestras aplicaciones.
El siguiente post será algo más práctico, por fin empezaremos a ver código y jugaremos un poco con la API de poliglotismo.
¡Suscríbete para no perdértelo!