Zona HTML Zona Java Zona PHP Zona ASP Zona Bases de datos
Inicio > Tutoriales > Lenguajes orientados a objeto > Java > J2EE > Escribir Aplicaciones Avanzadas para la Plataforma Java 2
-Tutoriales

Escribir Aplicaciones Avanzadas para la Plataforma Java 2


Técnicas de Mejora del Rendimiento

Uno de los mayores retos al desarrollar grandes aplicaciones Java es hacer que la aplicación alcance sus criterios de rendimiento. Este capítulo muestra cómo aumentar el rendimiento de la aplicación.

. Mejorar el Rendimiento por Diseño

Las restricciones del ancho de banda en las redes alrededor del mundo hacen de las operaciones basadas en red potenciales cuellos de botella que pueden tener un importante impacto en el rendimiento de las aplicaciones. Muchas aplicaciones de red están disañadas para usar almacenes de conexiones y por ello pueden reutilizar conexiones de red existentes y ahorrar el tiempo y la sobrecarga que conllevan el abrir y cerrar conexiones de red.

Junto con el almacen de conexiones, hay otras características que podemos diseñar dentro de nuestros programas para mejorar el rendimiento. Este capítulo explica cómo podemos diseñar un applet para que descargue ficheros y recursos de forma más eficiente, o diseñar un programa basado en threads para usar un almacen de threads para ahorrarnos el costoso proceso de arrancar threads.

. Mejorar la Velocidad de Descarga de un Applet

El rendimiento de descarga de un applet se refiere al tiempo que tarda el navegador en descargar todos los ficheros y recursos que necesita para arrancar el applet. Un factor importante que afecta al rendimiento de la descarga del applet es el número de veces que tiene que solicitar datos al servidor. Podemos reducir el número de peticiones empaquetando las imagenes del applet en un fichero class, o usando un archivo JAR.

. Empaquetar Imágenes en un Clase

Normalmente, si un applet tiene seis imágenes de botones se traducen en seis solicitudes adicionales al servidor para cargar esos ficheros de imágenes. Seis solicitudes adicionales podrían no parecer demasiadas en una red interna, pero en las conexiones de baja velocidad y eficiencia, esas solicitudes adicionales pueden tener un impacto muy negativo en el rendimiento. Por eso, nuestro último objetivo será cargar el applet tan rápido como sea posible.

Una forma de almacenar imágenes en un fichero class es usar un esquema de codificación ASCII como X-PixMap (XPM). De esta forma, en vez de mantener la imágenes en ficheros GIF en el servidor, los ficheros son codificados como un Strings y son almacenados en un sólo fichero class.

Este código de ejemplo usa páquetes del ganador de la JavaCup del JavaOne 1996, que contenía las clases XImageSource y XpmParser. Estas clases proporciona todo los necesario para leer un fichero XPM. Podemos ver esto ficheros en SunSite.

Para el proceso inicial de codificación, hay un número de herramientas gráficas que podemos usar para crear fichero XPM. En Solaris podemos usar ImageTool o una variedad de otros GNU image packages. Podemos ir a la web site Download.com para obtener software de codificación para las plataformas Windows.

El siguiente código extraido del ejemplo de código MyApplet que carga imágenes. Podemos ver el String codificado en la definición XPM de imágenes.

La clase Toolkit crea un objeto Image para cada imagen desde el objeto fuente XPM Image.

Toolkit kit = Toolkit.getDefaultToolkit();
  Image image;
  image = kit.createImage (new XImageSource (_reply));
  image = kit.createImage (new XImageSource (_post));
  image = kit.createImage (new XImageSource (_reload));
  image = kit.createImage (new XImageSource (_catchup));
  image = kit.createImage (new XImageSource (_back10));
  image = kit.createImage (new XImageSource (_reset));
  image = kit.createImage (new XImageSource (_faq));

La alternativa técnica de abajo usa ficheros GIF. Requiere una petición al servidor para cada imagen cargada.

Image image;
  image = getImage ("reply.gif");
  image = getImage ("post.gif");
  image = getImage ("reload.gif");
  image = getImage ("catchup.gif");
  image = getImage ("back10.gif");
  image = getImage ("reset.gif");
  image = getImage ("faq.gif");

Esta técnica reduce el trafico de la red porque todas las imágenes están disponibles en un sólo fichero class.

  • Usar imágenes XPM codificadas hace más grande el fichero de la clase, pero el número de peticiones de red es menor.
  • Al hacer que las definiciones de imágenes XPM formen parte del fichero class, hacemos que el proceso de carga de imágenes sea parte de la carga normal del fichero class del applet sin clases extras.

Una vez cargado, podemos usar las imágenes para crear botones u otros componentes del interface de usuario. El siguiente segmento de código muestra cómo usar la imágenes con la clase javax.swing.JButton.

ImageIcon icon = new ImageIcon (
                   kit.createImage (
		     new XImageSource (_reply))); 
JButton button = new JButton (icon, "Reply");

. Usar Ficheros JAR

Cuando un applet consta de más de un fichero, podemos mejorar el rendimiento de la descarga con ficheros JAR. Un fichero JAR contiene todos los ficheros del applet en un sólo fichero más rápido de dsacargar. Mucha parte del tiempo ahorrado viene de la reducción del número de conexiones HTTP que el navegador tiene que hacer.

El capítulo: Desarrollar Nuestra Aplicación tiene información sobre cómo crear y firmar ficheros JAR.

El código HTML de abajo usa la etiqueta CODE para especificar el ejecutable del applet MyApplet, y la etiqueta ARCHIVE especifica el fichero JAR que contiene todos los ficheros relacionados con MyApplet. El ejecutable especificado por la etiqueta CODE algunas veces es llamado code base.

Por razones de seguridas los ficheros JAR listados por el parámetro archive deben estar en el mismo directorio o subdirectorio que el codebase del applet. Si no se suministra el parámetro codebase el directorio de donde se cargó el applet se usa como el codebase.

