Guía de WeakHashMap en Java

1. Información general

En este artículo, veremos un WeakHashMap del paquete java.util .

Para comprender la estructura de datos, la usaremos aquí para implementar una implementación de caché simple. Sin embargo, tenga en cuenta que esto está destinado a comprender cómo funciona el mapa, y crear su propia implementación de caché casi siempre es una mala idea.

En pocas palabras, WeakHashMap es una implementación basada en tablas hash de la interfaz Map , con claves que son de tipo WeakReference .

Una entrada en un WeakHashMap se eliminará automáticamente cuando su clave ya no se use normalmente , lo que significa que no hay una única referencia que apunte a esa clave. Cuando el proceso de recolección de basura (GC) descarta una clave, su entrada se elimina efectivamente del mapa, por lo que esta clase se comporta de manera algo diferente a otras implementaciones de Map .

2. Referencias fuertes, suaves y débiles

Para comprender cómo funciona WeakHashMap , debemos mirar una clase WeakReference , que es la construcción básica para las claves en la implementación de WeakHashMap . En Java, tenemos tres tipos principales de referencias, que explicaremos en las siguientes secciones.

2.1. Referencias fuertes

La referencia fuerte es el tipo de referencia más común que usamos en nuestra programación diaria:

Integer prime = 1;

La variable primo tiene una fuerte referencia a un objeto Integer con valor 1. Cualquier objeto que tenga una fuerte referencia apuntando a él no es elegible para GC.

2.2. Referencias suaves

En pocas palabras, un objeto que tiene una SoftReference apuntando a él no será recolectado como basura hasta que la JVM necesite absolutamente memoria.

Veamos cómo podemos crear una SoftReference en Java:

Integer prime = 1; SoftReference soft = new SoftReference(prime); prime = null;

El objeto principal tiene una fuerte referencia que lo apunta.

A continuación, estamos envolviendo la referencia fuerte principal en una referencia suave. Después de hacer que esa referencia fuerte sea nula , un objeto principal es elegible para GC, pero se recopilará solo cuando JVM necesite absolutamente memoria.

2.3. Referencias débiles

Los objetos a los que se hace referencia solo por referencias débiles se recolectan ansiosamente como basura; el GC no esperará hasta que necesite memoria en ese caso.

Podemos crear una WeakReference en Java de la siguiente manera:

Integer prime = 1; WeakReference soft = new WeakReference(prime); prime = null;

Cuando hicimos una referencia principal nula , el objeto principal será recolectado como basura en el siguiente ciclo de GC, ya que no hay ninguna otra referencia fuerte que apunte a él.

Las referencias de un tipo WeakReference se utilizan como claves en WeakHashMap .

3. WeakHashMap como caché de memoria eficiente

Digamos que queremos construir una caché que mantenga grandes objetos de imagen como valores y nombres de imágenes como claves. Queremos elegir una implementación de mapa adecuada para resolver ese problema.

Usar un HashMap simple no será una buena opción porque los objetos de valor pueden ocupar mucha memoria. Es más, nunca serán recuperados de la caché por un proceso de GC, incluso cuando ya no estén en uso en nuestra aplicación.

Idealmente, queremos una implementación de mapa que permita a GC eliminar automáticamente los objetos no utilizados. Cuando una clave de un objeto de imagen grande no está en uso en nuestra aplicación en ningún lugar, esa entrada se eliminará de la memoria.

Afortunadamente, WeakHashMap tiene exactamente estas características. Probemos nuestro WeakHashMap y veamos cómo se comporta:

WeakHashMap map = new WeakHashMap(); BigImage bigImage = new BigImage("image_id"); UniqueImageName imageName = new UniqueImageName("name_of_big_image"); map.put(imageName, bigImage); assertTrue(map.containsKey(imageName)); imageName = null; System.gc(); await().atMost(10, TimeUnit.SECONDS).until(map::isEmpty);

Estamos creando una instancia de WeakHashMap que almacenará nuestros objetos BigImage . Estamos poniendo un objeto BigImage como valor y una referencia de objeto imageName como clave. El imageName se almacenará en un mapa como un tipo WeakReference .

A continuación, establecemos la referencia imageName para que sea nula , por lo tanto, no hay más referencias que apunten al objeto bigImage . El comportamiento predeterminado de un WeakHashMap es reclamar una entrada que no tiene ninguna referencia a ella en el próximo GC, por lo que esta entrada se eliminará de la memoria en el próximo proceso de GC.

Estamos llamando a System.gc () para forzar a la JVM a activar un proceso GC. Después del ciclo de GC, nuestro WeakHashMap estará vacío:

WeakHashMap map = new WeakHashMap(); BigImage bigImageFirst = new BigImage("foo"); UniqueImageName imageNameFirst = new UniqueImageName("name_of_big_image"); BigImage bigImageSecond = new BigImage("foo_2"); UniqueImageName imageNameSecond = new UniqueImageName("name_of_big_image_2"); map.put(imageNameFirst, bigImageFirst); map.put(imageNameSecond, bigImageSecond); assertTrue(map.containsKey(imageNameFirst)); assertTrue(map.containsKey(imageNameSecond)); imageNameFirst = null; System.gc(); await().atMost(10, TimeUnit.SECONDS) .until(() -> map.size() == 1); await().atMost(10, TimeUnit.SECONDS) .until(() -> map.containsKey(imageNameSecond));

Tenga en cuenta que solo la referencia imageNameFirst se establece en nulo . La referencia imageNameSecond permanece sin cambios. Una vez que se activa GC, el mapa contendrá solo una entrada: imageNameSecond .

4. Conclusión

En este artículo, analizamos los tipos de referencias en Java para comprender completamente cómo java.util. WeakHashMap funciona. Creamos una caché simple que aprovecha el comportamiento de un WeakHashMap y probamos si funciona como esperábamos.

La implementación de todos estos ejemplos y fragmentos de código se puede encontrar en el proyecto GitHub, que es un proyecto de Maven, por lo que debería ser fácil de importar y ejecutar como está.