Master J2EE de Oracle: Paso 11 de 12: Optimizar y Perfilar el uso de la Memoria

Puede encontrar la versión original de este artículo en Inglés en:

http://www.oracle.com/technology/pub/articles/masterj2ee/index.html

Aprenda cómo el perfilado de memoria y la depuración de la pila le pueden ayudar a eliminar los agujeros de memoria causados por el uso del patrón listener

Un poco de Historia

J2EE permite a los desarrolladores crear grandes y complejos sistemas empresariales relátivamente rápido. Son embargo, las aplicaciones que utilizan librerías comprehensivas multicapa como las proporcionadas por J2EE son propensas a tener cuellos de botella en el rendimiento. Esto es por lo que es especialmente importante el perfilar y optimizar las complejas aplicaciones multi-thread del lado del servidor, para poder detectar y eliminar esos potenciales cuellos de botella y los problemas de recursos, incluyendo los agujeros de memoria. Afortunadamente, JDeveloper viene equipado con herramientas que permiten el perfilado de aplicaciones J2EE

Una característica clave del manejo de memoria en Java es su pila del recolector de basura. Uno de los algoritmos típicos del recolector de basura usado por la máquina virtual Java es el recolector de seguimiento, que sigue los objetos que son alcanzables por un conjunto de objetos raíz, para determinar qué objetos deberían permanecer en memoria (los objetos alcanzables sobreviven al recolector de basura porque podrían ser utilizados en la ejecución del programa). Sin embargo, si un programador deja de forma inadvertida una referencia a un objeto al que de hecho no se va a acceder, el resultado es una retención de objeto no intencionada, también conocido como agujero de memoria: el objeto que ya no va a ser utilizado por el programa, tampoco es reclamado por la JVM.

Este tipo de agujeros de memoria es un patrón de error muy común, (debido al bug "lapsed listener") que normalmente ocurre por una mala aplicación del patrón listener. Este patrón permiten que las clases se notifiquen eventos unas a otras, Es una clase específica del patrón Observer/Observable, que consta de un subject y un observer. Normalmente se utilizan oyentes para manejadores de eventos específicos que ocurren sobre el objeto. Algunos de los más utilizados son:

  • Eventos GUI que son normalmente manejados por clases oyentes dedicadas que implementan el interface java.util.EventListener para eventos GUI en AWT, Swing u otras librerías GUI.
  • Eventos de conexiones a bases de datos que son típicamente manejados por oyentes que implementan el interface javax.sql.ConnectionEventListener.

Aunque este patrón se ha utilizado muy ampliamente, es propenso a errores y podría causar agujeros de memoria si no se utiliza apropiadamente. Demostraremos el agujero que podría ocurrir mostrando cómo se implementan los oyentes de forma interna.

Consideremos una situación en la que hemos registrado un oyente para esperar eventos que puedan ocurrir en un aparato (vea la Figura 1). Una tabla de oyentes, alcanzable desde el conjunto raíz (la raíz es la referencia a un objeto vivo, el conjunto raíz es un conjunto de todos los objetos alcanzables para los que existen referencias), consiste en una lista enlazada de todos los posibles oyentes; una tabla de eventos globales apunta a la lista de oyentes. Cuando ocurre un evento en el aparato, los despachadores de eventos pasan por la tabla de oyentes e invocan el manejador de eventos de los oyentes apropiados. La tabla de oyentes tiene una referencia del oyente para poder entregarle un evento cuando llegue; esta referencia se muestra como un flecha punteada en la Figura 1.

Sin embargo, si se preserva esa referencia, el objeto nunca será recolectado, porque es alcanzable desde el conjunto raíz. En muchos casos, el objeto podría consumir muchos recursos del sistema. Podría ser un enorme objeto GUI que inicializa recursos nativos del sistema o unos recursos limitados como conexiones a bases de datos. Aunque los oyentes normalmente son objetos pequeños y podrían no representar un problema inmediato, los objetos grandes también podrían permanecer alcanzables (y por lo tanto no son recolectados) y pueden afectar al rendimiento y provocar caídas del sistema debido al impacto que tienen en la memoria.

En la práctica, el registro de un oyente incluye el añadir una referencia del oyente al objeto. Este tipo de registro se consigue con una llamada como ésta:

object.addMyListener(new MyListener(...));

Es importante eliminar esta referencia con su correspondiente llamada a

object.removeMyListener(l);

Esto desregistra el oyente y permite que el objeto object sea recolectado por el recolector de basura.


Figura 1: Diseño de un sistema típico basado en eventos.

Una típica aplicación de gran escala con un GUI y una base de datos podría utilizar docenas de tipos diferentes de oyentes, todos los cuales deben desregistrarse para evitar los agujeros de memoria.

Perfilado de Memoria

Aunque los oyentes parados podrían no ser un problema en programas de corta duración, en aplicaciónes de larga ejecución, agujerear la memoria continuamente durante un largo periodo de tiempo puede acabar con una memoria exhausta. La característica Memory profiling de JDeveloper 10g le permite recolectar estadísticas de memoria en programas de larga ejecución.

Para ilustrar el uso del perfilado de memoria para buscar potenciales agujeros de memoria, hemos creado ListenerTest un programa de prueba que crea parejas de objetos/oyentes en un bucle que se repite 3000 veces. La estructura del programa corresponde con el de la Figura 1: los oyentes son accesibles desde la tabla de oyentes global y todos los oyentes tienen una referencia al objeto correspondiente. Al final de cada iteración, hay una llamada a System.gc() para inicializar una vuelta del recolector de basura.

