Saltar a contenido

1.3.-Código intermedio

1.3. Código intermedio (CE 1.d)

Idea principal

El código intermedio es una representación del programa que no depende de un procesador concreto. Una máquina virtual, como la JVM de Java o el CLR de .NET, lo ejecuta o lo traduce a código nativo para la plataforma donde se está ejecutando.

En el apartado anterior estudiamos el camino habitual en un lenguaje compilado: código fuente, código objeto y ejecutable. Sin embargo, no todos los lenguajes generan directamente un ejecutable específico para Windows, Linux, macOS, x86 o ARM. Java, Kotlin y C# pueden generar primero código intermedio, que necesita una máquina virtual o un entorno de ejecución para ponerse en marcha.

Imagina que preparas un manual en un idioma común. En vez de reescribirlo para cada país, cada oficina dispone de una persona intérprete que lo adapta a su idioma local. De forma parecida, el compilador genera una versión común del programa y la máquina virtual de cada plataforma se encarga de llevarla a las instrucciones que entiende su equipo.

El objetivo no es memorizar siglas, sino comprender qué ocurre entre el código fuente y la ejecución para poder explicar la portabilidad de una aplicación, elegir qué se debe instalar para ejecutarla y distinguir este proceso de la compilación nativa.

Según la normativa del módulo Entornos de Desarrollo, este contenido contribuye al siguiente resultado de aprendizaje y criterio de evaluación:

Código Descripción
RA 1 Reconoce los elementos y herramientas que intervienen en el desarrollo de un programa informático, analizando sus características y las fases en las que actúan hasta llegar a su puesta en funcionamiento.
CE 1.d Se han reconocido las características de la generación de código intermedio para su ejecución en máquinas virtuales.

Qué deberías saber al terminar

Al acabar este apartado deberías poder:

  • definir qué es el código intermedio y diferenciarlo del código fuente y del código máquina;
  • describir el flujo fuente → bytecode → máquina virtual → código nativo en Java;
  • relacionar bytecode, JVM y JIT con la portabilidad, la seguridad y el rendimiento;
  • explicar por qué una aplicación Java necesita un entorno de ejecución adecuado en cada plataforma.

Mapa del tema

Primero definiremos el código intermedio y el papel de la máquina virtual. Después seguiremos el proceso con Java, veremos sus ventajas y límites y lo compararemos con la generación de ejecutables nativos.

Código fuente que se transforma en código intermedio y se ejecuta mediante una máquina virtual
El código intermedio actúa como una representación común entre el programa fuente y las plataformas de destino.

1. Código intermedio y máquina virtual

El código intermedio es el resultado de traducir el código fuente a una representación que todavía no son instrucciones nativas para una CPU concreta. Por eso, el procesador no lo ejecuta directamente: necesita una máquina virtual o un entorno de ejecución que lo interprete, lo verifique y, con frecuencia, lo convierta a código nativo.

Código intermedio

Es una representación compilada del programa, independiente en gran medida de la plataforma, diseñada para que una máquina virtual o un entorno de ejecución la procese antes o durante su ejecución.

En este apartado, una máquina virtual no es una máquina virtual de sistema como VirtualBox, que puede ejecutar un sistema operativo completo. Es el software que proporciona un entorno de ejecución para un programa concreto y media entre su código intermedio, el sistema operativo y el hardware.

Los ejemplos más habituales son los siguientes:

Plataforma Código fuente Código intermedio Entorno de ejecución
Java y Kotlin .java, .kt bytecode en archivos .class o paquetes .jar JVM (Java Virtual Machine)
.NET y C# .cs CIL (Common Intermediate Language) en ensamblados .dll o .exe CLR (Common Language Runtime)

No confundas independencia con ausencia de requisitos

El mismo bytecode puede distribuirse en sistemas distintos, pero cada sistema necesita una JVM compatible. También deben ser compatibles la versión de Java, las bibliotecas y la configuración de la aplicación.

2. Del código fuente al bytecode en Java

El flujo general de una aplicación Java es el siguiente:

flowchart LR
    A["Código fuente<br/>HolaMundo.java"] -->|javac| B["Bytecode<br/>HolaMundo.class"]
    B -->|JVM de Windows, Linux o macOS| C["Instrucciones nativas"]
    C --> D["CPU ejecuta el programa"]

La persona desarrolladora escribe código fuente, por ejemplo:

public class HolaMundo {
    public static void main(String[] args) {
        System.out.println("¡Hola, mundo!");
    }
}

Al compilarlo, javac no genera normalmente un ejecutable nativo para una arquitectura concreta. Genera el archivo HolaMundo.class, que contiene bytecode:

javac HolaMundo.java
java HolaMundo

La primera orden genera el bytecode; la segunda solicita a la JVM que cargue la clase y ejecute el método main. Si ejecutas estos mismos archivos en Windows, Linux o macOS, la JVM instalada en cada plataforma se ocupa de adaptarlos al sistema y al procesador disponibles.

El contenido de un archivo .class es binario, por lo que no resulta legible al abrirlo como texto. Por ejemplo, suele comenzar con la cabecera hexadecimal CA FE BA BE. Para inspeccionar sus instrucciones de forma útil se puede usar javap:

javap -c HolaMundo

La salida muestra instrucciones de bytecode, no el código fuente original ni el lenguaje de máquina de tu procesador. Esto permite comprobar que el archivo .class es una etapa distinta de ambas.

3. Ejemplo completo: compilar y ejecutar una suma

Observa el ciclo con un programa algo más completo:

public class Suma {
    public static void main(String[] args) {
        int num1 = 5;
        int num2 = 3;
        int resultado = num1 + num2;
        System.out.println("El resultado es: " + resultado);
    }
}
  1. Guarda el código como Suma.java.
  2. Compílalo con javac Suma.java. Se generará Suma.class, que contiene bytecode.
  3. Ejecútalo con java Suma. La JVM carga el bytecode y lo ejecuta en la plataforma actual.
