Referencias en Java I

En Java existe más de una forma de “apuntar” a un objeto usando una referencia. Hay de hecho cuatro tipos distintos de referencias. Esto se debe a lo extraño que resulta a veces la forma en que trabaja el Garbage Collector. A pesar de contar con variedad de formas de referenciar a un objeto, normalmente se utiliza una sola.

Nota: Este tema es bastante largo y tedioso incluso. Está dividido en varias entregas con el fin de salvaguardar la integridad mental de los posibles lectores ante una indigestión por lectura violenta.

Motivación.

El Garbage Collector es un ser extraño. Repasemos por un segundo como funciona este particular habitante de la JVM. Cuando se inicia un programa en Java y se comienzan a crear objetos aparece un conjunto de referencias denominado root set donde se encuentran las referencias declaradas dentro del metodo main(), o run() si estamos trabajando con varios threads. Luego durante la ejecución del programa aparecerán otras referencias pero como atributos de algún objeto cuya referencia este en el root set. En la imagén, que tomé del excelente Hardcore Java de Robert Simmons Jr, se ve como en un metodo main() se crean tres referencias A, C y H a objetos a partir de las cuales se forma el grafo (fig.1).

Si cambiaramos en A la referencia que posee a B haciendola apuntar a null (fig.2) el Garbage Collector no eliminaria el objeto B puesto que aún existe un camino a traves de C->F->X->D->J->E->B. De esa forma funciona el Garbage Collector, elimina los objetos que no pueden ser accedidos a traves de un camino que se inicie en el root set. Esto es particularmente util para evitar las “islands of isolations” cuando hay un grupo de objetos con referencias entre si pero que no pueden ser accedidos por el thread principal.

Este comportamiento, en un principio benigno, es la principal causa de memory leaks en las aplicaciones cuando tenemos referencias circulares entre dos objetos o más objetos. Supongamos que el grafo representa una aplicación con interfaz gráfica donde A es la GUI y C el objeto a partir del cual se arma la lógica de negocios. El objeto A posee muchas referencias a otros objetos (panels, botones, textfields) y C a todas las clases del negocio. Ademas, como en todas las aplicaciones con GUI, se unen ambos mundos (Vista y Modelo) con algun mecanismo de Listeners que implemente el patrón observer. ¿Que pasa sí, como en el ejemplo anterior, el objeto B es una ventana a la que dejamos de referenciar desde A porque no queremos utilizarla más? Evidentemente aún queda un camino a traves del cual se puede acceder a este objeto, como vimos más arriba, por lo que la ventanita no es elegible para ser eliminada por el garbage collector. Tenemos una ventana en memoria (con todos sus componentes) que no queremos usar más pero sigue apuntada por un listener. Tenemos un Memory Leak.

Los cuatro tipos de referencias.

Java nos provee cuatro tipos de referencias distintos para ayudarnos a evitar problemas como el visto aguas arriba sin ensuciar el codigo con aventuradas técnicas de manejo de Listeners. Estos cuatro tipos de referencia son: Fuerte, Débil, Blanda y Fantasma.

  • Referencia Fuerte Las referencias a objetos que se utilizan en java comunmente son referencias fuertes (ó strong references). Su comportamiento es por demas conocido por todos. Apuntan a un objeto y lo mantienen en memoria mientras esta referencia se mantenga. Cuandro creamos un String texto = “algo” estamos creando una referencia fuerte apuntando a un objeto de tipo String.
  • Referencia Débil Una referencia debil (weak reference) es una referencia común y corriente pero con una salvedad: Si un objeto es apuntado solo por referencias debiles, sera elegible por el garbage colector para ser eliminado. Es decir, en el momento de elegir que objetos seran eliminados el GC ignora las referencias debiles haciendo como si no existieran.En el grafo original se reemplazan las referencias fuertes circulares, potencialmente peligrosas, por referencias debiles (indicadas en la figura con una flecha doble).


¿Qué sucede si ahora dejamos de apuntar a B desde A? ¡Sorpresa! B es elegible para ser eliminado.

  • Referencia Blanda Las referencias blandas (soft reference) se comportan como las referencias debiles pero el garbage collector va a eliminar los objetos unicamente si tiene poco espacio en el Heap. Digamos que si un objeto esta siendo apuntado unicamente por referencias blandas el garbage collector puede elegirlo para eliminación si tiene poca memoria en el heap, sino no necesariamente va a eliminarlo. Este comportamiento sin embargo, como pasa con todo lo relacionado al garbage collector, es dependiente de la implementación y en la práctica las referencias blandas son tratadas como referencias debiles por la Java Virtual Machine.
  • Referencia Fantasma El tipo más enigmático de todos y su uso es, por lo menos, tan bizarro como su nombre. La referencia fantasma (phantom reference) apunta a un objeto cuando este ya ha sido eliminado por el GC. Si, una vez que el objeto pasa por el GC la referencia fantasma apunta a él. Sin embargo no puede usarse para acceder a ningún miembro de este objeto, sino solo para saber que estuvo ahí en algun momento. ¿Extraño verdad? Su uso más común es contar cuantos objetos han sido eliminados y en técnicas de profilling.