Al Perfilador de memoria de Oracle JDeveloper se puede acceder desde Run | Memory profile... Puede configurar la frecuencia de toma de datos bajo Tools|Project Properties|Profiles|Development|Profiler|Memory. En este caso hemos seleccionado un intervalo de 0.8 segundos resultando en 9 tomas de datos al final de la ejecución.


Figura 2: Salida del Perfilador de Memoria mientras se ejecuta ListenerTest.

La vista del perfilador de memoria muestra estadísticas de las asignaciones y de las recolecciones de basura en cada toma de datos. Una forma rápida de ver los posibles agujeros de memoria es ordenar por el campo "Diff Alloc", la diferencia entre el número de objetos asignandos de cada tipo entre la toma de datos actual y la anterior. Los números más grandes en la columna Diff Alloc así como en la columna Diff sz indican objetos que han sido asignados pero no eliminados, esto nos señala los posibles agujeros de memoria.

Para afinar más el problema, también puede detener el proceso del perfilado de memoria y hacer doble-click en la clase que le interese para ver los lugares asignados para los objetos del tipo en cuestión. Cuando está disponible el código fuente, puede hacer doble-click en una posición de asignación para saltar directamente el código fuente.

Consejos...

Lo que debe y no debe hacer para evitar agujeros de memoria

  • No asuma que el recolector de basura desasignará por usted toda la memoria no utilizada.
  • Realice un perfilado de memoria en los programas de larga duración para encontrar los agujeros de memoria.
  • Cuando utilice el patrón Listener, asegúrese de que las llamadas de registro/desregistro de los oyentes se corresponden en cada camino de ejecución.

Depurado de la Pila

Conocer cuales son los objetos que potencialmente pueden provocar agujeros de memoria, no es siempre suficiente. Para averiguar la fuente del agujero, debe preguntarle al recolector de basura porqué se está dejando objetos.

Para responder a esta cuestión, podemos utilizar las capacidades de depuración de la pila de Oracle JDeveloper 10g's. Una vez que hemos determinado qué clases pueden potencialmente provocar los agujeros de memoria mediante el perfilado, puede obtener información más detallada usando la ventana Heap del depurador de JDeveloper. En LapsedListenerTest, la clase que parece ser la que provoca los agujeros de memmoria en el perfilador de memoria es la clase interna ListenerTest$Component.

Hemos ejecutado el programa en modo depuración, hemos pulsado con el botón derecho en la ventana Heap y hemos seleccionado Add class folder para mostrar todos los ejemplares de la clase en la que estamos interesados para que esté disponibles en tiempo de ejecución. Como muestra la Figura 3, hay 7 ejemplares de esta clase. Primero miramos los paths de referencia para un objeto ListenerTest$Component alojado en 0X4B7B4DE4. Luego pulsamos con el botón derecho en la primera de las dos referencias que corresponden a un campo estático y seleccionamos Expand reference path, lo que expandirá la vista de árbol para mostrar el objeto en cuestión. Como se esperaba, el objeto en cuestión ListenerTest$Component, es alcanzable desde la raíz a través de la tabla de oyentes en 0X48755AEC y luego en uno de los oyentes en 0X4875C1B4.


Figura 3: Usar la ventana Heap en JDeveloper para aislar los agujeros de memoria.

Una vez que ha localizado los agujeros de memoria, tiene una gran variedad de formas para solucionarlos. Una aproximación es usar WeakReferences, que no son atravesados por el recolector de basura cuando determinan qué objetos son alcanzables. Así, si hacemos la referencia desde la lista de oyentes al oyente el objeto del tipo ListenerTest$Component enventualmente no será alcanzable y será recolectado:

public static class Component {

    public void addListener(ComponentListener listener){      
        synchronized (listeners){      	
            listeners.add(new WeakReference(listener));
		}
	}
}
		

Sin embargo, en un sistema grande, seguir la pista de los oyentes normalmente se hace detrás de la escena, por eso la única forma que tienen los desarrolladores de evitar los agujeros es llamar al método remove_xxx de los paths de programa que registran oyentes, para hacer que el objeto que está siendo escuchado sea eventualmente recolectado.

Conclusión

Como el recolector de basura de Java podría no solucionar todos los problemas de memoria y puede dejar agujeros que pueden terminar llenando la memoria, Oracle JDeveloper viene totalmente equipado con herramientas para corregir todos los tipos de problemas de memoria que pueden ocurrir. En este artículo hemos visto lo efectivos que son el perfilador de memoria y el depurador de pila de JDeveloper para localizar los errores de memoria.

Próximos Pasos

Próximos pasos sobre Optimización y Perfilado

  1. Descargue Oracle JDeveloper 10g (10.1.3) Developer Preview
  2. Ayuda Online (en Inglés):Learn more about Oracle JDeveloper Profilers
  3. Track down memory leaks Using JDeveloper's Debugger

Información Adicional (en Inglés):

  • Online Help: Learn more about the powerful Oracle JDeveloper Debugger
  • Learn how to debug a multithreaded java application, using the convenient conditional breakpoints
  • Oracle JDeveloper Debugger Tips
  • Remote Debugging with Oracle JDeveloper

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP