Históricamente, los lenguajes siempre se habían dividido entre interpretados y compilados, en lo que se refiere a su ejecución. A día de hoy, con los compiladores JIT se crea una nueva categoría: los lenguajes que son una combinación de ambos. Los compiladores JIT son omnipresentes, por ejemplo los navegadores modernos disponen de uno para el lenguaje Javascript. También las máquinas virtuales de Java han evolucionado para incluir esta técnica, y es el caso concreto en el que se va a profundizar en este artículo.
Las implementaciones modernas de Java emplean un proceso de compilación dividido en dos pasos en el cual se produce una compilación en tiempo de ejecución, es decir, las máquinas virtuales de Java actuales disponen de un compilador JIT, just-in-time por sus siglas en inglés. En primer lugar, el compilador de Java compila en bytecode el código fuente, este bytecode es independiente de la plataforma de hardware donde se ejecuta. La máquina virtual de Java (JVM) es la encargada de ejecutar (interpretar mediante un intérprete) este bytecode. La máquina virtual sí es dependiente de la plataforma: Cada plataforma requiere de su propia máquina virtual para poder ejecutar el bytecode.
El compilador JIT de la máquina virtual es el encargado de detectar partes de este bytecode que se están ejecutando con mucha frecuencia y compilarlas al código máquina de la CPU de la plataforma para aumentar la velocidad de ejecución. El componente del compilador JIT encargado de detectar estos «puntos calientes» («hotspots» o «hot code» en inglés) del código es el profiler. Además, lo realiza en tiempo de ejecución, es decir, a medida que el programa se está ejecutando, detecta cuáles nuevas partes del código se han calentado y cuáles se han enfriado. De esta manera se supera la principal ineficiencia de los intérpretes: cuando código que se está ejecutando de forma reiterada (por ejemplo debido a un bucle while / for) debe ser interpretado una y otra vez. Además, esta característica es realmente potente, pues consigue que determinados tipos de programas se ejecuten más rápido que su equivalente en un lenguaje compilado orientado a objetos.
Para ver un caso concreto de cómo se optimiza en tiempo de ejecución un programa realizado en Java, pongamos a trabajar al ordenador: calculará 100 veces el número situado en la duodécima posición de la serie de Fibonacci, una forma como otra cualquiera de poner a calcular un ordenador. Este número es el 144, pero lo que realmente nos interesa es el tiempo transcurrido en cada una de las 100 ejecuciones.
En primer lugar, se ejecuta el programa con el compilador JIT desactivado:
$ java -Djava.compiler=NONE fibonacci.java 32715 57063 52927 62734 55639 51689 52716 33706 43873 52316 33760 33804 33869 34189 34024 33816 34075 34227 34046 34165 34035 33947 34186 34299 34045 34056 34151 34166 34357 34246 34154 33979 33983 33977 34736 34260 33801 34146 33923 34409 34164 33880 34148 34061 34231 33965 34385 34042 33939 33858 33873 34103 33957 34017 34246 34065 34034 33925 33891 34182 33876 34206 33908 34397 34147 33879 34062 34351 49945 34102 34965 33966 42351 34126 33659 33890 33997 34335 33968 34193 34015 34399 33911 34034 33690 33699 33719 33792 33970 33735 33791 33884 33847 34066 33871 33847 33679 34216 33773 33882
Cada uno de estos números representa la cantidad de nanosegundos consumidos para calcular la duodécima posición de la serie. A continuación, se ejecuta el programa normalmente, es decir, con el compilador JIT activo:
$ java fibonacci.java 43069 38093 7849 2312 1527 1957 1528 1486 1491 1981 1491 1486 1896 1476 1882 1465 1471 1491 1475 1472 6303 1523 1482 1494 1460 1967 1501 1892 1491 1918 1473 1926 1484 1491 1486 1469 1479 1501 1521 1494 1506 1524 1955 1551 1464 1875 1502 1928 1491 1475 1467 1456 1490 1468 1461 1465 1475 1466 1460 1478 1467 1473 1493 1462 1927 1508 1472 1499 1491 1505 1485 2131 1468 1474 1461 1482 1473 1467 1469 1470 1491 1490 1933 1473 1949 1498 1934 1471 1492 1486 1464 1497 1462 1460 1484 1482 1491 1477 1472 1457
Lo interesante sucede en las primeras 5 iteraciones. De la iteración inicial que consume 43069 nanosegundos se pasa a los 38093ns de la segunda. La mejoría destacable sucede en la tercera, cuarta y quinta iteración, donde el tiempo se rebaja a 7849, 2312 y 1527ns, respectivamente. Probablemente esto sea debido a la detección por parte del compilador JIT de la función fibonacci() como código caliente y su compilación. A partir de ahí, excepto en una ocasión, todas las iteraciones se mantienen por debajo de los 2000ns.
En definitiva, la incorporación del compilador de JIT a la máquina virtual de Java hace que el lenguaje ya no pueda clasificarse como estrictamente interpretado o compilado. Asimismo, se ha podido comprobar mediante un sencillo ejemplo como es capaz de mejorar sensiblemente los tiempos de ejecución. De hecho, algunos programas consiguen tiempos de ejecución en Java superiores a su equivalente en C++, un lenguaje compilado, incluso si la compilación se realiza con las opciones de optimización de nivel 2 del compilador gcc.