El siguiente ejemplo especifica jarfile como el fichero JAR que contiene todos los ficheros relacionados para el ejecutable MyApplet.class.

<APPLET CODE="MyApplet.class" ARCHIVE="jarfile"
	WIDTH="100" HEIGHT="200">
</APPLET>

Si la descarga del applet usa múltiples ficheros JAR como se muestra en el siguiente segmento HTML, el ClassLoader carga cada fichero JAR cuando el applet arranca. Por eso, si nuestro applet usa algunos ficheros de recursos de forma infrecuente, el fichero JAR que contiene esos ficheros es descargado sin importar si los recursos van a ser usados durante la sesión o no.

<APPLET CODE="MyApplet.class" ARCHIVE="jarfile1, jarfile2"
        WIDTH="100" HEIGHT="200">
</APPLET>

Para mejorar el rendimiento cuando se descargan fichero no usados de forma frecuente, ponemos los ficheros usados más frecuentemente dentro de un fichero JAR y los ficheros menos usados en el directorio de la clase del applet. Los ficheros usados poco frecuentemente son localizados y descargados sólo cuando el navegador los necesita.

. Almacen de Threads

El servidor de applets Java Developer Connection (JDC) y el Java Web Server hacen un uso extensivo del almacen de threads para mejorar el rendimiento. El almacen de threads es crear un suministro de threads durmientes al principio de la ejecución. Como el proceso de arranque de un thread es muy caro en términos de recursos del sistema, el almacen de threads hace el proceso de arrancada un poco más lento, pero aumenta el rendimiento en tiempo de ejecución porque los threads durmientes (o suspendidos) sólo se despiertan cuando cuando son necesarios para realizar nuevas tareas.

Este código de ejemplo tomado de la clase Pool.java muestra una forma de implementar la fusión de threads, En el constructor de la fusión (mostrado abajo), se inicializan y arrancan los WorkerThreads. La llamada al método start ejecuta el método run del WorkerThread, y la llamada a wait suspende el Thread mientras el Thread espera a que llegue un trabajo. La última línea del constructor empuja el Thread durmiente hacia la pila.

public Pool (int max, Class workerClass) 
                         throws Exception {

    _max = max;
    _waiting = new Stack();
    _workerClass = workerClass;
    Worker worker;
    WorkerThread w;
    for ( int i = 0; i < _max; i++ ) {
        worker = (Worker)_workerClass.newInstance();
        w = new WorkerThread ("Worker#"+i, worker);
        w.start();
        _waiting.push (w);
    }
}

Junto al método run, la clase WorkerThread tiene un método wake. Cuando viene el trabajo, se llama al método wake, que asigna los datos y notifica al WorkerThread durmiente (el inicializado por el Pool) para recuperar la ejecución. El método wake llama a notify hace que el WorkerThread bloqueado salga del estado de espera, y se ejecuta el método run de la clase HttpServerWorker. Una vez realizado el trabajo, el WorkerThread se pone de nuevo en el Stack (asumiento que el Pool de threads no está lleno) o termina.

synchronized void wake (Object data) {
  	_data = data; 
	notify();
  }

  synchronized public void run(){
    boolean stop = false; 
    while (!stop){ 
	if ( _data == null ){ 
	  try{
	     wait();
	  }catch (InterruptedException e){ 
		e.printStackTrace();
		continue;
	  }
	}

	if ( _data != null ){
	  _worker.run(_data);
	}

	_data = null;
	stop = !(_push (this));
    }
  }

En este alto nivel, el trabajo entrante es manejado por el método performWork en la clase Pool. Cuando viene el trabajo, se saca de la pila un WorkerThread existente (o se crea uno nuevo si el Pool está vacío). El WorkerThread durmiente es activado mendiate una llamada a su método wake.

public void performWork (Object data) 
               throws InstantiationException{
  WorkerThread w = null;
  synchronized (_waiting){
     if ( _waiting.empty() ){
	try{
	  w = new WorkerThread ("additional worker",
	  (Worker)_workerClass.newInstance());
	  w.start();
	}catch (Exception e){
	  throw new InstantiationException (
	              "Problem creating 
	              instance of Worker.class: " 
	              + e.getMessage());
	}
      }else{
	w = (WorkerThread)_waiting.pop();
      }
  }
  w.wake (data);
}

El constructor de la clase HttpServer.java crea un nuevo ejemplar Pool para servir ejemplares de la clase HttpServerWorker. Los ejemplares HttpServerWorker se crean y almacenan como parte de los datos WorkerThread. Cuando se activa un WorkerThread mediante una llamada a su método wake, el ejemplar HttpServerWorker es invocado mediante su método run.

try{
    _pool = new Pool (poolSize, 
                  HttpServerWorker.class);
  }catch (Exception e){
    e.printStackTrace();
    throw new InternalError (e.getMessage());
  }

Este código está en el método run de la clase HttpServer.java. Cada vec que viene una petición, el dato es inicializado y el Thread empieza el trabajo.

Nota: Si creamos un nuevo Hashtable por cada WorkerThread provocamos demasiada sobrecarga, sólo modificamos el código para que no use la abstración Worker.

try{
	Socket s = _serverSocket.accept();
	Hashtable data = new Hashtable();
	data.put ("Socket", s); 
	data.put ("HttpServer", this);
	_pool.performWork (data); 
  }catch (Exception e){ 
	e.printStackTrace();
  }

El almacen de threads es una técnica efectiva de ajuste de rendimiento que coloca el caro proceso de arranque de threads en la arrancada de la aplicación. De esta forma, el impacto negativo en el rendimiento ocurre sólo una vez durante el arrancada del programa donde se nota menos.

. Almacen de Conexiones

Si hemos usado SQL u otra herramienta similar para conectarnos con una base de datos y actúar sobre los datos, probablemente habremos notado que la obteneción de la conexión y el login es la parte que tarda más tiempo. Una aplicación puede fácilmente tardar varios segundos cada vez que necesita establecer una conexión.

