Comprender la recolección de basura

Comprender la recolección de basura

En este artículo, aprenderemos:

¿Qué es la recolección de basura?

Cuando se inicia un programa, el sistema asigna algo de memoria para que se ejecute el programa.

Cuando un programa C# instancia una clase, crea un objeto.

El programa manipula el objeto y, en algún momento, es posible que el objeto ya no sea necesario.
Cuando el programa ya no puede acceder al objeto y se convierte en un candidato para la recolección de elementos no utilizados.

Hay dos lugares en la memoria donde CLR almacena elementos mientras se ejecuta su código.

  • apilar
  • montón

La pila realiza un seguimiento de lo que se ejecuta en su código (como sus variables locales), y el montón realiza un seguimiento de sus objetos.

Los tipos de valor se pueden almacenar tanto en la pila como en el montón.

Para un objeto en el montón, siempre hay una referencia en la pila que apunta a él.

El recolector de basura comienza a limpiar solo cuando no hay suficiente espacio en el montón para construir un nuevo objeto
La pila se borra automáticamente al final de un método. El CLR se encarga de esto y no tienes que preocuparte por eso.

El montón es administrado por el recolector de elementos no utilizados.

En entornos no administrados sin un recolector de basura, debe realizar un seguimiento de los objetos que se asignaron en el montón y debe liberarlos explícitamente. En .NET Framework, esto lo hace el recolector de elementos no utilizados.

¿Cómo funciona el recolector de basura?

Veamos el siguiente diagrama para entenderlo mejor.

Antes de que se ejecute el recolector de basura:

En el diagrama anterior, antes de que se ejecute el recolector de basura, la raíz de la aplicación depende del objeto 1, el objeto 3 y el objeto 5.
El objeto 1 depende del objeto 2 y el objeto 5 depende del objeto 6. Por lo tanto, la raíz de la aplicación no no tiene ninguna dependencia en el objeto 4 y el objeto 7.

Cuando se ejecuta el recolector de basura:

Fase de Marcado:

  • Marca toda la memoria del montón como no en uso
  • Luego examina todas las variables de referencia de programas, parámetros que tienen referencia de objeto, registros de CPU y otros elementos que apuntan a objetos de montón

Fase de traslado:

  • Para cada referencia, el recolector de elementos no utilizados marca el objeto al que apunta la referencia como en uso

Fase compacta:

  • Luego compacta la memoria del montón que todavía está en uso y actualiza la referencia del programa
  • El recolector de basura actualiza el montón para que el programa pueda asignar memoria de la parte no utilizada

Después de que se ejecute el recolector de basura:

Descarta el Objeto 4 y el Objeto 7 ya que no existe ninguna dependencia y compacta la memoria del montón.

Cuando destruye un objeto, el recolector de basura libera la memoria del objeto y cualquier recurso no administrado que contenga.

Puede usar Destructor y el método Dispose para determinar cuándo y cómo el objeto libera recursos administrados y no administrados.

Destructores:

  • Los destructores se pueden definir solo en clases, no en estructuras.
  • Una clase puede tener como máximo un destructor.
  • Los destructores no se pueden heredar ni sobrecargar.
  • Los destructores no se pueden llamar directamente.
  • Los destructores no pueden tener modificadores o parámetros.

Destructor a Finalizador:

El GC en realidad llama al finalizador de un objeto, no a su destructor. El destructor se convierte en una versión anulada del método Finalize que ejecuta el código del destructor y luego llama al método Finalize de la clase base.

Por ejemplo, supongamos que la clase Empleado incluye el siguiente destructor:

~Employee()
{
// Free unmanaged resources here.
...
}
This destructor is converted into the following Finalize method:
protected override void Finalize()
{
try
{
// Free unmanaged resources here.
...
}
finally
{
base.Finalize();
}
}

No puede anular explícitamente el método Finalize en el código C#.

Método de eliminación:

C# define la interfaz IDisposable, que declara el método Dispose.
Si una clase implementa esta interfaz, la declaración de uso llamará automáticamente al método Dispose de un objeto, por lo que no necesita hacerlo explícitamente.

Si el método Dispose ha liberado todos los recursos del objeto, entonces no es necesario invocar al destructor.

El método Dispose puede llamar a GC.SuppressFinalize para decirle al recolector de basura que omita el Destructor de objetos.

Veamos un ejemplo bajo para ver la implementación del método Dispose en C#.

public class MyWrappedResource : IDisposable
{
    //our managed resource
    IDbConnection _conn = null;
    public MyWrappedResource(string filename)
    {
    }
    public void Close()
    {
        Dispose(true);
    }
    public void Dispose()
    {
        Dispose(true);
    }
    private bool _disposed = false;
    protected void Dispose(bool disposing)
    {
        //in a class hierarchy, don’t forget to call the base class!
        //base.Dispose(disposing);
        if (!_disposed)
        {
            _disposed = true;
            if (disposing)
            {
                //cleanup managed resources
                if (_conn != null)
                {
                    _conn.Dispose();
                }
            }
            //cleanup unmanaged resources here, if any
        }
    }
}

Reglas sobre la gestión de recursos:

  • Si una clase no contiene recursos administrados ni recursos no administrados, no necesita implementar IDisposable ni tener un destructor.
  • Si la clase solo tiene recursos administrados, debería implementar IDisposable pero no necesita un destructor.
  • Si la clase solo tiene recursos no administrados, debe implementar IDisposable y necesita un destructor en caso de que el programa no llame a Dispose.
  • El método Dispose debe ser seguro para ejecutarse más de una vez. Puede lograrlo utilizando una variable para realizar un seguimiento de si se ha ejecutado antes.
  • El método Dispose debe liberar tanto los recursos administrados como los no administrados.
  • El destructor debe liberar solo los recursos no administrados.
  • Después de liberar recursos, el destructor debe llamar a GC.SuppressFinalize, para que el objeto pueda omitir la cola de finalización.

