GraalVM Native Image en 2026: Java compilado a nativo con Spring y Quarkus

GraalVM es una JVM alternativa desarrollada por Oracle y disponible también en versión open source bajo el nombre GraalVM Community. Lo que la diferencia de la JVM clásica es su compilador JIT más avanzado y, sobre todo, la herramienta de compilación AOT (Ahead-Of-Time) que viene incluida: Native Image.

Native Image compila tu aplicación Java a un binario nativo: un ELF en Linux, un PE en Windows o un Mach-O en macOS. El resultado es un ejecutable que no necesita ninguna JVM instalada para correr. Arranca en milisegundos, usa entre 2 y 5 veces menos RAM que su equivalente en JAR y no tiene calentamiento JIT porque todo el análisis se hace en tiempo de compilación.

Eso, que suena muy bien, tiene un precio. La compilación nativa es exigente, el proceso no es trivial y hay limitaciones reales que conviene conocer antes de lanzarse a convertir todo a nativo.

Cómo funciona la compilación nativa

Native Image analiza la aplicación entera en tiempo de compilación mediante análisis de alcance estático: recorre todo el grafo de llamadas desde el punto de entrada y determina qué código puede ejecutarse realmente. Solo ese código acaba en el binario. El resto se descarta, de ahí el tamaño y el consumo de memoria tan reducidos.

El problema con este enfoque es que Java tiene mecanismos que se resuelven en runtime: la reflexión, la carga dinámica de clases y los dynamic proxies. Native Image no puede saber en compilación qué clases vas a cargar dinámicamente o qué métodos vas a invocar por reflexión si esa información solo existe en tiempo de ejecución. Por eso, hay que declarar explícitamente qué clases usan reflexión mediante ficheros de configuración JSON (principalmente reflect-config.json).

Otro detalle importante: compilar nativo consume recursos. En un proyecto Spring Boot de tamaño medio puedes esperar entre 5 y 15 minutos de compilación, varios gigas de RAM y toda la CPU disponible. No es algo que hagas en cada cambio; es para la fase de build de producción.

Spring Boot 3 y el soporte AOT oficial

Desde Spring Boot 3.x, el soporte para compilación nativa es oficial y está bien integrado. Spring AOT genera automáticamente los hint files de reflexión para todos los Spring beans, lo que elimina la mayor parte del trabajo manual de configuración.

Tienes dos formas de compilar:

  • Con Buildpacks (sin GraalVM local): genera una imagen Docker con el binario nativo dentro. No necesitas tener GraalVM instalado en tu máquina.
./mvnw -Pnative spring-boot:build-image
  • Con GraalVM instalado en local: compila directamente en tu máquina.
./mvnw -Pnative native:compile

El resultado con una app Spring Boot típica es llamativo: de arrancar en 3-8 segundos y consumir 300-400 MB de RAM a arrancar en menos de 100 ms y quedarse en unos 50 MB. Para ciertos contextos esa diferencia lo cambia todo.

Eso sí, Spring AOT no resuelve mágicamente todo. Si tu aplicación usa librerías que hacen reflexión dinámica por su cuenta y esas librerías no tienen soporte nativo declarado, vas a tener que añadir configuración manual o usar el Tracing Agent (lo vemos más abajo).

Quarkus: nativo desde el principio

Quarkus se diseñó pensando en la compilación nativa desde el primer día, y se nota. Las extensiones de Quarkus llevan integrados los hint files de reflexión pre-generados, así que la experiencia de compilar nativo es bastante más fluida que con Spring en casos complejos.

El flujo de trabajo con Quarkus separa claramente desarrollo de producción. Para desarrollar usas el modo JVM clásico con recarga en caliente, que es rápido y cómodo. Solo compilas nativo para producción:

# Compilar nativo con GraalVM instalado en local
quarkus build --native

# Compilar nativo usando Docker (sin GraalVM local)
quarkus build --native -Dquarkus.native.container-build=true

La opción con Docker es especialmente útil en CI/CD: no necesitas que el agente de build tenga GraalVM instalado, solo Docker.

El Tracing Agent: detectar la reflexión automáticamente

Uno de los mayores dolores de cabeza al compilar nativo es descubrir, ya en producción o en pruebas, que algo falla porque Native Image no supo en compilación que cierta clase se usaría por reflexión. El Tracing Agent resuelve esto de forma elegante.

El agente se ejecuta con la JVM clásica y graba todas las llamadas a reflexión, proxies y recursos mientras la aplicación corre:

java -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image 
  -jar app.jar