El varsiones anteriores a JDBC 2.0 cada sesión de base de datos requería una nueva conexión y un login incluso si la conexión anterior usaba la misma tabla y cuenta de usuario. Si estámos usando versioens anteriores al JDBC 2.0 y queremos mejorar el rendimiento, podemos cachear las conexiones JDBC.

Las conexiones cacheadas se mantienen un objeto pool en tiempo de ejecución y pueden ser utilizadas y reutilizadas cuando las necesite la aplicación. Una forma de implementar un objeto pool es hacer una una simple hashtable de objetos conection. Sin embargo, una forma más sencilla de hacerlo es escribir un driver JDBC envuelto que es un intermediario entre la aplicación y la base de datos.

La envoltura trabaja particulamente en los Beans de Enterprise que san persistencia manejada por el Bean por dos razones: 1) Sólo se carga una clase Driver por cada Bean, y 2) los detalles específicos de la conexión se manejan fuera del Bea.

Esta sección explica cómo escribir una clase Driver JDBC envuelta.

. Clases Wrapper

El Driver JDBC envuelto creado para estos ejemplos consta de las siguientes clases:

  • JDCConnectionDriver
  • JDCConnectionPool
  • JDCConnection

. Driver de Conexión

La clase JDCConnectionDriver.java implementa el interface java.sql.Driver, que proporciona método para cargar drivers y crear nuevas conexiones a bases de datos.

Un objeto JDCConnectionManager es creado por una aplicación que pretende una conexión con una base de datos. La aplicación proprociona el ULR para la base de datos, el ID del usuario y la password.

El constructor JDCConnectionManager hace esto.

  • Registra el objeto JDCConnectionManager con DriverManager.
  • Carga la clase Driver pasada al constructor por el programa llamante.
  • Inicializa un objeto JDCConnectionPool para las conexiones con la URL de la base de datos, el ID y el password del usuario pasados al constructor por el programa llamante.
public JDCConnectionDriver(String driver, 
                String url, 
                String user, 
                String password) 
        throws  ClassNotFoundException,
                InstantiationException, 
                IllegalAccessException, 
                SQLException {

  DriverManager.registerDriver(this);
  Class.forName(driver).newInstance();
  pool = new JDCConnectionPool(url, user, password);
}

Cuando el programa llamante necesita una conexión con la base de datos, llama al método JDCConnectionDriver.connect, que a su vez, llama al método JDCConnectionPool.getConnection.

. Almacen de Conexiones

La clase JDCConnectionPool.java tiene conexiones disponibles para el programa llamando en su método getConnection. Este método busca una conexión disponible en el almacen de conexiones. Si no hay ninguna disponible, crea una nueva conexión. Si hay una conexión disponible en el almacen, el método getConnection alquila la conexión y la devuelve al programa llamante.

public synchronized Connection getConnection() 
	throws SQLException {

  JDCConnection c;
  for(int i = 0; i < connections.size(); i++) {
     c = (JDCConnection)connections.elementAt(i);
     if (c.lease()) {
        return c;
     }
  }

  Connection conn = DriverManager.getConnection(
			url, user, password);
  c = new JDCConnection(conn, this);
  c.lease();
  connections.addElement(c);
  return c;
}

La clase JDCConnection.java representa una conexión JDBC en el almacen de conexiones, y esencialmente es una envoltura alrededor de un conexión real JDBC. El objeto JDCConnection mantiene una bandera de estado para indicar si la conexión está en uso y el momento en que la conexión se sacó del almacen. Este tiempo es usado por la clase ConnectionReaper.java para identificar las conexiones colgadas.

. Bloqueos y Cuelgues

Mientras que muchos clientes y servidores de bases de datos tiene formas de manejar los bloqueos y los cuelgues y no tenemos que preocuparnos de escribir código para manejar estas situaciones, muchos de los nuevos modelos de base de datos ligeros distribuidos no están tan bien equipados. La clase conection pool proporciona una cosechador de conexiones muerta para manejar dichas situacciones.

La clase ConnectionReaper decide que una clase está muerta cuando se cumplen las siguientes condiciones.

  • La conexión está marcada como que está en uso.
  • La conexión es más vieja que tiempo de timeout preseleccionado.
  • La conexión falla en un chequeo de validación.

El chequeo de validación ejecuta una simple consulta SQL sobre la conexión para ver si lanza una excepción. En este ejemplo, el método de validación solicita una descripción de alto nivel de las tablas de la base de datos. Si una conexión falla el test de validación, se cierra, se inicia una nueva conexión con la base de datos y se añade al almacen de conexiones.

public boolean validate() {
  try {
     conn.getMetaData();
  }catch (Exception e) {
     return false;
  }
  return true;
}

. Cerrar Conexiones

La conexiónes devuelta al almacen de conexiones cuando el programa llamante llama al método JDCConnection.close en su claúsulafinally.

public void close() throws SQLException {
  pool.returnConnection(this);
}

. Aplicación de Ejemplo

Usamos un almacen de conexiones en una aplicación de forma similar a como usaríamos cualquiere otro driver JDBC. Aquí está el código de un RegistrationBean controlado por el Bean. Este RegistrationBean se ha adaptado desde la casa de subastas de JavaBeans enterprise descrito en los coaítulo 1 -3.

Cuando se crea el primer objeto RegistrationBean, crea un ejemplar estático de la clase JDCConnectionDriver. Este objeto driver estático se registra a sí mismo con el DriverManager en el constructor JDCConnectionDriver poniendo disponibles la solicitudes de conexiones para todos los objetos RegistrationBean creados por la aplicación cliente.

Pasar la URL como jdbc:jdc:jdcpool en el método getConnection permite que el DriverManager corresponda la getConnection solicitada al driver registrado. El DriverManager usa un sencillo String para encontrar un driver disponible que pueda manejar URLs en ese formato.

public class RegistrationBean implements EntityBean{