Gestión de recursos no gestionados:

El recolector de basura se encargará de los recursos administrados. Pero cuando se ocupe de los recursos no administrados, como la conexión de red, el identificador de archivos, el identificador de ventanas, etc., debe liberar explícitamente esos elementos. De lo contrario, obtendrá errores como "Este archivo está en uso" o no podrá conectarse a su base de datos porque todas las conexiones están en uso.

Para manejar recursos no administrados, C# admite el concepto de finalización. Este mecanismo permite que un tipo se limpie antes de la recolección de basura.
Pero en C#, no puede estar seguro de cuándo se llama a un finalizador.
Ocurrirá solo cuando el recolector de basura determine que su objeto está listo para ser limpiado.
Un finalizador en C# requiere una sintaxis especial, al igual que un constructor. Debe anteponer el nombre de la clase con una tilde (~) para crear un finalizador.

Añadir finalizador:

public class FinalizerExample
{
~FinalizerExample()
{
// This code is called when the finalize method executes
}
}

Dentro del finalizador, puede limpiar otros recursos y asegurarse de que se libere toda la memoria.

Nota:

El finalizador se llama solo cuando se produce una recolección de elementos no utilizados.

Forzar recolección de basura:

Puede forzar esto agregando una llamada a GC.Collect.

Ejemplo

StreamWriter stream = File.CreateText(“temp.dat”);
stream.Write(“some test data”);
GC.Collect();
GC.WaitForPendingFinalizers();
File.Delete(“temp.dat”);


La línea WaitForPendingFinalizers se asegura de que todos los finalizadores se hayan ejecutado antes de que el código continúe.
No se recomienda que llame a GC.Collect usted mismo.

Un finalizador aumenta la vida de un objeto. Debido a que el código de finalización también debe ejecutarse, .NET Framework mantiene una referencia al objeto en
una cola de finalización especial. Un subproceso adicional ejecuta todos los finalizadores a la vez que se considera apropiado según el contexto de ejecución. Esto retrasa la recolección de elementos no utilizados para los tipos que tienen un finalizador.

Implementación de IDisposable y Finalizer:

using System;
using System.IO;
class UnmangedWrapper : IDisposable
{
public FileStream Stream { get; private set; }
    public UnmangedWrapper()
        {
        this.Stream = File.Open(“temp.dat”, FileMode.Create);
        }
    ~UnmangedWrapper()
        {
        Dispose(false);
        }
public void Close()
    {
        Dispose();
    }
public void Dispose()
    {
        Dispose(true);
        System.GC.SuppressFinalize(this);
    }
public void Dispose(bool disposing)
    {
        if (disposing)
            {
            if (Stream != null)
                {
                Stream.Close();
                }
        }
    }
}

Diferencia entre desechar y finalizar:

Eliminar Finalizar
Se utiliza para liberar recursos no administrados en cualquier momento. Se puede usar para liberar recursos no administrados retenidos por un objeto antes de que ese objeto se destruya.
Es llamado por el código de usuario y la clase que está implementando el método dispose, debe implementar la interfaz IDisposable. Es llamado por Garbage Collector y no puede ser llamado por código de usuario.
Se implementa implementando el método Dispose() de la interfaz IDisposable. Se implementa con la ayuda de Destructors
No hay costes de rendimiento asociados con el método Dispose. Hay costos de rendimiento asociados con el método Finalize, ya que no limpia la memoria inmediatamente y el GC lo llama automáticamente.

Uso de referencias débiles:

Las referencias débiles se usan mejor para elementos que pueden usar mucha memoria, pero que se recrean fácilmente según sea necesario, como en situaciones de caché en las que sería bueno si el objeto todavía estuviera en la memoria, pero aún desea que se recolecte basura. eventualmente.

Veamos el siguiente ejemplo para entender cómo usar referencias débiles:

class Program
{

class Book
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Author { get; set; }
};

static void Main(string[] args)
{
    Cache<int, Book> bookCache = new Cache<int, Book>();
    Random rand = new Random();
    int numBooks = 100;
    //add books to cache
    for (int i=0;i<numBooks;++i)
    {
    bookCache.Add(i, GetBookFromDB(i));
    }
    //lookup random books and track cache misses
    Console.WriteLine(“Looking up books”);
    long lookups = 0, misses = 0;
    while (!Console.KeyAvailable)
    {
    ++lookups;
    int id = rand.Next(0, numBooks);
    Book book = bookCache.GetObject(id);
    if (book == null)
    {
    ++misses;
    book = GetBookFromDB(id);
    }
    else
    {
    //add a little memory pressure to increase
    //the chances of a GC
    GC.AddMemoryPressure(100);
    }
    bookCache.Add(id, book);
    }
    Console.ReadKey();
    Console.WriteLine(“{0:N0} lookups, {1:N0} misses”,
    lookups, misses);
    Console.ReadLine();
}
static Book GetBookFromDB(int id)
{
    //simulate some database access
    return new Book { Id = id,
    Title = “Book” + id,
    Author = “Author” + id };
}
}

Resumen:

En este artículo, hemos discutido:

  • ¿Qué es la recolección de basura?
  • ¿Cómo funciona el recolector de basura?
  • Destructores
  • Administrar recursos no administrados
  • Implementación de IDisposable y Finalizer
  • Implementación del método Dispose
  • Diferencia entre los métodos Dispose y Finalize
  • Uso de referencias débiles

También te pueden interesar las preguntas de la entrevista sobre la recolección de basura aquí.

¡¡Gracias por visitarnos!!