Des de la primera línia de codi d'aquest curs, .NET ha gestionat automàticament la memòria de cada objecte creat amb new: mai has hagut de reservar-la ni alliberar-la a mà, com sí que caldria en llenguatges com C o C++. Aquesta comoditat té un mecanisme al darrere, el recol·lector d'escombraries (Garbage Collector, GC), i uns límits clars: hi ha recursos —fitxers, connexions de xarxa o de base de dades— que el GC no sap alliberar per si sol, i que depenen del patró IDisposable/using ja vist al Mòdul 5. Aquesta lliçó explica com funciona el GC per dins, retoma IDisposable amb la seva forma completa (el patró Dispose/finalitzador), i exposa una font de fuites de memòria molt real en C#: els esdeveniments amb subscriptors que mai es desubscriuen.

Contingut

  1. Pila (stack) i munt (heap): on viu cada dada
  2. El recol·lector d'escombraries: generacions 0, 1 i 2
  3. IDisposable i using/using var: repàs des de la lliçó d'E/S de Fitxers
  4. El patró Dispose/finalitzador complet
  5. GC.Collect(): per què gairebé mai s'ha de cridar manualment
  6. Fuites de memòria comunes en C#: esdeveniments no desubscrits
  7. Exemple: IDisposable complet sobre una connexió SQLite de BiblioTech

  1. Pila (stack) i munt (heap): on viu cada dada

.NET reserva memòria en dues zones amb regles de vida molt diferents:

Pila (stack) Munt (heap)
Què hi viu Variables locals de tipus valor (int, bool, struct...) i referències a objectes Els objectes en si, de qualsevol class (Llibre, Soci, List<T>...)
Com s'allibera Automàticament en sortir del mètode o bloc que la va declarar El recol·lector d'escombraries decideix quan, segons s'explica a l'apartat 2
Velocitat de reserva/alliberament Molt ràpida (n'hi ha prou amb moure un punter) Més lenta, gestionada pel recol·lector d'escombraries
Exemple a BiblioTech La variable local Llibre llibre1 (la referència) L'objecte Llibre en si (el que llibre1 referencia)

És important distingir la variable del que referencia: Llibre llibre1 = new Llibre(...); guarda a la pila una referència (una adreça de memòria) que apunta a l'objecte Llibre real, creat al munt per new. Quan el mètode acaba, aquesta referència de la pila desapareix immediatament; l'objecte Llibre del munt, en canvi, continua existint fins que el recol·lector d'escombraries decideix que ja no el necessita ningú —tema de l'apartat següent. Els tipus valor (struct, com els vistos a la lliçó d'Structs i Records del Mòdul 3) sí que poden viure íntegrament a la pila quan són variables locals, sense passar pel munt en absolut.

  1. El recol·lector d'escombraries: generacions 0, 1 i 2

El recol·lector d'escombraries de .NET allibera automàticament els objectes del munt que ja no tenen cap referència accessible des del programa —és a dir, aquells als quals ja no es pot arribar des de cap variable en ús—. Per no haver d'examinar tot el munt cada vegada, organitza els objectes en tres generacions, basant-se en una observació empírica: la majoria d'objectes moren joves (variables temporals d'un mètode), i els que sobreviuen tendeixen a viure molt de temps (el catàleg complet de Biblioteca, per exemple):

Generació Què conté Freqüència de recollida
Generació 0 Objectes acabats de crear (la majoria de Prestec, string temporals...) Molt freqüent i molt ràpida
Generació 1 Objectes que han sobreviscut a una recollida de la generació 0 Intermèdia
Generació 2 Objectes de llarga vida (el Cataleg de Biblioteca, mentre duri l'aplicació) Poc freqüent, més costosa

Quan el GC recol·lecta la generació 0 i troba que un objecte encara té referències actives, el promociona a la generació 1; si sobreviu també a una recollida de la generació 1, passa a la generació 2. Aquesta estratègia —revisar amb freqüència el que probablement ja ha mort, i amb poca freqüència el que probablement seguirà viu molt de temps— és el que permet que el recol·lector d'escombraries de .NET sigui, a la pràctica, molt eficient sense intervenció manual del programador.

flowchart LR
    A["new Prestec(...)"] --> B["Generacio 0"]
    B -->|Sobreviu a una recollida| C["Generacio 1"]
    C -->|Sobreviu de nou| D["Generacio 2"]
    B -->|Sense referencies actives| E["Memoria alliberada"]
    C -->|Sense referencies actives| E
    D -->|Sense referencies actives| E

  1. IDisposable i using/using var: repàs des de la lliçó d'E/S de Fitxers

El recol·lector d'escombraries resol la memòria gestionada (els objectes .NET del munt), però hi ha recursos que .NET no pot alliberar només recollint memòria: un fitxer obert pel sistema operatiu, una connexió de xarxa o de base de dades —recursos no gestionats—. IDisposable, ja presentat a la lliçó d'E/S de Fitxers (Mòdul 5), és el contracte que aquests tipus implementen per alliberar aquest recurs de forma determinista, és a dir, en un moment exacte i previsible, en lloc d'esperar que el GC decideixi actuar (cosa que podria trigar un temps indeterminat, deixant el fitxer o la connexió oberts mentrestant):

using SqliteConnection connexio = new SqliteConnection("Data Source=bibliotech.db");
connexio.Open();
// ... treball amb la connexio ...
// connexio.Dispose() es crida automaticament aqui, al final del bloc/metode

La regla pràctica fixada llavors continua vigent sense canvis: qualsevol objecte que implementi IDisposable es declara amb using (o using var), sense excepcions —StreamReader, SqliteConnection, BibliotecaDbContext (Mòdul 5) i, com es veurà a l'apartat 7, qualsevol classe pròpia que embolcalli un recurs no gestionat.

  1. El patró Dispose/finalitzador complet

La lliçó de Constructors i Destructors (Mòdul 3) va introduir el finalitzador (~Llibre()) com un mètode que el GC invoca abans d'alliberar un objecte, i va avançar que avui dia es prefereix IDisposable per la seva alliberació determinista. Ara que es coneixen tots dos mecanismes amb més profunditat, es poden combinar en el patró complet que Microsoft recomana per a qualsevol classe que administri un recurs no gestionat directament:

class RecursAmbFinalitzador : IDisposable
{
    private bool _alliberat = false;

    // Punt d'entrada public: alliberacio determinista, invocada explicitament per qui fa servir la classe
    public void Dispose()
    {
        Alliberar(disposing: true);
        GC.SuppressFinalize(this); // ja s'ha alliberat explicitament: el finalitzador no ha de repetir la feina
    }

    // Finalitzador: xarxa de seguretat, nomes actua si Dispose() mai es va cridar
    ~RecursAmbFinalitzador()
    {
        Alliberar(disposing: false);
    }

    protected virtual void Alliberar(bool disposing)
    {
        if (_alliberat)
        {
            return;
        }

        if (disposing)
        {
            // Alliberar aqui recursos GESTIONATS (altres objectes IDisposable que aquest objecte posseeixi)
        }

        // Alliberar aqui recursos NO GESTIONATS (handles de fitxer, connexions, memoria nativa...)

        _alliberat = true;
    }
}

Cada peça compleix un paper concret:

Membre Paper
Dispose() públic Camí normal: qui fa servir la classe amb using l'invoca de forma determinista
~RecursAmbFinalitzador() (finalitzador) Xarxa de seguretat: si algú oblida el using, el GC crida el finalitzador abans d'alliberar la memòria, evitant que el recurs quedi obert per sempre
GC.SuppressFinalize(this) Li diu al GC "ja he alliberat el recurs manualment, no cal que executis també el finalitzador" —evita alliberar dues vegades i accelera la recollida d'aquest objecte
_alliberat (bandera) Evita alliberar el mateix recurs dues vegades, si Dispose() es cridés més d'una vegada per error
Paràmetre disposing Distingeix si la crida ve de Dispose() (true, segur tocar altres objectes gestionats) o del finalitzador (false, en aquell moment el GC ja pot haver recol·lectat altres objectes, així que només és segur alliberar recursos no gestionats directes)

A la pràctica, la immensa majoria de classes que implementen IDisposable en C# modern no necessiten un finalitzador propi: n'hi ha prou amb Dispose() quan la classe només posseeix altres objectes IDisposable ja gestionats (com SqliteConnection), delegant en ells la part més delicada. El finalitzador complet només és necessari quan la classe gestiona directament un recurs no administrat per cap altre objecte IDisposable intermedi —un cas menys habitual, però important de reconèixer si apareix en codi existent.

  1. GC.Collect(): per què gairebé mai s'ha de cridar manualment

.NET exposa GC.Collect(), que força una recollida d'escombraries immediata. És temptador pensar que cridar-lo "ajuda" al rendiment, però en la immensa majoria de casos aconsegueix just el contrari:

  • El recol·lector d'escombraries ja decideix, amb la seva pròpia heurística de generacions, quan és el moment més eficient de recollir; forçar-lo manualment sol interrompre aquesta heurística en un moment subòptim.
  • Una recollida completa (generació 2) és la més costosa de les tres; cridar-la amb freqüència des de codi d'aplicació pot degradar el rendiment en lloc de millorar-lo.
  • GC.Collect() no allibera recursos no gestionats (fitxers, connexions): això continua sent responsabilitat exclusiva d'IDisposable/using, no del recol·lector d'escombraries.

Els únics escenaris on GC.Collect() té una justificació real són molt específics (per exemple, immediatament després d'alliberar una quantitat enorme i puntual de memòria, en una eina de diagnòstic, o en proves de rendiment que mesuren el propi comportament del GC) i queden fora de l'abast d'aquest curs. La regla general per a BiblioTech i qualsevol aplicació normal: confia en el recol·lector d'escombraries automàtic i concentra't a alliberar correctament, amb using, els recursos no gestionats —això sí que està sota el teu control directe.

  1. Fuites de memòria comunes en C#: esdeveniments no desubscrits

Encara que el GC allibera automàticament la memòria gestionada, en C# és perfectament possible patir una fuita de memòria (objectes que haurien de poder alliberar-se però que mai s'alliberen): la causa més comuna és una subscripció a un esdeveniment que mai es desubscriu. La lliçó de Delegats i Esdeveniments (Mòdul 4) va definir Biblioteca.PrestecRegistrat:

class Biblioteca
{
    public event Action<Prestec> PrestecRegistrat;
    // ...
}
class PanelDeNotificacions
{
    public PanelDeNotificacions(Biblioteca biblioteca)
    {
        biblioteca.PrestecRegistrat += MostrarAvis; // se subscriu, pero mai es desubscriu
    }

    private void MostrarAvis(Prestec prestec)
    {
        Console.WriteLine($"Nou prestec: {prestec.Llibre.Titol}");
    }
}

El problema: mentre biblioteca.PrestecRegistrat mantingui aquesta subscripció (biblioteca.PrestecRegistrat += MostrarAvis), biblioteca conserva internament una referència a l'objecte PanelDeNotificacions subscrit —a través del delegat que apunta a MostrarAvis—. Si el codi de l'aplicació descarta la seva pròpia referència a un PanelDeNotificacions concret (per exemple, en tancar una finestra en una futura aplicació d'escriptori, Mòdul 7) sense desubscriure'l primer, aquest PanelDeNotificacions continua viu —inabastable per a la resta del programa, però referenciat per biblioteca—, i el recol·lector d'escombraries mai podrà alliberar-lo mentre biblioteca continuï viva. Si Biblioteca és una instància de llarga vida (com sol ser-ho, durant tota l'execució de l'aplicació) i es creen molts PanelDeNotificacions al llarg del temps sense desubscriure'ls mai, la memòria ocupada per panells ja "descartats" creix sense límit: una fuita de memòria clàssica en aplicacions C# amb esdeveniments, molt més habitual del que sembla a primera vista.

class PanelDeNotificacions : IDisposable
{
    private readonly Biblioteca _biblioteca;

    public PanelDeNotificacions(Biblioteca biblioteca)
    {
        _biblioteca = biblioteca;
        _biblioteca.PrestecRegistrat += MostrarAvis;
    }

    private void MostrarAvis(Prestec prestec)
    {
        Console.WriteLine($"Nou prestec: {prestec.Llibre.Titol}");
    }

    public void Dispose()
    {
        _biblioteca.PrestecRegistrat -= MostrarAvis; // desubscripcio explicita: trenca la referencia
    }
}

Convertir PanelDeNotificacions també en IDisposable, amb -= al seu Dispose(), resol el problema: en desubscriure's explícitament, biblioteca deixa de mantenir cap referència a aquest panell concret, i el GC pot alliberar-lo amb normalitat tan bon punt la resta del programa deixi de fer-lo servir. La regla general: qualsevol subscripció a un esdeveniment de llarga vida ha de tenir el seu -= corresponent en algun punt del cicle de vida del subscriptor, igual que tot Open() necessita el seu Dispose().

  1. Exemple: IDisposable complet sobre una connexió SQLite de BiblioTech

Unint el patró de l'apartat 4 amb SqliteConnection (Mòdul 5), així queda una classe pròpia de BiblioTech que embolcalla la connexió i garanteix la seva alliberació:

using Microsoft.Data.Sqlite;

class RepositoriSqlite : IDisposable
{
    private readonly SqliteConnection _connexio;
    private bool _alliberat = false;

    public RepositoriSqlite(string cadenaConnexio)
    {
        _connexio = new SqliteConnection(cadenaConnexio);
        _connexio.Open();
    }

    public List<Llibre> ObtenirLlibresDisponibles()
    {
        List<Llibre> llibres = new List<Llibre>();

        using SqliteCommand ordre = _connexio.CreateCommand();
        ordre.CommandText = "SELECT Titol, Autor, Isbn FROM Llibres WHERE Disponible = 1";

        using SqliteDataReader lector = ordre.ExecuteReader();
        while (lector.Read())
        {
            llibres.Add(new Llibre(lector.GetString(0), lector.GetString(1), lector.GetString(2)));
        }

        return llibres;
    }

    public void Dispose()
    {
        Alliberar(disposing: true);
        GC.SuppressFinalize(this);
    }

    ~RepositoriSqlite()
    {
        Alliberar(disposing: false);
    }

    protected virtual void Alliberar(bool disposing)
    {
        if (_alliberat)
        {
            return;
        }

        if (disposing)
        {
            _connexio.Dispose(); // SqliteConnection es IDisposable: nomes es segur tocar-la si disposing == true
        }

        _alliberat = true;
    }
}
using RepositoriSqlite repositori = new RepositoriSqlite("Data Source=bibliotech.db");
List<Llibre> disponibles = repositori.ObtenirLlibresDisponibles();

foreach (Llibre llibre in disponibles)
{
    Console.WriteLine(llibre.Titol);
}
// repositori.Dispose() es crida automaticament aqui, tancant _connexio al seu torn

RepositoriSqlite és, en si mateix, un IDisposable que posseeix un altre IDisposable (_connexio): un cas on, a la pràctica, n'hi hauria prou amb Dispose() sense finalitzador (perquè SqliteConnection ja té el seu propi finalitzador de reserva). Es mostra aquí el patró complet, amb finalitzador inclòs, precisament per deixar visible l'estructura sencera —la mateixa que fan servir internament classes del propi .NET com SqliteConnection— i que sàpigues reconèixer-la si la trobes en codi de tercers.

Errors Comuns i Consells

  • Oblidar using sobre un IDisposable propi o aliè: sense using, el recurs no gestionat (fitxer, connexió) roman obert fins que el finalitzador s'executi —en un moment indeterminat, decidit pel GC, no pel programador—, cosa que pot esgotar recursos del sistema operatiu sota càrrega.
  • Subscriure's a un esdeveniment de llarga vida sense desubscriure's mai: com s'ha vist amb PrestecRegistrat, és la causa més comuna de fuites de memòria en aplicacions C#; tota subscripció += de llarga durada necessita el seu -= corresponent.
  • Afegir un finalitzador a una classe que no gestiona directament cap recurs no gestionat: un finalitzador té cost (el GC necessita almenys dos cicles de recollida per alliberar del tot un objecte amb finalitzador); si la classe només posseeix altres IDisposable ja gestionats, n'hi ha prou amb Dispose(), sense finalitzador propi.
  • Cridar GC.Collect() pensant que "neteja" recursos no gestionats: no ho fa; només actua sobre memòria gestionada, i en la majoria d'aplicacions normals empitjora el rendiment en lloc de millorar-lo.
  • Consell: per detectar fuites de memòria per esdeveniments no desubscrits en una aplicació real, les eines de perfilatge de memòria (memory profilers) permeten veure quins objectes continuen vius i per quina cadena de referències —sovint, la resposta és exactament un delegat d'esdeveniment oblidat.

Exercicis

  1. Explica, en un paràgraf breu, la diferència entre la pila i el munt, i en quina de les dues zones viu l'objecte Llibre creat per Llibre llibre1 = new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4"); davant la variable llibre1 en si.

  2. Escriu una classe ConnexioSimulada : IDisposable amb un camp bool _oberta = true i un mètode Dispose() que posi _oberta a false i imprimeixi "Connexio tancada.". Fes-la servir amb using dins d'un bloc i comprova, després del bloc, que la connexió ja no està oberta.

  3. Retomant PanelDeNotificacions de l'apartat 6: escriu una versió d'aquesta classe que implementi IDisposable i es desubscrigui correctament de Biblioteca.PrestecRegistrat al seu Dispose(). Subscriu-la, registra un préstec amb RegistrarPrestec(...) (Mòdul 4) per comprovar que rep l'avís, crida Dispose(), i registra un segon préstec comprovant que ja no es mostra cap avís.

Solucions

La pila emmagatzema variables locals de vida curta, lligades al mètode que les declara —inclosa la referència llibre1, que no és més que una adreça de memòria—; s'allibera automàticament en sortir del mètode. El munt emmagatzema els objectes en si, creats amb new —aquí, l'objecte Llibre amb les seves propietats Titol, Autor, Isbn—; aquest objecte viu al munt fins que el recol·lector d'escombraries determina que ja no hi ha cap referència accessible cap a ell des del programa.

class ConnexioSimulada : IDisposable
{
    private bool _oberta = true;

    public bool EstaOberta => _oberta;

    public void Dispose()
    {
        _oberta = false;
        Console.WriteLine("Connexio tancada.");
    }
}

ConnexioSimulada? referenciaExterna;
using (ConnexioSimulada connexio = new ConnexioSimulada())
{
    referenciaExterna = connexio;
    Console.WriteLine(connexio.EstaOberta); // True
} // Dispose() es crida aqui automaticament

Console.WriteLine(referenciaExterna.EstaOberta); // False
class PanelDeNotificacions : IDisposable
{
    private readonly Biblioteca _biblioteca;

    public PanelDeNotificacions(Biblioteca biblioteca)
    {
        _biblioteca = biblioteca;
        _biblioteca.PrestecRegistrat += MostrarAvis;
    }

    private void MostrarAvis(Prestec prestec)
    {
        Console.WriteLine($"Nou prestec: {prestec.Llibre.Titol}");
    }

    public void Dispose()
    {
        _biblioteca.PrestecRegistrat -= MostrarAvis;
    }
}

Biblioteca biblioteca = new Biblioteca();
Llibre llibre1 = new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Soci soci1 = new Soci(1, "Ana Martinez");
biblioteca.AfegirMaterial(llibre1);
biblioteca.AfegirSoci(soci1);

PanelDeNotificacions panel = new PanelDeNotificacions(biblioteca);

llibre1.Prestar();
biblioteca.RegistrarPrestec(new Prestec(llibre1, soci1)); // el panell mostra l'avis

panel.Dispose(); // desubscripcio explicita

Llibre llibre2 = new Llibre("Ficciones", "Jorge Luis Borges", "978-84-376-0496-1");
biblioteca.AfegirMaterial(llibre2);
llibre2.Prestar();
biblioteca.RegistrarPrestec(new Prestec(llibre2, soci1)); // ja no es mostra cap avis

Conclusió

En aquesta lliçó has vist com gestiona .NET la memòria: la diferència entre pila i munt, com el recol·lector d'escombraries organitza els objectes en generacions per recollir de forma eficient, i per què gairebé mai convé cridar GC.Collect() manualment. També has completat el patró IDisposable/finalitzador iniciat al Mòdul 3, aplicat sobre una connexió SQLite de BiblioTech, i has vist una font molt real de fuites de memòria en C#: els esdeveniments subscrits que mai es desubscriuen, amb PrestecRegistrat com a exemple concret.

L'última lliçó d'aquest mòdul, Multifil i Programació Paral·lela, canvia de pla: en lloc de memòria, s'ocupa de temps de CPU. Retoma l'asincronia del Mòdul 4 (async/await, pensada per esperar E/S sense bloquejar) i la contrasta amb el paral·lelisme real —diversos fils de CPU treballant literalment alhora—, amb Thread, Task.Run, Parallel.For/Parallel.ForEach i les tècniques necessàries per protegir dades compartides entre fils, tancant així el Mòdul 6 abans de passar, al Mòdul 7, a construir per fi una interfície per a BiblioTech.

Curs de Programació en C#

Mòdul 1: Introducció al C#

Mòdul 2: Estructures de Control

Mòdul 3: Programació Orientada a Objectes

Mòdul 4: Conceptes Avançats de C#

Mòdul 5: Treballant amb Dades

Mòdul 6: Temes Avançats

Mòdul 7: Construcció d'Aplicacions

Mòdul 8: Bones Pràctiques i Patrons de Disseny

Mòdul 9: Projecte Final

© Copyright 2026. Tots els drets reservats