  private transient EntityContext ctx;
  public String theuser, password; 
  public String	creditcard, emailaddress;
  public double balance;

//Static class instantiation
  static {
        try{
        new pool.JDCConnectionDriver(
		"COM.cloudscape.core.JDBCDriver", 
		"jdbc:cloudscape:ejbdemo",
		"none", "none");
        }catch(Exception e){}
  }

  public Connection getConnection() 
                      throws SQLException{
        return DriverManager.getConnection(
				"jdbc:jdc:jdcpool");
  }
}

. Características y Herramientas de Rendimiento

La nueva Máquina Virtual Java (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 HotSpot 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 (JDK) 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 es el 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.

. Análisis de Rendimiento

Otra forma de aumentar el rendimiento es con ánalisis de rendimiento. Los análisis de rendimientos buscan las ejecución del programa apuntar donde podrían estar los cuellos de botella y otros problemas de rendimiento como los picos de memoria. Una vez que sables donde están los puntos de problemas potenciales podemos cambiar nuestro código para eliminar o reducir su impacto.

. Perfiles

Las Máquinas Vituales Java (JVMs) han tenido la habilidad de proporcionar sencillos informes de perfiles desde Java Development Kit (JDK) 1.0.2. Sin embargo, la información que ellos proporcionaban estaban limitadas a una lista de los métodos que un programa había llamado.

La plataforma Java® 2 proporciona muchas más capacidades de perfilado que las anteriormente disponibles y el análisis de estos datos generado se ha hecho más fácil por la emergencia de un "Heap Analysis Tool" (HAT). Esta herramienta, como implica su nombre, nos permite analizar los informes de perfiles del heap. El heap es un bloque de memoria que la JVM usa cuando se está ejecutando. La herramienta de análisis de heap nos permite generar informes de objetos que fueron usado al ejecutar nuestra aplicación. No sólo podemos obtener un listado de los métodos llamados más frecuentemente y la memoria usada en llamar a esos métodos, pero también podemos seguir los picos de memeoria. Los picos de memoria pueden tener un significante impacto en el rendimiento.

. Analizar un Programa

Para analizar el programa TableExample3 incluido en el directorio demo/jfc/Table de la plataforma Java 2, necesitamos generar un informe de perfil. El informa más sencillo de generar es un perfil de texto. Para generarlo, ejecutamos la aplicación el parámetro -Xhprof. En la versión final de la plataforma Java 2, esta opción fue renombrada como -Xrunhprof. Para ver una lista de opciones actualmente disponibles ejecutamos el comando.

java -Xrunhprof:help
Hprof usage: -Xrunhprof[:help]|[<option>=<value>, ...]
Nombre de Opción y Valor Descripción Por Defecto
-------------------------- --------------- ---------------
heap=dump|sites|all heap profiling all
cpu=samples|times|old CPU usage off
monitor=y|n monitor contention n
format=a|b ascii or binary output a
file=<file> write data to file java.hprof(.txt for ascii)
net=<host>:<port> send data over a socket write to file
depth=<size> stack trace depth 4
cutoff=<value> output cutoff point 0.0001
lineno=y|n line number in traces y
thread=y|n thread in traces? n
doe=y|n dump on exit? y
Example: java -Xrunhprof:cpu=samples,file=log.txt,
                               depth=3 FooClass

La siguiente invocación crea una fichero de texto que podemos ver sin la herramienta de análisis de heap llamado java.hprof.txt cuando el programa genera un seguimiento de pila o sale. Se utiliza una invocación diferente para crear un fichero binario para usarlo con la herramienta de análisis de heap.

  java -Xrunhprof TableExample3

  d:\jdk12\demo\jfc\Table> java -Xrunhprof TableExample3
  Dumping Java heap ... allocation sites ... done.

La opción de perfil literalmente hace un diario de cada objeto creado en el heap, por incluso cuando arrancamos y paramos el pequeño progeama TableExample3 resulta un ficheo de informe de cuatro megabytes. Aunque la herramienta de análisis de heap usa una versión binaria de este fichero y proporciona un sumario, hay algunas cosas rápidas y fáciles que podemos aprender desde el fichero de texto sin usar la herramienta de análisis de heap.

Nota: Para listar todas las opciones disponibles, usamos

java -Xrunhprof:help

. Ver el Fichero de Texto

Elegimos un fichero que pueda manejar grandes ficheros y vamos hasta el final del fichero. Podría haber cientos o miles de líneas, por eso un atajo es buscar las palabras SITES BEGIN. Veríamos una lista de línea que empezarían un tango creciente de números seguido por dos números de porcentaje. La primera entrada en la lista sería similar a este ejemplo.

SITES BEGIN (ordered by live bytes) 
                  Sun Dec 20 16:33:28 1998

 

percent

 

live

 

alloc'ed

 

stack class
rank self accum bytes objs bytes objs trace name
1 55.86% 55.86% 826516 5 826516 5 3981 [S

La notación [S al final de la última línea indica que es la primera entrada de un array de short, un tipo primitivo. Esto es lo esperado con aplicaciones (AWT). Los primeros cinco contadores bajo la cabecera objs significa que actualmente hay cinco de esos arrays, y sólo ha habido cinco durante el tiempo de vida de esta aplicación, y han ocupado 826516 bytes. La referencia clase de este objeto es el valor listado bajp stack trace. Para encontrar donde se creo esté objeto en este ejmplo, buscamos TRACE 3981. Veremos esto.

TRACE 3981.
java/awt/image/DataBufferUShort.<init>(
                           DataBufferUShort.java:50)
java/awt/image/Raster.createPackedRaster(
                                    Raster.java:400)
java/awt/image/DirectColorModel.
      createCompatibleWritableRaster(
		             DirectColorModel.java:641)
sun/awt/windows/WComponentPeer.createImage(
                             WComponentPeer.java:186)

El código TableExample3 selecciona un scrollpane de 700 por 300. Cuando miramos el fuente de Raster.java, qu está en el fichero src.jar, encontraremos estas sentencias en la línea 400.

  case DataBuffer.TYPE_USHORT.
         d = new DataBufferUShort(w*h);
         break;

Los valores w y h son la anchura y altura de la llamada a createImage que arranca en TRACE 3981. El constructor DataBufferUShort crea un array de shorts.

  data = new short[size];

donde size es w*h. Por eso, en teoría debería hacer una entrada en el array para 210000 elementos. Buscamos una enteada por cada ejemplarización de esta clase buscando por trace=3981. Una de las cinco entradas se parecerá a esto.

  OBJ 5ca1fc0 (sz=28, trace=3979, 
	class=java/awt/image/DataBufferUShort@9a2570)
   data 5ca1670
   bankdata 5ca1f90
   offsets 5ca1340
   ARR 5ca1340 (sz=4, trace=3980, nelems=1, 
                                elem type=int)
   ARR 5ca1670 (sz=420004, trace=3981, nelems=210000, 
                                elem type=short)
   ARR 5ca1f90 (sz=12, trace=3982, nelems=1, 
                                elem type=[S@9a2d90)
   [0] 5ca1670

Podemos ver que los valores de los datos de estas referencias de imagen en un array 5ca1670 que devuelve un alista de 210000 elementos short de tamaño 2. Esto significa qu este array usa 420004 bytes de memoria.

De este dato podemos concluir que el programa TableExample3 usa cerca de 0.5Mb para mapear cada tabal. Si la aplicación de ejemplo se ejecuta en una máquina con poca memoria, debemos asegurarnos de que no mantenemos referencias a objetos geandes o a imágenes que fueron construidas con el método createImage.

. La Herramienta de Análisis de Heap

Esta herramienta puede analizar los mismos datos que nosotros, pero requere un fichero de informe binario como entrada. Podemos generar un fichero de informa binario de esta forma.

  java -Xrunhprof:file=TableExample3.hprof,format=b 
                                       TableExample3

Para generar el informe binario, cerramos la ventana TableExample3. El fichero de informe binario TableExample3.hprof se crea al salir del programa. La Herramienta de Análisis de Heap arranca un servidor HTTP que analiza el fichero de perfil binario y muestra el resultado en un HTML que podemos ver en un navegador.

Podemos obtener una copia de la Herramienta de Análisis de Heap de la site java.sun.com. Una vez instalado, ejecutamos los scripts shell y batch en el directorio bin instalado para poder ejecutar el servidor de la Herramienta de Análisis de Heap de esta forma.

  >hat TableExample3.hprof
  Started HCODEP server on port 7000
  Reading from /tmp/TableExample3.hprof...
  Dump file created Tue Jan 05 13:28:59 PST 1999
  Snapshot read, resolving...
  Resolving 17854 objects...
  Chasing references, 
            expect 35 dots.......................
  Eliminating duplicate 
                references.........................
  Snapshot resolved.
  Server is ready.

La salida de arriba nos dice que nuestro servidor HTTP se ha arrancado en el puerto 7000. Para ver este informe introducimos la URL http://localhost:7000 o http://your_machine_name:7000 en nuestro navegador Web. Si tenemos problema en arrancar el servidor usando el script, podemos alternativamente ejecutar la aplicación incluyendo el fichero de clases hat.zip en nuestro CLASSPATH y usar el siguiente comando.

  java hat.Main TableExample3.hprof

La vista del informe por defecto contiene una lista de todas las clases. En la parte de abajo de está página inicial están las dos opciones básicas de informes.

  Show all members of the rootset
  Show instance counts for all classes

Si seleccionamos el enlace Show all members of the rootset, veremos un alista de las siguientes referencias porque estas referencias apuntan a picos potenciales de memoria.

 Java Static References
 Busy Monitor References
 JNI Global References
 JNI Local References
 System Class References

Lo que vemos aquí son ejemplares en la aplicación que tienen referencias a objetos que tienen un riesgo de no se recolectados para la basura. Esto puede ocurrir algunas veces en el caso del JNI su se asigna memoria para un objeto, la memoria se deja para que la libere el recolector de basura, y el recolector de basura no teine la información que necesita para hacerlo. En esta lista de referencias, estamos principalmente interesados en un gran número de referencias a objetos o a objetos de gran tamaño.

El otro informe clave es el Show instance counts for all classes. Este lista los números de llamadas a un método particular. Los objetos array String y Character, [S y [C, están siempre en la parte superior de esta lista, pero algunos objetos son un poco más intrigantes. ¿Por qué hay 323 ejemplares de java.util.SimpleTimeZone, por ejemplo?

  5109 instances of class java.lang.String
  5095 instances of class [C
  2210 instances of class java.util.Hashtable$Entry
  968 instances of class java.lang.Class
  407 instances of class [Ljava.lang.String;
  323 instances of class java.util.SimpleTimeZone
  305 instances of class 
        sun.java2d.loops.GraphicsPrimitiveProxy
  304 instances of class java.util.HashMap$Entry
  269 instances of class [I
  182 instances of class [Ljava.util.Hashtable$Entry;
  170 instances of class java.util.Hashtable
  138 instances of class java.util.jar.Attributes$Name
  131 instances of class java.util.HashMap
  131 instances of class [Ljava.util.HashMap$Entry;
  130 instances of class [Ljava.lang.Object;
  105 instances of class java.util.jar.Attributes

Para obtener más información sobre los ejemplares SimpleTimeZone, pulsamos sobre el enlace (la línea que empieza por 323). Esto listará las 323 referencias y calculará cuánta memoria ha sido utilizada. en este ejemplo, se han utilizado 21964 bytes.

  Instances of java.util.SimpleTimeZone

  class java.util.SimpleTimeZone

  java.util.SimpleTimeZone@0x004f48c0 (68 bytes)
  java.util.SimpleTimeZone@0x003d5ad8 (68 bytes)
  java.util.SimpleTimeZone@0x004fae88 (68 bytes)
  .....
 Total of 323 instances occupying 21964 bytes.

Si pulsamos sobre uno de estos ejemplares SimpleTimeZone, veremos donde fue asignado este objeto.

  Object allocated from.

  java.util.TimeZoneData.<clinit>(()V) : 
        TimeZone.java line 1222
  java.util.TimeZone.getTimeZone((Ljava/lang/String;)
	Ljava/util/TimeZone;) : 
	TimeZone.java line (compiled method)
  java.util.TimeZone.getDefault(
        ()Ljava/util/TimeZone;) : 
	TimeZone.java line (compiled method)
  java.text.SimpleDateFormat.initialize(
        (Ljava/util/Locale;)V) : 
	SimpleDateFormat.java line (compiled method)

En este ejemplo el objeto fue asignado desde TimeZone.java. El fichero fuente de este fichero están el fichero estándard src.jar, y examinando este fichero, podemos ver que de hehco hay cerca de 300 de estos objetos en memoria.

  static SimpleTimeZone zones[] = {
   // The following data is current as of 1998.
   // Total Unix zones: 343
   // Total Java zones: 289
   // Not all Unix zones become Java zones due to 
   // duplication and overlap.
   //-------------------------------------------
   new SimpleTimeZone(-11*ONE_HOUR, 
                  "Pacific/Niue" /*NUT*/),

Desafortunadamente, no tenemos control sobre la memoria usada en este ejemplo, porque es asignada cuando el programa hizo la primera solicitud al timezone por defecto. Sin embargo, esta misma técnica puede aplicarse para analizar nuestra propia aplicación donde probablemente podríamos hacer algunas mejoras.

. ¿Dónde Gasta el Tiempo la Aplicació?

De nuevo, podemos usar el parámetro -Xrunhprof para obtener información sobre el tiempo que gasta la aplicación procesando un método particular.

Podemos usar una o dos opciones de perfil de CPU para conseguir esto. La primera opción es cpu=samples. Esta opción devuelve el resultado de un muestreo de ejecución de threads de la Máquina Virtual Java con un conteo estadístico de la frecuencia de ocurrencia con que se usa un método particular para encontrar secciones ocupadas de la aplicación. La segunda opción es cpu=times, que mide el tiempo que tardan los métodos individuales y genera un ranking del porcentaje total del tiempo de CPU ocupado por la aplicación.

Usando la opción cpu=times, deberíamos ver algo como esto al final del fichero de salida.

CPU TIME (ms) BEGIN (total = 11080) 
                       Fri Jan  8 16:40:59 1999
rank   self   accum   count  trace   method
 1   13.81%  13.81%       1   437   sun/
    awt/X11GraphicsEnvironment.initDisplay
 2    2.35%  16.16%       4   456   java/
    lang/ClassLoader$NativeLibrary.load
 3    0.99%  17.15%      46   401   java/
    lang/ClassLoader.findBootstrapClass

Si constrastamos esto con la salida de cpu=samples, veremos la diferencia entre la frecuencia de ejecuciónde un método durante la ejecución de la aplicación comparada con el tiempo que tarda ese método.

CPU SAMPLES BEGIN (total = 14520) 
                   Sat Jan 09 17:14:47 1999
rank  self   accum   count  trace   method
 1    2.93%  2.93%   425    2532    sun/
    awt/windows/WGraphics.W32LockViewResources
 2    1.63%  4.56%   237     763    sun/
    awt/windows/WToolkit.eventLoop
 3    1.35%  5.91%   196    1347    java/
    text/DecimalFormat.<init>

El método W32LockView, que llama a una rutina de bloqueo de ventana nativa, se llama 425 veces. Por eso cuando aparecen en los threads activos porque también toman tiempo para completarse. En contraste, el método initDisplay sólo se le llama una vez, pero es el método que tarda más tiempo en completarse en tiempo real.

. Herramientas de Rendimiento de Sistema Operativo

Algunas veces los cuellos de botella del rendimiento ocurren al nivel del sistema operativo. Esto es porque la JVM depende en muchas operacioens de las librerías del sistema operativo para funcionalidades como el acceso a disco o el trabajo en red. Sin embargo, lo que ocurre después de que la JVM haya llamado a estas librerías va más alla de las herramientas de perfilado de la plataforma Java.

Aquí hay una lista de herramietnas que podemos usar para analizar problemas de rendimiento en algunos sistemas operativos más comunies.

. Plataforma Solaris

System Accounting Reports, sar, informa de la actividad del sistema en términos de I/O de disco, actividad del programa de usuario, y actividad a nivel del sistema. Si nuestra aplicación usa una cantidad de memoria excesiva, podría requerir espacio de intercambio en disco, por lo que veriamos grandes porcentajes en la columna WIO. Los programas de usuario que se quedan en un bucle ocupado muestran un alto porcentaje en la columna user.

developer$ sar 1 10

SunOS developer 5.6 Generic_105181-09 sun4u    
                                      02/05/99

11:20:29    %usr    %sys    %wio   %idle
11:20:30      30       6       9      55
11:20:31      27       0       3      70
11:20:32      25       1       1      73
11:20:33      25       1       0      74
11:20:34      27       0       1      72

El comando truss sigue y guarda los detalles de cada llamada al sistema por la JVM al kernel Solaris. Un forma común de usar truss es.

   truss -f -o /tmp/output -p <process id>

El parámetro -f sigue cualquier proceso hijo que haya creado, el parámetro -o escribe la salida en el fichero nombrado, y el parámetro -p sigue un programa en ejecución desde sis ID de proceso. De forma alternativa podemos reemplazar -p <process id> con la JVM, por ejemplo.

   truss -f -o /tmp/output java MyDaemon

El /tmp/output es usado para almacenar la salida de truss, lo que se debería parecer a esto.

15573:  execve("/usr/local/java/jdk1.2/solaris/
                bin/java", 0xEFFFF2DC,
	        0xEFFFF2E8)  argc                   = 4
15573:  open("/dev/zero", O_RDONLY)                 = 3
15573:  mmap(0x00000000, 8192, 
             PROT_READ|PROT_WRITE|PROT_EXEC,
	     MAP_PRIVATE, 3, 0) = 0xEF7C0000
15573:  open("/home/calvin/java/native4/libsocket.so.1", 
              O_RDONLY) Err#2 ENOENT
15573:  open("/usr/lib/libsocket.so.1", 
              O_RDONLY)                             = 4
15573:  fstat(4, 0xEFFFEF6C)                        = 0
15573:  mmap(0x00000000, 8192, PROT_READ|PROT_EXEC, 
              MAP_SHARED, 4, 0) = 0xEF7B00 00
15573:  mmap(0x00000000, 122880, PROT_READ|PROT_EXEC, 
              MAP_PRIVATE, 4, 0) = 0xEF7 80000
15573:  munmap(0xEF78E000, 57344)                   = 0
15573:  mmap(0xEF79C000, 5393, 
              PROT_READ|PROT_WRITE|PROT_EXEC,
              MAP_PRIVATE|MAP_FIXED, 4, 49152) 
              = 0xEF79C000
15573:  close(4)                                    = 0

En la salida de truss, buscamos los ficheros que fallaran al abrirlos debido a problemas de acceso, como un error ENOPERM, o un error de fichero desaparecido ENOENT. También podemos seguir los datos leidos o escrito con los parámetros de truss: -rall para seguir todos los datos leídos, o -wall para seguir todos los datos escritos por el programa. Con estos parámetros, es posible analizar datos enviados a través de la red o a un disco local.

. Plataforma Linux

Linux tiene un comando trace llamado strace. Sigue las llamadas del sistema al kernel Linux. Este ejemplo sigue el ejemplo SpreadSheet del directorio demo del JDK.

$ strace -f -o /tmp/output 
                      java sun.applet.AppletViewer 
                      example1.html
$ cat /tmp/output

639   execve("/root/java/jdk117_v1at/java/
               jdk117_v1a/bin/java", ["java",
               "sun.applet.AppletViewer ", 
               "example1.html"], [/* 21 vars */]) = 0
639   brk(0)                              = 0x809355c
639   open("/etc/ld.so.preload", O_RDONLY)       = -1 
        ENOENT (No such file or directory)
639   open("/etc/ld.so.cache", O_RDONLY)          = 4
639   fstat(4, {st_mode=0, st_size=0, ...})       = 0
639   mmap(0, 14773, PROT_READ, MAP_PRIVATE, 
             4, 0) = 0x4000b000
639   close(4)                                    = 0
639   open("/lib/libtermcap.so.2", O_RDONLY)      = 4
639   mmap(0, 4096, PROT_READ, MAP_PRIVATE, 
             4, 0) = 0x4000f000

Para obtener información del sistema similar al comando sar de Solaris, lee los contenidos del fichero /proc/stat. El formato de este fichero se describe en las páginas del manual proc. Miramos la línea cpu para obtener la hora del sistema de usuario.

   cpu  4827 4 1636 168329

En el ejemplo anterior, la salida cpu indica 48.27 segundos de espacio de usuario, 0.04 de prioridad máxima, 16.36 segundos procesando llamadas al sistema, y 168 segundos libre. Esta es una ejecución total, las entradas para cada proceso están disponibles en /proc/<process_id>/stat.

. Plataforma Windows95/98/NT

No hay herramientas de análisis de rendimiento estándard incluidas en estas plataformas, pero si hay herramientas de seguimiento disponibles mediante recursos freeware o shareware como http://www.download.com .

Análisis de memoria: Memory meter

Análisis de Red: Traceplus

. Caché en Aplicaciones Cliente/Servidor

El caché es una de las primera técnicas usadas para aumetnar el rendimiento de navegadores y servidores web. El caché del navegador hace innecesarios los bloqueos de red porque una copia reciente del fichero se mantiene en el caché local, y el caché del servidor reduce el coste de la carga de ficheros desde disco para cada petición. Esta sección explica cómo podes usar el caché de forma similar para mejorar el rendimiento en muchas aplicaciones cliente/servidor escritas en lenguaje Java.

El API java.util.Collections disponible en el SDK Java® 2 hace sencilla la implementación del caché. Este API proporciona la clase HashMap, que funciona bien para cachear un objeto, y la clase LinkedList, que funciona bien en combinaciones con la clase HashMap para cachear muchos objetos.

. Caché de un Objeto

Un objeto HashMap almacena objetos en una pareja clave valor. cuando ponemos un datp en un HashMap, le asignamos una clave y luego usamos esa clave para recuperar el dato.

Un objeto HashMap es muy similar a un Hashtable y puede ser usado para mantener una copia temporal de resultados generados préviamente. Los objetos mantenidos en el caché HashMap podría, por ejemplo, ser una lista de subastas completadas.

En este caso, los resultados de una consulta JDBC podrían solicitarse cientos de veces en un segundo por personas que están esperando conocer la puja más alta, pero la lista de resultados completa sólo cambia una vez por minuto cuando se ompleta una subasta. Podemos escribir nuestro programa para recuperar los objetos que no han cambiado desde el caché de resultados en vez de solicitar a la base de datos cada vez y obtener un significante aumento de rendimiento.

Este ejemplo de código ejecuta una consulta a la base de datos por cada minuto, y devuelve copias cacheadas para las solicitudes que vienen entre consultas.

import java.util.*;
import java.io.*;

class DBCacheRecord {
  Object data;
  long time;

  public DBCacheRecord(Object results, long when) {
    time=when;
    data=results;
  }
  public Object getResults() {
	return data;
  }
  public long getLastModified() {
	return time;
  }
}

public class DBCache {
  Map cache;

  public DBCache() {
    cache = new HashMap();
  }

  public Object getDBData(String dbcommand) {
    if(!cache.containsKey(dbcommand)) {
	synchronized(cache) {	
	  cache.put(dbcommand, readDBData(dbcommand)); 
        }
     } else {
       if((new Date().getTime() ) -
         ((DBCacheRecord)cache.get(
		dbcommand)).getLastModified()>=1000) {
	 synchronized(cache) {	
	   cache.put(dbcommand, readDBData(dbcommand)); 
         }
       }	
     }
     return ((DBCacheRecord)cache.get(
	dbcommand)).getResults();
  }

  public Object readDBData(String dbcommand) {

/*Insert your JDBC code here For Example.
  ResultSet results=stmt.executeQuery(dbcommand);
*/
    String results="example results";
    return(new DBCacheRecord(results,new 
			     Date().getTime()));
	
  }

  public static void main(String args[]) {
    DBCache d1=new DBCache();
    for(int i=1;i<=20;i++) {
	d1.getDBData(
	  "select count(*) from results where 
	  TO_DATE(results.completed) <=SYSDATE");
    }
  }
}

. Cache de Muchos Objetos

Algunas veces queremos cachear más de un objeto. Por ejemplo, podríamos querer mantener los ficheros accedidos más recientemente en el caché de un servidor web. Si usamos un objeto HashMap para un propósito como este, continuará creciendo y usando mucha memoria.

Si nuestra máquina tiene una gran cantidad de memoria y sólo un pequeño número de objetos que cachear entonces un creciente HashMap podría no ser un problema. Sin embargo, si estamos intentar cachear muchos objetos entonces podríamos queres sólo mantener los objetos más recientes en el caché proporcionando el mejor uso de la mémoria de la máquina. Podemos combinar un objeto HashMap con un LinkedList para crear un caché llamado "Most Recently Used" (MRU).

Con un caché MRU, podemos situar una restricción sobre los objetos que permanecen en el caché, y por lo tanto, control sobre el tamaño del caché. Hay tres operaciones principales que puede realizar un caché MRU.

  • Si el caché no está lleno, los nuevos objetos que no están en el caché se insertan en la parte superior de la lista.
  • Si el caché no está lleno y el objeto a inserta ya está en el caché, se mueve a la parte superior del caché.
  • Si el caché está lleno y se inserta un nuevo objeto, el último objeto del caché es eliminado y el nuevo objeto se pone en la parte superior de la lista.

Este diagrama muestra cómo trabajan juntos LinkedList y HashMap para implementar las operaciones descritas arriba.

Caché MRU con LinkedList y HashMap

El LinkedList proporciona el mecanismo de cola, y las entradas de la LinkedList contienen la clave de los datos en el HashMap. Para añadir una nueva entrada en la parte superior de la lista, se llama al método addFirst.

  • Si la lista ya está llena, se llama al método removeLast y a entrada de datos también se elimina del HashMap.
  • Si una entrada ya existe en la lista, se elimina con una llamada al método remove y se inserta en la parte superior de la lista con una llamada al método addFirst.

El API Collectios no implementa bloqueos, por eso si eliminados o añadimos entradas a objetos LinkedList o HashMap, necesitamos bloquear los accesos a esos objetos. También podemos usar un Vector o ArrayList para obtener el mismo resultado mostrado en el códido de abajo del LinkedList.

Este ejemplo de código usa un caché MRU para mantener un caché de ficheros cargados desde disco. Cuando se solicita un fichero, el programa chequea para ver si el fichero está en el caché. Si el fichero no está en el caché, el programa lee el fichero desde el disco y sitúa una copia en el caché al principio de la lista.

Si el fichero está en el caché, el programa compara la fecha de modificación del fichero y la entrada del caché.

  • Si la entrada del caché es más vieja, el programa lee el fichero del disco, elimina la copia del caché, y sitúa una nueva copia en el caché en la parte superior del LinkedList.
  • Si el fichero es más viejo que el caché, el programa obtiene el fichero del caché y mueve la copia del caché a la parte superior de la lista.
import java.util.*;
import java.io.*;

class myFile {
  long lastmodified;
  String contents;

  public myFile(long last, String data) {
    lastmodified=last;
    contents=data;		
  }
  public long getLastModified() {
    return lastmodified;
  }
  public String getContents() {
    return contents;
  }
}

public class MRUCache {

  Map cache;
  LinkedList mrulist;
  int cachesize;

  public MRUCache(int max) {
    cache = new HashMap();
    mrulist= new LinkedList();
    cachesize=max;
  }

  public String getFile(String fname) {
    if(!cache.containsKey(fname)) {
      synchronized(cache) {	
        if(mrulist.size() >=cachesize) {
	  cache.remove(mrulist.getLast());
	  mrulist.removeLast();
        }
        cache.put(fname, readFile(fname)); 
        mrulist.addFirst(fname);
      }
    } else {
      if((new File(fname).lastModified())> 
	((myFile)cache.get(fname)).getLastModified()) {
	  synchronized(cache) {	
	    cache.put(fname, readFile(fname)); 
          }
       }	
       synchronized(cache) {
	 mrulist.remove(fname);
	 mrulist.addFirst(fname);
       }
    }
       return ((myFile)cache.get(fname)).getContents();
  }
	
  public myFile readFile(String name) {
    File f = new File(name);
    StringBuffer filecontents= new StringBuffer();

    try {
      BufferedReader br=new BufferedReader(
                              new FileReader(f));
      String line;

      while((line =br.readLine()) != null) {
	filecontents.append(line);
      }	
    } catch (FileNotFoundException fnfe){
      return (null);
    } catch ( IOException ioe) {
		return (null);
    }
      return (new myFile(f.lastModified(), 
		filecontents.toString()));
  }

  public void printList() {
    for(int i=0;i<mrulist.size();i++) {
      System.out.println("item "+i+"="+mrulist.get(i));
    }
  }

  public static void main(String args[]) {

    // Number of entries in MRU cache is set to 10 
    MRUCache h1=new MRUCache(10);
    for(int i=1;i<=20;i++) {
      // files are stored in a subdirectory called data
      h1.getFile("data"+File.separatorChar+i);
    }
      h1.printList();
  }
}
 
Patrocinados
 

Copyright © 1999-2006 Programación en castellano. Todos los derechos reservados.
Formulario de Contacto - Datos legales - Publicidad

Hospedaje web y servidores dedicados linux por Ferca Network