El resultado es: 8

Mismo paquete, plataformas distintas

Un equipo puede crear un archivo .jar en su entorno de integración continua, probarlo en un equipo con Windows y desplegar ese mismo paquete en un servidor Linux con procesador ARM. No recompila el código fuente para cada destino; instala o usa una JVM compatible en cada uno de ellos.

4. Por qué se utiliza código intermedio

El uso de código intermedio ofrece ventajas importantes en proyectos reales:

  • Portabilidad: un mismo bytecode puede ejecutarse en distintos sistemas operativos y arquitecturas cuando existe un entorno de ejecución compatible. Esta es la idea que resume el lema de Java write once, run anywhere.
  • Verificación y seguridad: antes de ejecutar el bytecode, la JVM realiza comprobaciones estructurales y de tipos. Estas verificaciones añaden una capa de protección, pero no sustituyen medidas como actualizar dependencias, controlar permisos o validar los datos de entrada.
  • Optimización durante la ejecución: una JVM moderna puede comenzar interpretando bytecode y detectar qué partes se usan más. Su compilador JIT (Just-In-Time) puede traducir esas partes frecuentes a código nativo optimizado mientras la aplicación está en marcha.

El rendimiento no es automático ni idéntico

La compilación JIT puede necesitar un periodo de warm-up: tras arrancar una aplicación, la JVM aún no ha recopilado información suficiente para optimizar las rutas más usadas. Por ello, la latencia inicial puede diferir de la observada después de varios minutos de uso.

5. Comparación con la compilación nativa

En lenguajes como C o C++, el flujo habitual genera código objeto y, tras el enlazado, un ejecutable nativo para un sistema operativo y una arquitectura determinados. Si se quiere distribuir la aplicación en otra plataforma, normalmente hay que volver a compilar y preparar un artefacto para ese destino.

Aspecto Código intermedio con JVM Ejecutable nativo típico
Resultado de la compilación Bytecode, por ejemplo .class Ejecutable específico de la plataforma
Requisito para ejecutar JVM o entorno compatible Sistema operativo, arquitectura y bibliotecas compatibles
Distribución en varias plataformas Puede reutilizar el mismo bytecode Suele requerir compilaciones o paquetes por destino
Rendimiento Puede optimizarse con JIT durante la ejecución Suele estar optimizado antes de la ejecución

No se trata de que un enfoque sea siempre mejor. El código intermedio facilita el despliegue multiplataforma y aporta servicios del entorno de ejecución; la compilación nativa puede ser preferible cuando se necesita un binario muy ajustado al sistema o cuando no se desea depender de una máquina virtual.

6. Buenas prácticas

  • Distingue los artefactos: identifica qué archivos son fuente (.java), cuáles son bytecode (.class, .jar) y cuáles pertenecen a la JVM o al sistema de despliegue.
  • Comprueba el entorno de destino: antes de desplegar, verifica la versión de Java, la arquitectura, las variables de entorno y las dependencias que necesita la aplicación.
  • Usa herramientas de inspección: ejecuta java --version, javac --version y javap -c NombreClase para comprobar qué entorno usas y qué se ha generado.
  • Mide antes de optimizar: no atribuyas una lentitud inicial a un error sin revisar el arranque, la carga de dependencias y el posible warm-up de la JVM.

7. Errores frecuentes

Error frecuente Por qué ocurre Cómo evitarlo
Pensar que un archivo .java y un .class son lo mismo. Ambos pertenecen al mismo programa, pero representan fases diferentes. Recuerda: javac transforma fuente en bytecode y la JVM ejecuta el bytecode.
Intentar ejecutar un .class directamente con el sistema operativo. El bytecode no contiene instrucciones nativas para la CPU. Ejecútalo mediante java NombreClase y una JVM compatible.
Creer que el bytecode funciona sin instalar nada en la plataforma destino. Se confunde la portabilidad del artefacto con la independencia total del entorno. Instala o proporciona el entorno de ejecución compatible y revisa las dependencias.
Confundir una JVM con una máquina virtual de sistema. Comparten el término «máquina virtual», pero tienen objetivos distintos. Diferencia un entorno de ejecución de una máquina que virtualiza un sistema operativo completo.
Suponer que JIT elimina cualquier problema de rendimiento. La optimización en tiempo de ejecución depende de la carga y del comportamiento de la aplicación. Perfila y mide la aplicación en condiciones representativas antes de tomar decisiones.

8. Resumen

En este apartado has aprendido que:

  • el código intermedio es una representación compilada que no es todavía código nativo para una CPU concreta;
  • Java y Kotlin generan bytecode, que la JVM ejecuta o traduce para cada plataforma;
  • la portabilidad requiere una máquina virtual, bibliotecas y versiones compatibles en el destino;
  • la JVM puede verificar bytecode y aplicar optimizaciones JIT durante la ejecución;
  • frente a la compilación nativa, este enfoque facilita distribuir el mismo artefacto en varios entornos.

Idea clave

Reconocer el flujo código fuente → bytecode → JVM permite explicar por qué una aplicación Java puede reutilizarse en distintas plataformas sin recompilarse, pero sigue necesitando un entorno de ejecución compatible.

9. Para seguir practicando

  • Crea HolaMundo.java, compílalo con javac y localiza el archivo .class generado.
  • Ejecuta javap -c HolaMundo e identifica que la salida contiene instrucciones de bytecode, no el código Java original.
  • Compara java --version en dos equipos o contenedores y razona qué ocurriría al ejecutar una aplicación que requiriese una versión posterior.

Bibliografía y fuentes

Presentación