Ads by Smowtion

Gestión de Memoria - Parte I

Parte II | Parte III | Parte IV

A pesar de que con .NET no tenemos que preocuparnos con la gestión de memoria, aquí intentaremos tener una comprensión básica de cómo funciona. Esto nos ayudará a comprehender el comportamiento de las variables con las que trabajamos en los programas que escribimos. Estas nociones nos pueden ayudar a escribir programas más eficientes en su desempeño.

El .NET framework cuando ejecuta su programa guarda los elementos en dos tipos de lugares en la memoria:

1. El Stack.
2. El Heap.

Tanto el Stack como el Heap actúan en la ejecución del programa y residen en la memoria operativa de la máquina.

Cuál es la diferencia entre Stack y Heap?

Bueno, el Stack es responsable por hacer el seguimiento de la ejecución de lo que está en nuestro código. El Heap es más o menos responsable por hacer seguimiento de la mayor parte de nuestros datos.

El Stack lo podemos imaginar como un conjunto de cajas ordenadas una encima de otra. Cada vez que llamamos a un método, para hacer el seguimiento de lo que ocurre en la aplicación colocamos una caja en la parte superior del Stack a esta caja se le llama “Frame”. Una vez que el método es ejecutado retiramos esta caja y la tiramos a la basura. Procedemos entonces, a utilizar la caja anterior que ahora se encuentra en la parte superior del Stack. El Heap es similar, solo que su función es mantener la información. Lo que existe en el Heap puede ser accedido en cualquier momento sin limitaciones en lo que se puede acceder como ocurre en el Stack.

El Stack se mantiene por sí mismo, lo que significa que básicamente se encarga de su gestión propia memoria. Cuando el cuadro de arriba ya no se utiliza, es expulsado y nada más ocurre. El Heap, en cambio, tiene que preocuparse por la recolección de basura (GC) - que trata de mantener la limpieza del Heap.



La imagen anterior, aunque no es una verdadera representación de lo que está pasando en la memoria, nos ayuda a distinguir un Stack de un Heap.

Que elementos tenemos en el Stack y en el Heap?

Tenemos cuatro tipos principales de elementos que se pueden poner en el Stack y Heap cuando nuestro código se está ejecutando:

1. Tipos Valores
2. Tipos Referencia
3. Punteros
4. Instrucciones

Tipos Valores

En C #, todas las “cosas", declaradas de tipos de la siguiente lista son Tipos Valor (porque pertenecen al espacio de nombres System.ValueType):

• bool
• byte
• char
• decimal
• double
• enum
• float
• int
• long
• sbyte
• short
• struct
• uint
• ulong
• ushort

Tipos Referencia

Todas las "cosas" declaradas de tipos de la siguiente lista son Tipos Referencia (y heredan de System.Object ... excepto, por supuesto, para el objeto que es el objeto System.Object):

• class
• interface
• delegate
• object
• string

Punteros

El tercer tipo de “cosas” que existen en nuestro esquema de memoria son las referencias a un tipo, conocidas con frecuencia como Punteros. Un Puntero (o referencia) es un fragmento de espacio en la memoria que apunta a otro espacio en la memoria. Un puntero ocupa un espacio igual que cualquier otra cosa que estamos poniendo en la pila y pila y su valor puede ser una dirección de memoria o nulo.

Instrucciones

Sobre la marcha comprenderemos lo que son las instrucciones

¿Cómo se decide lo que va a donde?

Bien, pasemos a la parte divertida...
Existen dos reglas de oro para tal:

1. Un Tipo Referencia siempre va en el Heap. ¿Fácil no?
2. Los Tipos Valor y los Punteros van siempre donde fueron declarados. Esto es
un poco más complejo y necesita un poco de comprensión de cómo funciona el
Stack, para de esta forma averiguar donde las "cosas" se declaran.

El Stack, como se mencionó anteriormente, es responsable de mantener la pista de dónde está cada subproceso durante la ejecución de nuestro código. Usted puede
pensar en él como un hilo de "estado" y cada hilo tiene su propio Stack. Cuando nuestro código realiza una llamada a ejecutar un método del hilo comienza a
ejecutar las instrucciones que se han compilado JIT y que existen en la tabla de métodos, también pone a los parámetros del método en la pila de subprocesos.

Luego, a medida que avanzamos a través del código y actuamos sobre las variables al ejecutar el método las cuales se colocan en la parte superior del Stack. Esto será más fácil de entender con el siguiente ejemplo.

public int SumaDos(int punteroValor)
{
int resultado;
resultado = punteroValor + 2;
return resultado;
}

Esto es lo que sucede en la parte superior del Stack. Tenga en cuenta que lo que estamos viendo es colocado encima de muchos otros elementos que ya existen en el Stack:

Una vez que ejecutamos el método, sus parámetros se colocan en el Stack (vamos a hablar más acerca de como pasar parámetros más adelante).

