Yo también quiero opinar sobre si Java es lento

Mucha gente tiene la idea de que Java es lento, cosa que es lógico pensar teniendo en cuenta que la máquina virtual es una máquina encima de otra máquina (la física) y una capa intermedia seguro que supone mayor tiempo de ejecución.

También hay gente que se queja de que las aplicaciones en Java son más lentas y, sin embargo, no tienen el cuidado o el conocimiento necesario para desarrollar código Java que no sea redundante o que haga un uso óptimo de la memoria.

En la Java Magazine de este mes hay un artículo sobre cómo funciona el intérprete y el compilador JIT que compila en tiempo de ejecución Java bytecode a instrucciones del procesador en el que se están ejecutando. Ya sólo el hecho de usar un compilador JIT hace que la ejeucución de código Java sea más rápida que cualquier código ejecutado en un intérprete (como el que tiene Java).

Pero en el artículo remarcan que no sólo JIT compila a código máquina si no que se aplican muchas optimizaciones de bajo nivel y es esto lo que hace, en realidad, que el tiempo de ejecución de un programa Java pueda en ocasiones superar al tiempo de ejecución de un programa con compilación AOT. De hecho, comentan en el artículo que algunos métodos se pueden volver a compilar en diferentes fases de la aplicación para aplicar diferentes optimizaciones que dependen de la fase de ejecución.

Ahora me imagino la máquina virtual como un servicio de optimización gratuito. Compilando una aplicación C++, consigo código máquina pero no código máquina optimizado para la arquitectura en la que se ejecuta. Yo no sabría optimizarlo -desarrollo aplicaciones de propósito general-, sin embargo, el equipo de desarrollo del compilador JIT sí conoce las diferentes arquitecturas en donde se ejecuta la JVM y la hacen inteligente para aprovechar cada arquitectura al máximo.

Anuncios

Java Mission Control

A partir del JDK 7 update 40 está incluido Java Mission Control una aplicación para analizar métricas de rendimiento de aplicaciones que se ejecutan en la máquina virtual que es terriblemente fácil de usar.

Con esta aplicación probé lo que escribí en mi anterior artículo para ver hasta que punto me equivoqué. El objetivo es comprobar cuantos GCs ejecutan y la duración del conjunto en una ejecución con los parámetros -Xms1G -Xmx1G y en otra sin -Xms1G para comprobar si penaliza el redimensionamiento del heap.

El programa ejecutado consume de media por iteración (ejecuta 10 iteraciones) unos 640 MB. Es el tamaño que le paso por parámetro para que cree un array de 640 M de bytes ya que con un máximo de 1 GB para el heap un tamaño mayor que 640 MB provoca una OutOfMemoryException. El resto ocupado será memoria reservada (o virtual).

Para crear un archivo con las métricas y sólo el parámetro -Xmx1G ejecuto:

java -Xmx1G
-XX:+UnlockCommercialFeatures
-XX:+FlightRecorder
-XX:StartFlightRecording=duration=10s,
filename=record-gcing1G_NoXms1G.jfr,
settings=settings.jfc
GCing 640

Xmx1g

Flight Recording es el módulo encargado de grabar las métricas que posteriormente se analizan con Java Mission Control. Se activa con los flags –XX:+UnlockCommercialFeatures -XX:+FlightRecorder y -XX:StartFlightRecording al que se incluyen parámetros como la duración de la grabación, el nombre del archivo que creará y el nombre de la plantilla de métricas que tiene que tomar.

Esta plantilla se crea y exporta con Java Mission Control en el menú Window y la opción Flight Recording Template Manager. Este módulo por defecto no recoge las métricas específicas del GC, para que lo haga hay que indicarle una plantilla creada con JMC en la que se haya seleccionado all para Event Options GC.

Para crear el segundo archivo de métricas con parámetros -Xms1G y -Xmx1G ejecuto:

java -Xms1G -Xmx1G
-XX:+UnlockCommercialFeatures
-XX:+FlightRecorder
-XX:StartFlightRecording=duration=10s,
filename=record-gcing1G_Xms1G.jfr,
settings=settings.jfc
GCing 640

Xmxs1GXmx1G

Sin establecer un tamaño de heap inicial se ejecutan 20 procesos GC ya sean DefNew o SerialOld durando poco más de 52 milisegundos mientras que estableciendo 1G de heap inicial (porque se ha medido que la aplicación consume de media 640 MB) se ejecutan 18 procesos de GC que en total duran un poco más de 46 milisegundos. Estos datos se pueden ver en la pestaña izquierda Memory y pestañas inferiores Garbage Collections, GC Times y Allocations.

Como conclusión no se puede decir que la mejora en reducción del tiempo de ejecución sea sustancial porque casi es despreciable usando -Xms1G, pero en el JDK ahora tenemos una gran herramienta muy fácil de usar que pone en bandeja un montón de métricas que no costará nada analizar cuando se perciba un rendimiento pobre.

Enlace a zip con grabaciones, plantilla y código fuente.

Teniendo en cuenta el Garbage Collector en Java

No pensaba yo que la tarea del garbage collector tomase demasiado tiempo o pudiese interferir en la ejecución de una aplicación Java. He leído el libro Java Programming Interviews Exposed que incluye un capítulo llamado Understanding the Java Virtual Machine en el que he aprendido cosas como que el proceso de garbage collection incluye una tarea de compactación para que los objetos que se usan conjuntamente estén cerca en memoria, lo que en arquitectura de computadores se explica como uno de los principios de localidad. En esta tarea de compactación también se intenta que toda la memoria libre esté consecutiva para evitar demasiados huecos.

Previo a la tarea de compactación el GC marca las zonas que la aplicación sigue referenciando como vivas y las que ha dejado de referenciar como muertas. Durante este proceso todos los hilos ejecutándose dentro de la máquina virtual se paran para evitar inconsistencias. A esto se le llama stop-the-world.

Para evitar que el GC entre en acción demasiadas veces durante la vida de la aplicación si ésta hace uso intensivo de la memoria parece necesario configurar adecuadamente los parámetros -Xms y -Xmx:

– El parámetro -Xms es el mínimo tamaño del heap y por defecto es 1/64 de la memoria total. Si la máquina es de 4 GB = 2^32 B, por defecto el espacio mínimo es de 64 MB.

– El parámetro -Xmx es el máximo tamaño del heap y por defecto es 1/4 de la memoria total.

while (condition()) {
 process() // Creates inside objects, between 1GB and 2GB of memory size
}

Para esta aplicación en la que se realizan varias iteraciones de un proceso que utiliza un rango de memoria entre 1 y 2 GB y para la siguiente iteración esta memoria ya no es necesaria, sería conveniente ejecutar la aplicación con -Xms2G y -Xmx2G porque de entrada la máquina virtual cuenta con el espacio de memoria suficiente para no tener que solicitar más al sistema operativo y porque el GC seguramente no recolectará espacio libre hasta que no haya terminado una iteración. Si se ejecuta entre iteración e iteración se puede dar el caso de que recolecte prácticamente los 2 GB de memoria que ha usado la iteración anterior y cuyos objetos ya no son necesarios más.

No hay que olvidar que la optimización se lleva a cabo una vez que se ha notado un rendimiento que no es óptimo y que se han encontrado los cuellos de botella. Jugar a ser adivinos pronosticando dónde ocurrirán los cuello de botella nos llevará a desarrollar aplicaciones complejas y pobres en rendimiento porque los cuellos de botella que aparezcan serán diferentes a los que se han intentado evitar.