by Valeriano Tortola
22. octubre 2007 05:05
Con este, quiero empezar una serie de pequeños artículos sobre como esta estructurada la memoria en .NET, sin profundizar en demasiados detalles... pero empezar por lo más simple y acabar hablando sobre temas de sincronización, atomicidad y volaticidad pasando por el GarbageCollector, intentando aportar una información que a mi parecer, todo desarrollador debería tener en mente a la hora de programar... y que se debe conocer en profundidad para desarrollar aplicaciones multithreading. Espero que sea útil para quien lo lea y personalmente me ayude a comprenderlo mejor.
La memoria de la que hacen uso nuestras aplicaciones administradas se divide en dos partes principalmente:
- Thread stack:
- Espacio de memoria asociado al hilo de ejecución.
- Es la pila donde va progresando nuestro código.
- Aquí se van "apilando" las llamadas a funciones y las variables locales y parámetros, de forma que el puntero de ejecución va cargando, ejecutando y liberando métodos con sus respectivas variables.
- No se pueden compartir ese espacio entre varios hilos, esta únicamente ligado un hilo de ejecución.
- Como decía es el espacio de memoria asociado al hilo de ejecución, por lo que es bastante rápido.
- Está limitado a un máximo de 1MByte por hilo.
- Si se intenta superar el límite de memoria se obtiene un StackOverflowException.
- Aquí se almacenan tipos de los cuales se conoce su tamaño antes de su inicialización (tipos por valor) y referencias a objetos del managed heap, que contienen la dirección de memoria del objeto allí ó un nulo si no referencian nada.
- El proceso de liberar memoria de este espacio se realiza de forma determinística por el puntero de ejecución.
- Managed Heap:
- Espacio de memoria asociado al proceso.
- Compartido entre los hilos que lo forman (si hubiese varios).
- Accedido mediante punteros (dirección/indirección) lo que lo hace más lento que el thread stack, pero permite ser accedido desde otros hilos.
- No tiene una limitación en su tamaño que no sea la del hardware.
- Normalmente almacena tipos por referencia y tipos por valor cuando tienen una relación de composición con un tipo por referencia.
- Si se superar el máximo de memoria disponible se obtiene un OutOfMemoryException.
- El proceso de liberar memoria de este espacio se realiza de forma no determinística por el recolector de basura (GarbageCollector).
Como se puede ver, nuestra ejecución reside en el thread stack (ya que MSIL es stack based), el puntero de ejecución va cargando lo que necesita para la ejecución y liberandolo en cuanto acaba. El problema viene cuando utilizamos tipos de los que no podemos saber la memoria que ocupan hasta después de su inicialización, entonces dependemos de otra zona de memoria "ilimitada" y de acceso mediante punteros llamada managed heap donde instanciamos dicho tipo, de forma que nuestro puntero de ejecución tiene una referencia en el thread stack apuntando a esa instancia pero no la instancia en si, lo que hace más lento el acceso al tener que recurrir a la indirección de la referencia.
Cuando hablamos de una referencia, estamos hablando de un puntero seguro y tipado, de forma que no puede apuntar a una dirección de memoria cualquiera, solo a la ubicación en memoria de una instancia de un tipo dado ó ser nulo(null).
El managed heap, al contrario del thread stack, puede ser compartido por varios hilos, lo cual no significa que pueda ser libremente compartido. Siempre que se vaya a trabajar con una instancia suceptible de ser accedida por múltiples hilos... es necesario sincronizar el acceso para evitar condiciones de anticipación y/ó dejarla en un estado inconsistente. Además, la memoria de este espacio no puede ser liberada de forma determinista, debe delegarse en un proceso en segundo plano llamado recolector de basura (GarbageCollector) encargado de liberar la memoria usada por instancias que no son referenciadas desde ningún thread stack.
En el próximo capítulo... las variables, tipos por valor y referencia.