Modelos de gestión de memoria III: Contador de referencias manual
Menú de navegaciónMenú
Categorías

La mejor forma de Aprender Programación online y en español www.campusmvp.es

Modelos de gestión de memoria III: Contador de referencias manual

abacoTal y como hemos dicho en más de una ocasión, uno de los aspectos más importantes en todo lenguaje de programación es cómo se gestiona la memoria. Es decir cuándo, cómo y quién libera los objetos que se van creando durante la ejecución del programa. De las cuatro grandes técnicas de gestión de la memoria, en post anteriores ya hemos hablado de la gestión manual, y de garbage collector. Hoy nos centraremos en el contador de referencias manual.

Contador de referencias manual

El tercer modelo de manejo de memoria es el contador de referencias y quizá Objective-C sea su mayor exponente, aunque no es el único. En estos casos el runtime del lenguaje mantiene una lista de las referencias que apuntan a cada objeto. Cada vez que una nueva referencia apunta al objeto el contador se incrementa en uno. Cuando una referencia deja de apuntar al objeto (p. ej. porque la variable sale de ámbito) el contador descuenta uno. En el momento en que el contador llega a cero, el objeto es destruido por el runtime.

La clave del contador de referencias es quién y cuándo lo actualiza. Si el contador de referencias debe actualizarlo el desarrollador estamos delante de un modelo de contador de referencias manual. Y si lo hace el sistema estamos delante de un modelo de contador de referencias automático.

Veamos un ejemplo simple en Objective-C de un contador de objetos manual:

NSMutableArray *beers = [[NSMutableArray alloc] init];
[beers addObject:@"Heineken"];
[beers release];

Este código crea un objeto llamado beers de tipo NSMutableArray. Luego inserta una cadena a dicho objeto y finalmente llama al método release para reducir el contador de referencias en uno. En Objective-C el método alloc reserva memoria para un objeto y además pone a 1 el contador de referencias. El método release no libera la memoria sino que disminuye en uno el contador de referencias del objeto asociado. La memoria se libera automáticamente cuando el contador llega a cero.

Usar un contador de referencias manual implica tener muy en cuenta el grafo de objetos (es decir, qué referencias acceden a qué objetos). Funciona un nivel por encima de la gestión manual pura y dura, ya que no debemos preocuparnos de saber si somos los últimos en acceder a un objeto (y por tanto debemos destruirlo). En su lugar solo debemos preocuparnos de sumar y restar uno al contador de referencias cuando usemos un objeto o dejemos de usarlo respectivamente.

Observa el siguiente código:

NSMutableArray* beers = [[NSMutableArray alloc] init];
[beers addObject:@"Heineken"];
NSMutableArray* beerscopy = beers;
[beers release]

Cuando llamamos al método alloc se reserva espacio para un MSMutableArray y el contador de referencias pasa a valer uno. En la línea 3 copiamos el puntero beers en beerscopy. Eso no clona el objeto, lo que tenemos ahora son dos punteros apuntando al mismo objeto. Pero ¿cuánto vale el contador de referencias? Pues en este caso sigue valiendo uno. Copiar un puntero no implica aumentar el valor del contador de referencias. Es decir tenemos la siguiente figura:

image

El objeto creado es el círculo y el número en su interior, el valor del contador de referencias. La flecha que va de beerscopy al objeto se ha dibujado con línea discontinua para indicar que este puntero es lo que se llama un puntero débil. Un puntero débil nos permite acceder a un objeto pero no incrementa el contador de referencias asociado. Como consecuencia la última línea llama a release del objeto apuntado por beers (el mismo que beerscopy), por lo tanto el contador de referencias disminuirá en una unidad, es decir, valdrá cero y entonces el objeto será destruido.

Si queremos que el contador de referencias se incremente al asignar beerscopy debemos indicarlo explícitamente llamando al método retain:

NSMutableArray* beers = [[NSMutableArray alloc] init];
[beers addObject:@"Heineken"];
NSMutableArray* beerscopy = beers;
[beerscopy retain]
[beers release]

En este caso, después de la llamada a retain el contador de referencias del objeto vale 2, por lo que la siguiente llamada al método release no destruye al objeto, puesto que después el contador al disminuir en una unidad pasará a valer 1.

La existencia de los punteros débiles parece un fallo pero de hecho son imprescindibles para evitar el gran problema de los sistemas basados en contadores de referencias (manuales y automáticos): las referencias circulares. Ejemplo clásico: Imagina un objeto A que apunta a otro objeto B, concretamente una relación de padre-hijo donde el padre tiene un puntero a su hijo y viceversa (el hijo tiene un puntero a su padre). Si por ejemplo el padre es el nodo raíz de un árbol, tendríamos un grafo de objetos como el siguiente:

image

El objeto de la clase Tree encapsula el árbol y tiene un puntero que apunta a él, llamado mytree. Su contador de referencias vale uno. La clase Tree tiene un destructor (método que se ejecuta cuando el runtime destruye un objeto) que automáticamente llama a release del nodo raíz (el único nodo apuntado por el objeto de la clase Tree ). Cuando se llama al método release de la referencia myTree, se reduce en uno el contador de referencias del objeto asociado. Entonces, éste pasa a valer 0, lo que implica destruir el objeto (y ejecutar el destructor asociado). El destructor llama a release del nodo raíz (con la intención de liberarlo) pero finalmente el grafo de objetos queda así:

image

Efectivamente la llamada al método release de mytree ha destruido el objeto Tree y se ha ejecutado el constructor, disminuyendo en uno el contador del objeto nodo raíz. Pero el valor del contador no es cero y el objeto no se libera. Además no tenemos ya ningún puntero para poder acceder a dicho objeto: tenemos un memory leak. Los punteros débiles permiten solucionar este caso:

image

La figura anterior muestra el mismo grafo de objetos pero con punteros débiles de los nodos hijos al nodo padre. Si ahora destruimos el objeto Tree , el destructor disminuiría en uno el contador del nodo raíz que pasaría a valer cero por lo que se destruiría. Eso ejecutaría el destructor de la clase TreeNode que debería llamar a release de todos los nodos hijos, destruyendo el árbol en cascada.

En resumen, el contador de referencias manual es una abstracción sobre la gestión manual, donde en lugar de pensar cuándo tenemos que liberar un objeto, pensamos cuándo debemos dejar de usarlo, pero nos despreocupamos sobre si otros lo siguen usando. La liberación del objeto es gestionada automáticamente por el runtime en base al contador que actualizamos manualmente.

En Objective-C se conoce como MMR (Manual Retain Release) al contador de referencias manual y actualmente está considerado obsoleto. Apple recomienda encarecidamente usar ARC (Automatic Reference Counting) que es un contador de referencias automático, y de hecho Swift (el lenguaje creado para terminar sustituyendo a Objective-C) soporta ya solamente ARC.

Eduard Tomás Eduard es ingeniero informático y atesora muchos años de experiencia como desarrollador. Está especializado en el desarrollo de aplicaciones web y móviles. Mantiene un conocido blog sobre desarrollo en .NET en general y ASP.NET MVC en particular. Colabora con la comunidad impartiendo charlas en formato webcast y en eventos de distintos grupos de usuarios. Puedes seguirlo en Twitter en @eiximenis. Ver todos los posts de Eduard Tomás

No te pierdas ningún post

Únete gratis a nuestro canal en Telegram y te avisaremos en el momento en el que publiquemos uno nuevo.

Archivado en: Lenguajes y plataformas

Agregar comentario