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 nativoen 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.
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:
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:
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);
}
}
- Guarda el código como
Suma.java. - Compílalo con
javac Suma.java. Se generaráSuma.class, que contiene bytecode. - Ejecútalo con
java Suma. La JVM carga el bytecode y lo ejecuta en la plataforma actual.
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 --versionyjavap -c NombreClasepara 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 conjavacy localiza el archivo.classgenerado. - Ejecuta
javap -c HolaMundoe identifica que la salida contiene instrucciones de bytecode, no el código Java original. - Compara
java --versionen dos equipos o contenedores y razona qué ocurriría al ejecutar una aplicación que requiriese una versión posterior.
Bibliografía y fuentes¶
- Oracle, The Java Virtual Machine Specification: https://docs.oracle.com/javase/specs/jvms/se21/html/.
- Oracle, documentación de la herramienta
javap: https://docs.oracle.com/en/java/javase/21/docs/specs/man/javap.html. - Microsoft, Common Language Infrastructure (CLI): https://learn.microsoft.com/en-us/dotnet/standard/components.