Con estos cuatro tipos de referencias podemos lograr que nuestros programas hagan un uso más racional de la memoria pero como aplicarlos queda para mas adelante. Antes de terminar esta primera entrega es menester recordar que estos no son structurals patterns o algo parecido sino que java nos ofrece una API estandar para implementarlos programaticamente.

[Patrones] Observer

El patrón Observer es tan simple y elegante como efectivo. Es utilizado cuando uno o más objetos (Observers) necesitan saber sobre los cambios de estados u ocurrencia de eventos producidos en un objeto Subject. La implementación más corriente se da en la arquitectura Model-View-Controller donde la vista necesita saber cuando actualizarse debido a cambios en el modelo. Observer elimina las ineficientes “esperas activas” usando un mecanismo de callback o inversión de control.

El modelo consiste en una clase Subject con referencias a todos los Observers, objetos interesados en el subject, y un metodo notifyObservers() que invoca un metodo notify() implementado por cada uno de los Observers. De esta forma cuando el Subject sufre un cambio de estado o ante un evento avisa a los Observers y mediante el callback de una funcion notify que implementa cada Observer estos reaccionan al evento.

Java proporciona una clase java.util.Observable con el comportamiento de la clase Subject y una interface java.util.Observer que describe un método update() con la logica a ejecutar ante un cambio de estado. La forma de uso deberia explicarse sola: Tenemos un objeto Perro que cumple años y un Observador que avisa que el perro cumplio años :)

class Perro extends Observable {
    private int edad;
    public void cumplirAños() {
        this.edad++;
        this.setChanged(); // se marca el objeto indicando que ha cambiado su estado.
        this.notifyObservers(); // avisa a sus observadores sobre el cambio
                              // de estado del objeto o del evento ocurrido
   }
}

El observador implementa el metodo update() que se ejecutará cuando el Perro notifique de su cambio de estado. El Perro cuenta, ademas, con un metodo addObserver(Observer o) que inserta un Observer dentro de los objetos a los que notificar los cambios o eventos.

class Observador implements Observer {
    public void update(Observable perro, Object args) {
         System.out.println("El perro cumplio años!");
    }
}

En lugar de Perros que cumplen años y Observadores que hacen regalos el escenario tipico para utilizar este patrón se presenta cuando el Observador es un elemento de una GUI (un JTextField por ejemplo) que debe actualizarse de acuerdo a los cambios que sufre el modelo de datos.

Esto es Lazy Loading!

Lazy Loading es una técnica que implica no cargar un componente hasta que es usado. Esta técnica evita inicializar todo el grafo de dependencias de un objeto distribuyendo la creacion de las dependencias a medida que se necesitan. Si bien puede implementarse de muchas formas (la más usada es con un Virtual Proxy) un simple proof-of-concept es el siguiente:

class Ventana {
    private Widget componente;
    public Widget getComponente() {
         if(this.componente==null) {
              this.componente = new Widget();
         }
        return this.componente;
    }
}

El componente es creado la primera vez que se usa en lugar de inicializarse dentro del constructor de la clase. Si miramos detenidamente el código vemos que no es Thread-safe ya que más de un hilo podria volver a inicializar el componente provocando efectos impredecibles. Por ejemplo con la siguiente secuencia:

Thread 1: llamado a getComponente()
Thread 1: if( this.componente == null ) <- Componente es null entonces entra en el bloque del if

Thread 2: llamado a getComponente()
Thread 2: if( this.componente == null) <- Componente es null entonces entra en el bloque del if
Thread 2: this.componente = new Widget();
Thread 2: this.componente.value = “Soy un componente perezoso”;

Thread 1: this.componente = new Widget();
OMFG!

Tampoco es negocio sincronizar el metodo getComponente() porque lo que ganamos implementando Lazy Loading lo perdemos en las sucesivas llamadas por el costo que tiene invocar un metodo sincronizado. Una solución es usar un mecanismo llamado Double Check modificando el código anterior de esta manera.

class Ventana {
   private Widget componente;
   public Widget getComponente() {
         if(this.componente==null) {
               synchronized(this) {
               if(this.component==null) {
                     this.componente = new Widget();
               }
            }
          }
        return this.componente;
    }
}

Lo que parece una redundancia grande como una casa en realidad es un hack maravilloso, con double check la condición se evalua dos veces: una vez sin sincronizar y otra vez sincronizada. Al repetir este proceso se gana en velocidad ya que en el peor de los casos solo se ejecuta el bloque sincronizado tantas veces como threads esten utilizando el objeto en lugar de ejecutarlo tras cada llamada a getComponente. Hay que tener en cuenta que esta tecnica, si bien es un hack maravilloso, puede tener problemas con las optimizaciones que hace la JVM Hotspot, más especificamente con el reordenamiento de instrucciones que hace el compilador.

Lazy Loading en realidad abarca más conceptos, como la carga perezosa de clases, y lo que se vio recien es conocido como Lazy Instantiation. Más alla de eso me parecio bueno comentar un poco sobre el tema tras el cambio de nombre que sufrio este blog ;)

Ya soy SCJP

 Espero que me sirva más que la tarjeta de YPF o el carnet del Club

Entradas y comentarios feeds. 13 queries. 0.251 seconds.

60920 pages viewed, 15 today
29269 visits, 10 today
FireStats icon Powered by FireStats