Características y Herramientas de Rendimiento

La nueva Máquina Virtual JavaTM (JVM) tiene características para mejorar el rendimiento, y podemos usar un número de herramientas para incrementar el rendimiento de la aplicación o reducir el tamaño de los ficheros Class generados. Por eso las características y herramientas mejoran el rendimiento de nuestra aplicación con muy pocos o casi ningún cambio en en nuestra aplicación.

Caracterísitcas de la Máquina Virtual Java (JVM)

La plataforma Java® 2 ha presentamo muchas mejoras de rendimiento sobre versiones anteriores, incluyendo asignación más rápida de memoria, reducción del tamaño de las clases, mejorar la recolección de basura, monitores lineales y un JIT interno como estándard. Cuando usamo la nueva JVM de Java 2 nada más sacarla de la caja veremos una mejora, sin embargo para entendiendo como funciona el aumento de velocidad podemos ajustar nuestra aplicación para exprimir hasta el último bit de rendimiento.

Métodos en Línea

La versión Java 2 de la JVM automáticamente alinea métodos sencillo en el momento de la ejecución. En una JVM sin optimizar, cada vez que se llama a un método, se crea un nuevo marco de pila. La creacción de un nuevo marco de pila requiere recursos adicionales así como algún re-mapeo de la pila, el resultado final crear nuevos marcos de pila incurre en una pequeña sobrecarga.

Los métodos en línea aumenta el rendimiento reduciendo el número de llamadas a métodos que hace nuestro programa. La JVM alínea métodos que devuelven constantes o sólo acceden a campos internos. Para tomar ventaja de los métodos en línea podemos hacer una de estas dos cosas. Podemos hacer que un método aparezca atractivo para que la JVM lo ponga en línea o ponerlo manualmente en línea si no rompe nuestro modelo de objetos. La alineación manual en este contexto sólo significa poner el código de un método dentro del método que lo ha llamado.

El alineamiento automático de la JVM se ilustra con este pequeño ejemplo:

public class InlineMe {

    int counter=0;

    public void method1() {
        for(int i=0;i<1000;i++)
        addCount();
        System.out.println("counter="+counter);
    }

    public int addCount() {
        counter=counter+1;
        return counter;
    }

    public static void main(String args[]) {
        InlineMe im=new InlineMe();
        im.method1();
        }
}
En el estado actual, el método addCount no parece muy atractivo para el detector en línea de la JVM porque el método addCount devuelve un valor. Para ver si éste método está en línea compilamos el ejemplo con este perfil activado:
java -Xrunhprof:cpu=times InlineMe
Esto genera un fichero de salida java.hprof.txt. Los 10 primeros métodos se parecerán a esto:
CPU TIME (ms) BEGIN (total = 510) 
                       Thu Jan 28 16:56:15 1999
rank self accum  count trace method
 1  5.88%  5.88%    1  25 java/lang/Character.
                            <clinit>
 2  3.92%  9.80% 5808  13 java/lang/String.charAt
 3  3.92% 13.73%    1  33 sun/misc/
                            Launcher$AppClassLoader.
                            getPermissions
 4  3.92% 17.65%    3  31 sun/misc/
                            URLClassPath.getLoader
 5  1.96% 19.61%    1  39 java/net/
                            URLClassLoader.access$1
 6  1.96% 21.57% 1000  46 InlineMe.addCount
 7  1.96% 23.53%    1  21 sun/io/
                            Converters.newConverter
 8  1.96% 25.49%    1  17 sun/misc/
                            Launcher$ExtClassLoader.
                            getExtDirs
 9  1.96% 27.45%    1  49 java/util/Stack.peek
10  1.96% 29.41%    1  24 sun/misc/Launcher.<init>
Si cambiamos el método addCount para que no devuelva ningún valor, la JVM lo pondrá en línea durante la ejecución. Para amigable el código en línea reemplazamos el método addCount con esto:
public void addCount() {
        counter=counter+1;
}
Y ejecutamos el perfil de nuevo:
java -Xrunhprof:cpu=times InlineMe
Esta vez el fichero de salida java.hprof.txt debería parecer diferente. El método addCount se ha ido. Ha sido puesto en línea!
CPU TIME (ms) BEGIN (total = 560) 
                       Thu Jan 28 16:57:02 1999
rank self  accum  count trace method
 1  5.36%  5.36%    1  27 java/lang/
                            Character.<clinit>
 2  3.57%  8.93%    1  23 java/lang/
                            System.initializeSystemClass
 3  3.57% 12.50%    2  47 java/io/PrintStream.<init>
 4  3.57% 16.07% 5808  15 java/lang/String.charAt
 5  3.57% 19.64%    1  42 sun/net/www/protocol/file/
                            Handler.openConnection
 6  1.79% 21.43%    2  21 java/io/InputStreamReader.fill
 7  1.79% 23.21%    1  54 java/lang/Thread.<init>
 8  1.79% 25.00%    1  39 java/io/PrintStream.write
 9  1.79% 26.79%    1  40 java/util/jar/
                            JarFile.getJarEntry
10  1.79% 28.57%    1  38 java/lang/Class.forName0

Sincronización

Los métodos y objetos sincronizados en Java han tenido un punto de rendimiento adicional como el mecanismo utilizado para implementar el bloqueo de este código usando un registro de monitor glogal que sólo fue enhebrado en algunas áreas como la búsqueda de monitores existentes. En la versión Java 2, cada thread tiene un registro de monitor y por eso se han eliminado mucho de esos cuellos de botellas.

Si hemos usado préviamente otros mecanimos de bloqueos porque el punto de rendimiento con los métodos sincronizados merece la pena re-visitar ese código y incorporarle los bloqueos en línea de Java 2.

En el siguiente ejemplo que está creando monitores para el bloque sincronizado podemos alcanzar un 40% de aumento de velocidad. El tiempo empleado fue 14ms usando JDK 1.1.7 y sólo 10ms con Java 2 en una máquina Sun Ultra 1.

class MyLock {

  static Integer count=new Integer(5);
  int test=0;

  public void letslock() {
     synchronized(count) {
        test++;
     }
  }
}

public class LockTest {

  public static void main(String args[]) {

     MyLock ml=new MyLock();
     long time = System.currentTimeMillis();

     for(int i=0;i<5000;i++ ) {
      ml.letslock();
     }
     System.out.println("Time taken="+
      (System.currentTimeMillis()-time));
  }
}

Java Hotspot

La máquina virtual Java HotSpotTM es la siguiente generación de implementaciones de la máquina virtual de Sun Microsystem. La Java HotSpot VM se adhiere a la misma especificación que la JVM de Java 2, y ejecuta los mismos bytecodes, pero ha sido rediseñada para lanzar nuevas tecnologías como los modelos de la optimización adaptativa y de recolección de basura mejorada para mejorar dramáticamente la velocidad del JVM.

Optimización Adaptativa

El Java Hotspot no incluye un compilador interno JIT pero en su lugar compila y pone métodos en línea que parecen ser los más utilizados en la aplicación. Esto significa que en el primer paso por los bytescodes Java son interpretados como si ni tubieramos un compilador JIT. Si el código aparece como un punto caliente de nuestra aplicación el compilador Hotspot compilará los bytecodes a código nativo que es almacenado en un caché y los métodos en línea al mismo tiempo.

Una ventaja de la compilazión selectiva sobre un compilador JIT es que el compilador de bytes puede gastar más tiempo en generar alta optimización para áreas que podrían provocar la mayor optimización. el compilador también puede compiladr código que podría ejecutarse mejor en modo intérprete.

En el versiones anteriores de la Java HotSpot VM donde no era posible optimizar código que no estába actualmente en uso. El lado negativo de esto es que la aplicación estaba en una enorme bucle y el optimizador no podía compilar el código del área hasta que el bucle finalizara. Posteriores versiones de la Java Hotspot VM, usa un reemplazamiento en la pila, significando que el código puede ser compilado en código nativo incluso si está siendo utilizado por el intérprete.

Recolección de Basura Mejorada

El recolector de basura usado en el la Java HotSpot VM presenta varias mejoras sobre los recolectores de basura existentes. El primero es que el recolector se ha convertido en un recolector de basura totalmente seguro. Lo que esto significa es que el recoelcto sabe exactamente qué es una referencia y qué son sólo datos. El uso de referencias directas a objetos en el heap en una Java HotSpot VM en lugar de usar manejadores de objetos. Este incremento del conocimiento significa que la fragmentación de memoria puede reducirse con un resultado de una huella de memoria más compacta.

La segunda mejora es el uso de cópiado generacional. Java crea un gran número de objetos en la pila y frecuentemente estos objetos tenían una vida muy corta. Reemplazado los objetos creados recientemente por un cubo de memoria, esperando a que el cubo se lene y luego sólo copiando los objetos vivos restantes a una nuevo área del bloque de memoria que el cubo puede liberar en un bloque. Esto significa que la JVM no tiene que buscar un hueco para colocar cada nuevo objeto en la pila y significa que se necesita manejar secciones de memoria más pequeñas de una vez.

Para objetos viejos el recolector de basura hace un barrido a través del hepa y compacta los huecos de los objetos muertos directamente, eliminando la necesidad de una lista libre usada en algoritmos de recolección de basura anteriores.

El tercer área de mejora es eliminar la percepción de pausar en la recolección de basura escalonando la compactaciónde grandes objetos liberados en pequeños grupos y compactándolos de forma incremental.

Sincronización Rápida de Threads

La Java HotSpot VM also mejora el código de sincronización existente. Los bloques y métodos sincronizados siempren representan una sobrecarga cuando se ejecutan en una JVM. El Java HotSpot implementa los propios puntos de entrada y salida del monitor de sincroniación y no dependen del Sistema Operativo local para proporcionar esta sincronización. Este resultado es un gran aumento de la velocidad especialmente en las frecuentes aplicaciones GUI sincronizadas.

Compiladores Just-In-Time

La herramienta más sencilla para mejorar el rendimiento de nuestra aplicación el compilador Just-In-Time (JIT). Un JIT es un generador de código que convierte los bytecodes Java en código nativo de la máquina. Los programas Java invocados con un JIT generalmente se ejecutan más rápidos que cuando se ejecutan en bytecodes por el intérprete. La Java Hotspot VM elimina la necesidad de un compilador JIT en muchos casos, sin embargo podrían utilizar el compilador JIT en versiones anteriores.

El compilador JIT se puso disponible como una actualización de rendimiento en la versión Java Development Kit (JDKTM) 1.1.6 y ahora es una herramienta estándard invocada siempre qu eusamos el intérprete java en la versión de la plataforma Java 2. Podemos desactivar el uso del compilador JIT usando la opción -Djava.compiler=NONE en la JVM.

¿Cómo Funcionan los Compiladores JIT?

Los compiladores JIT se suministran como librerías nativas dependientes de la plataforma. Si xiste la librería del compilador JIT, la JVM inicializa el JNI (Java Native Interface) para llamar a las funciones JIT disponibles en la librería en lugar de su función equivalente del intérprete.

Se usa la clase java.lang.Compiler para cargar la librería nativa y empezar la inicialización dentro del compilador JIT.

Cuando la JVM llama a un método Java, usa un método llamante como especificado en el bloque método del objeto class cargado. La JVM tiene varios métodos llamantes, por ejemplo, se utiliza un llamante diferente si el método es sincronizado o si es un método nativo.

El compilador JIT usa su propio llamante. Las versión de Sun chequean el bit de aceso al método por un valor ACC_MACHINE_COMPILED para notificarle al intérprete que el código de esté método ya está compilado y almacenado en las clases cargadas.

¿Cuando el se compilado el código JIT?

Cuando se llama a un método por primera vez el compilador JIT compilad el bloque del método a código nativo y lo almacena en un bloque de código.

Una vez que el código ha sido compilado se activa el bit ACC_MACHINE_COMPILED, que es usado en la plataforma Sun.

¿Cómo puedo ver lo que está haciendo el compilador JIT?