Lanza la app con ese agente, ejercita todos los caminos de código que puedas (especialmente los que usan reflexión: deserialización, plugins, factories dinámicas), y el agente genera automáticamente los JSON de configuración necesarios. Luego compilas nativo con esos ficheros ya listos.

La trampa está en la cobertura: si no ejercitas un camino de código mientras el agente corre, ese camino no quedará registrado y fallará en nativo. Por eso conviene combinar el agente con una suite de tests de integración lo más completa posible.

Las limitaciones reales en 2026

El soporte de la reflexión ha mejorado mucho, pero sigue siendo el punto más delicado. Si declaras las clases correctamente en reflect-config.json (o el Tracing Agent lo hace por ti), funciona. El problema llega con librerías que usan reflexión de forma muy dinámica y que no tienen soporte nativo declarado.

El dynamic classloading directamente no está soportado. La buena noticia es que la mayoría de frameworks modernos lo han eliminado o encapsulado en sus extensiones nativas. Pero si dependes de algún mecanismo de plugins que carga clases en runtime, vas a tener un problema.

Hay otros puntos a tener en cuenta:

  • Algunos agentes de APM y librerías de instrumentación (los que hacen bytecode instrumentation) no son compatibles con binarios nativos.
  • La serialización Java clásica (Serializable) tiene soporte limitado. Lo recomendable es usar alternativas modernas como JSON o protobuf.
  • Depurar un binario nativo es más difícil que depurar en JVM. Las herramientas existen pero la experiencia no es la misma.

Cuándo tiene sentido compilar nativo

Hay contextos donde Native Image cambia la ecuación por completo:

  • Serverless (AWS Lambda, Google Cloud Functions): el cold start de una JVM clásica puede ser de 5 a 15 segundos. Con nativo bajas a menos de 100 ms. En serverless eso marca la diferencia entre una experiencia aceptable y una inaceptable.
  • Kubernetes con escala a cero (KEDA, scale-to-zero): la misma razón que serverless. Si tus pods arrancan desde cero frecuentemente, el tiempo de inicio importa mucho.
  • CLI tools en Java: distribuir un JAR de 50 MB que requiere JVM instalada es incómodo. Un binario nativo de 15 MB que arranca en 10 ms es una herramienta de verdad. Picard, Micronaut CLI o Quarkus CLI van por ahí.
  • Microservicios con alta densidad de pods y RAM limitada: si tienes 50 instancias de un servicio y cada una consume 300 MB con JVM vs 60 MB con nativo, el ahorro en infraestructura es real y se nota en la factura.

Si estás trabajando en despliegues más complejos, puedes ver cómo combinar esto con despliegue de Spring Boot en la nube con imágenes nativas.

Cuándo no merece la pena

Hay casos en los que el esfuerzo no compensa:

  • Servidores de larga vida que arrancan una vez y sirven millones de peticiones: el JIT de la JVM clásica tiene tiempo de sobra para calentarse y optimizar los hot paths. En throughput sostenido, la JVM clásica suele ganar al binario nativo.
  • Apps con mucha reflexión dinámica que no tienen soporte nativo: si dependes de Groovy scripting, algunos ORMs menos conocidos o librerías con mucha magia dinámica, el coste de configurar toda esa reflexión puede superar los beneficios.
  • Equipos que no conocen el proceso: la curva de configuración inicial es real. Si no tienes experiencia con Native Image y el proyecto no justifica el aprendizaje, puede ser más coste que ahorro a corto plazo.

Para apps con flujos duraderos y recuperación de fallos, donde el tiempo de arranque importa pero también la resiliencia, puedes ver cómo encaja esto en GraalVM y Spring Boot en producción: de microservicio a binario nativo.

Por dónde empezar

Si quieres probar Native Image sin complicarte, la forma más rápida es con un proyecto Spring Boot 3 nuevo desde start.spring.io, marcando la dependencia GraalVM Native Support, y ejecutar ./mvnw -Pnative spring-boot:build-image. Sin GraalVM instalado, solo Docker.

Para proyectos existentes, lo más útil es empezar con el Tracing Agent para entender qué reflexión usa tu app antes de intentar compilar nativo. Te da una imagen real de lo que tienes que configurar y evita las sorpresas en el momento de la compilación.

GraalVM Native Image lleva años mejorando y en 2026 la experiencia con Spring Boot y Quarkus es bastante buena. No es magia, pero tampoco es tan difícil como era hace tres años.

Imagen: Pexels / Daniil Komov

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP