Regreso a las bases: Memoria

Lo que sigue es una traducción de una sección del ebook gratuito Foundations of Programming de Karl Seguin.


Regreso a las bases: Memoria


Por más que se intente, los lenguajes modernos de programación no pueden abstraer completamente los aspectos fundamentales de los sistemas computacionales. Por ejemplo, podemos asumir que usted se ha encontrado con las siguientes excepciones .NET: NullReferneceException, OutOfMemoryException, StackOverflowException y ThreadAbortException. Tan importante como es para desarrolladores adopter varios patrones y técnicas de alto nivel, es igualmente importante comprender el ecosistema en el cual su programa se ejecuta. Mirando por encima de las capas proveídas por el compilar de C# (o VB.NET), el CLR y el sistema operativo, nos encontramos con la memoria. Todos los programas hacen uso extensivo de la memoria del sistema e interaccionan con ella en maravillas maneras, es difícil ser un buen programador sin comprender esta interacción fundamental.
Mucha de la confusión sobre la memoria nace del hecho de que tanto C# y VB.NET son lenguajes administrados y que el CLR provee la recolección automática de basura. Esto ha causado que muchos desarrolladores asuman erróneamente que no necesitan preocuparse por la memoria.


Asignación de Memoria


En .NET, como en muchos otros lenguajes, cada variable que se defina está almacenada en el stack  o en el heap . Estos son dos espacios separados asignados en la memoria de sistema que sirven un propósito distinto, aunque complementario. Lo que va donde está predeterminado: valores de tipos van en el stack, mientras que la referencia a tipos va en el heap. En otras palabras, todos los tipos de sistema, como char, int, long, byte, enum y cualquier estructura (ya sean definidas por.NET o por usted) van en el stack. La única excepción a esta regla son los valores de tipos que pertenecen a referencias de tipos – por ejemplo la propiedad Id de una clase User va en el heap junto con la instancia de la clase User misma.


El Stack


Aunque estamos acostumbrados al mágico colector de basura, los valores en el stack son automáticamente administrados aún en un mundo sin colector de basura (como en C). Esto es porque cuando sea que entramos a un nuevo alcance (como un método o una sentencia If) los valores son empujados al stack y cuando salen del stack los valores son liberados. Esta es la razón por la que un stack es sinónimo a LIFO – last-in first-out (último en entrar primero en salir). Puede pensarlo en este modo: cuando se crea un nuevo alcance, por ejemplo un método, un marcador es puesto en el stack y los valores son añadidos como se necesiten. Cuando se deja ese alcance, todos los valores son liberados incluyendo el marcador del método. Esto funciona en cualquier nivel de anidado.
Hasta que veamos la interacción entre el heap y el stack, la única manera real de meterse en problemas con el stack es con StackOverflowException. Esto significa que ha usado todo el espacio disponible del stack. 99.9% del tiempo, esto indica una llamada recursiva interminable (una función que se llama a sí misma ad infinitum). En teoría esto puede ser causado por un muy muy mal diseño de sistema, aunque nunca he visto una llamada  no recursiva usando todo el espacio del stack.


El Heap


La asignación de memoria en el heap no es tan simple como el stack. La mayoría de la asignación de memoria basada en el heap ocurre cuando creamos un objeto new. El compilador averigua cuanta memoria necesitaremos (lo cual no es tan difícil, aún con objetos con referencias anidadas), toma un apropiado montón de memoria y regresa el apuntador a la memoria asignada (más acerca de esto en un momento). El ejemplo más sencillo es una cadena, si cada carácter en una cadena toma 2 bytes, y creamos una nueva cadena con el valor de “Hola Mundo”, entonces el CLR necesitará asignar 22 bytes (11×2) más cualquier adicional necesitado.
Hablando de cadenas, seguramente ha oído que las cadenas son inmutables – esto es, una vez que ha sido declarada una cadena y asignado un valor, si se modifica esa cadena (cambiando el valor o concatenando otra cadena a ella), entonces una nueva cadena se crea. Esto realmente puede tener implicaciones de rendimiento negativas, y por ello la recomendación general es usar un StringBuilder para cualquier manipulación de cadenas significativa. La verdad es que cualquier objeto almacenado en el heap es inmutable con respecto a la asignación de tamaño, y cualquier cambio en el tamaño subyacente requerirá una nueva asignación. El StringBuilder, junto con algunas colecciones, parcialmente pueden sacar la vuelta a esto usando buffers internos. Una vez que el buffer se llena, la misma reasignación ocurre y algún tipo de algoritmo de crecimiento es usado para determinar el nuevo tamaño (el más simple siendo antiguoTamaño * 2). Siempre que sea posible es buena idea especificar la capacidad inicial de dichos objetos para evitar este tipo de reasignación (el constructor para tanto el StringBuilder y el  ArrayList (entre muchas otras colecciones) le permiten especificar capacidad inicial).
Recolectar basura del heap es una tarea no trivial. A diferencia del stack donde el último alcance puede simplemente liberarlo, los objetos del heap no son locales a un determinado alcance. En lugar de ello, la mayoría son referencias profundamente anidadas de otros objetos referenciados. En lenguajes como en C, cuando un programador causa que la memoria sea asignada al heap, debe asegurarse también de remover del heap cuando ha terminado con él. En lenguajes administrados, el motor en tiempo de ejecución se encarga de limpiar los recursos (.NET usa un Recolector de Basura Generacional que está brevemente descrito en la Wikipedia).
Hay muchos incidentes horribles que pueden molestar a los desarrolladores mientras trabajan con el heap. Fugas de memoria no solo son posibles sino muy comunes, la fragmentación de memoria puede causar todo tipo de caos, y varios problemas de rendimiento pueden generarse gracias a comportamiento de asignación extraño o interacción con código sin administrar (lo cual .NET hace mucho debajo del agua).

One thought on “Regreso a las bases: Memoria”

Leave a Reply

Your email address will not be published. Required fields are marked *


*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>