NOTA : El método no existe en el Stack solo se ilustra como referencia.



A continuación, el control (el hilo de ejecución del método) se pasa a las instrucciones de SumaDos(), que existe en nuestro tipo de tabla de métodos, una compilación JIT se realiza si esta es la primera vez que se llama al método



A medida que el método se ejecuta, necesitamos de memoria para la variable "resultado" y se asigna en el Stack.



Se finaliza la ejecución del método y se devuelve el resultado.



Y toda la memoria asignada en el Stack se limpia por desplazamiento del puntero a la dirección de memoria disponible donde SumaDos() tenia inicio y bajamos al método anterior en el Stack.



En este ejemplo, nuestra variable "resultado" se coloca en el Stack. De hecho, cada vez que un Tipo Valor se declara en el cuerpo de un método, este se colocará en el Stack.

Ahora, los Tipos Valor por veces también se colocan en el Heap. Recuerde la regla, Tipos Valor siempre van a donde fueron declarados? Bueno, si un Tipo Valor se declara fuera de un método, pero dentro de un Tipo Referencia se colocará en el Tipo Referencia del Heap.

Si tenemos la siguiente clase MiEntero (que es un Tipo Referencia porque es una clase):

public class MiEntero
{
public int MiValor;
}

y el siguiente método es ejecutado:

public MiEntero SumaDos(int punteroValor)
{
MiEntero resultado = new MiEntero();
resultado.MiValor = punteroValor + 2;
return resultado;
}

Al igual que antes, el hilo se inicia con la ejecución del método y sus parámetros se colocan en el Stack del hilo.



Ahora es cuando se pone interesante ...
Porque MiEntero es un Tipo Referencia, que se coloca en el Heap y que es referenciado por un puntero en el Stack.



Después de terminar la ejecución de SumaDos() (como en el primer ejemplo), procedemos a una limpieza ...



Y de esta forma nos quedamos con un MiEntero huérfano en el Heap (ya no existe ningun puntero en el Stack haciendo referencia a MiEntero en el Heap)!



Aquí es donde el recolector de basura (GC) entra en juego. Una vez que nuestro programa llega a un umbral de memoria y necesitamos de más espacio en el Heap,
el GC se iniciará. El CG detendrá todos los hilos en marcha (una Parada Total),
para localizar todos los objetos en el Heap que no están siendo utilizados por
el programa principal y eliminarlos. El CG entonces reorganiza todos los objetos
que quedan en el Heap para crear espacio y ajustar todos los punteros de estos objetos, tanto en la Stack como en el Heap. Como se puede imaginar, esto puede
ser muy costoso en términos de rendimiento, ahora puede observar el por qué
puede ser importante prestar atención a lo que hay en el Stack y en el Heap
cuando se trata de escribir código de alto rendimiento.

Ok ... pero ¿cómo me afecta?
Buena pregunta.

Cuando estamos usando de Tipos Referencia, estamos tratando con los punteros
que reflejan el tipo, no con la cosa misma. Cuando estamos usando Tipos Valores, estamos usando la cosa misma.

Para entender mejor esto, tenemos el siguiente ejemplo:

Si ejecutamos el siguiente código

public int RetornaValor()
{
int x = new int();
x = 3;
int y = new int();
y = x;
y = 4;
return x;
}

Vamos a obtener el valor 3. Bastante simple, ¿verdad?

Sin embargo, si estamos usando la clase MiEntero de antes

public class MiEntero
{
public int MiValor;
}

y el siguiente método es ejecutado:

public int RetornaValor2()
{
MiEntero x = new MiEntero();
x.MiValor = 3;
MiEntero y = new MiEntero();
y = x;
y.MiValor = 4;
return x.MiValor;
}

¿Qué obtenemos? ... 4!

¿Por qué? ... ¿Cómo x.MiValor puede llegar a ser 4? ... Observemos lo que estamos haciendo y veamos si tiene sentido:

En el primer ejemplo todo va según lo previsto:

public int RetornaValor()
{
int x = 3;
int y = x;
y = 4;
return x;
}



En el siguiente ejemplo, no conseguimos "3" ya que ambas variables "x" e "y" apuntan al mismo objeto en el Heap.

public int RetornaValor2()
{
MyInt x;
x.MyValue = 3;
MyInt y;
y = x;
y.MyValue = 4;
return x.MyValue;
}



Esperemos que con esto tenga una mejor comprensión de las diferencias básicas entre Tipo Valor y variables de Tipo Referencia en C # y una comprensión básica de lo que es un puntero y cuando se utiliza. En la siguiente parte de esta serie, vamos a llegar más lejos en el manejo de la memoria y hablaremos específicamente acerca de los parámetros de métodos.

Hasta la próxima parte de esta serie...


Sharing is caring. Share this article now!