La variable de entorno JIT_ARGS permite un control sencillo sobre el compilador JIT en Sun Solaris. Hay dos valores útiles trace y exclude(list). Para excluir los métodos del ejemplo InlineMe un mostrar un seguimiennto seleccionamos JIT_ARGS de esta forma:
Unix:
export JIT_ARGS="trace exclude(InlineMe.addCount 
                               InlineMe.method1)"

$ java InlineMe                                               
Initializing the JIT library ...
DYNAMICALLY COMPILING java/lang/System.getProperty 
                                  mb=0x63e74
DYNAMICALLY COMPILING java/util/Properties.getProperty 
                                  mb=0x6de74
DYNAMICALLY COMPILING java/util/Hashtable.get 
                                  mb=0x714ec
DYNAMICALLY COMPILING java/lang/String.hashCode 
                                  mb=0x44aec
DYNAMICALLY COMPILING java/lang/String.equals 
                                  mb=0x447f8
DYNAMICALLY COMPILING java/lang/String.valueOf 
                                  mb=0x454c4
DYNAMICALLY COMPILING java/lang/String.toString 
                                  mb=0x451d0
DYNAMICALLY COMPILING java/lang/StringBuffer.<init> 
                                  mb=0x7d690
 <<<< Inlined java/lang/String.length (4)
Observa que los métodos en línea como String.length está exentos. El metodo String.length también es un método especial y es normalmente compilado en un atajo de bytecodes interno para el intérprete java. Cuando usamos el compilador JIT estás optimizaciones proporcionadas por el intérprete Java son desactivadas para activar el compilador JIT para entender qué método está siendo llamado.

¿Cómo Aprovechar la Ventaja del Compilador JIT?

Lo primero a recordar es que el compilador JIT consigue la mayoría del aumento de velocidad la segunda vez que llama a un método. El compilador JIT compila el método completo en lugar de intérpretarlo línea por línea que también puede ser una ganancia de rendimiento cuando se ejecuta una aplicación el JIT activado. Esto significa que si el código sólo se llama una vez no veremos una ganancia de rendimiento significante. El compilador JIT también ignora los constructores de las clases por eso si es posible debemos mantener al mínimo el código en los constructores.

El compilador JIT también consigue una ganancias menores de rendimiento al no prechequear ciertas condiciones Java como punteros Null o excepciones de array fuera de límites. La única forma de que el compilador JIT conozca una excepción de puntero null es mediante una señal lanzada por el sistema operativo. Como la señal viene del sistema operativo y no de la JVM, nuestro programa mejora su rendimiento. Para asegurarnos el mejor rendimiento cuando se ejecuta una aplicación con el JIT, debemos asegurarnos de que nuestro código está muy limpio y sin errores como excepciones de punteros null o arrays fuera de límites.

Podríamos querer desactivar el compilador JIT su estámos ejecutando la JVM en modo de depuración remoto, o si queremos ver los números de líneas en vez de la etiqueta (Compiled Code) en nuestos seguimientos de pila. Para desactivar el compilador JIT, suministramos un nombre no válido o un nombre en blanco para el compilador JIT cuando invoquemos al intérprete. Los siguientes ejemplos muestran el comando javac para compilar el código fuente en bytecodes, y dos formas del comando java para invocar al intérprete sin el compilador JIT:

  javac MyClass.java
  java -Djava.compiler=NONE MyClass
o
  javac MyClass.java
  java -Djava.compiler="" MyClass

Herramientas de Terceras Partes

Hay otras herramientas disponibles incluidas aquellas que reducen el tamaño de los ficheros class generados. El fichero class Java contiene un área llamada almacen de constantes. Este almacen de constantes mantiene una lista de strings y otra información del fichero class para referencias. Unas de las piezas de información disponibles en el almacen de constantes son los nombres de los métodos y campos.

El fichero class se refiere a un campo de la clase como a una referencia a un entrada en el almacen de constantes. Esto significa que mientras las referencias permanezcan iguales no importa los valores almacenados en el almacen de constantes. Este conocimiento es explotado por varias herramientas que reescriben los nombres de los campos y de los métodos en el almacen de constantes con nombres recortardos. Esta técnica puede reducir el tamaño del fichero class en un porcentaje significante con el beneficio de que un fichero class más pequeño significa un tiempo de descarga menor.


Ozito