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
- Pila (stack) i munt (heap): on viu cada dada
- El recol·lector d'escombraries: generacions 0, 1 i 2
IDisposableiusing/using var: repàs des de la lliçó d'E/S de Fitxers- El patró
Dispose/finalitzador complet GC.Collect(): per què gairebé mai s'ha de cridar manualment- Fuites de memòria comunes en C#: esdeveniments no desubscrits
- Exemple:
IDisposablecomplet sobre una connexió SQLite de BiblioTech
- 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.
- 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
IDisposable i using/using var: repàs des de la lliçó d'E/S de Fitxers
IDisposable i using/using var: repàs des de la lliçó d'E/S de FitxersEl 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/metodeLa 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.
- El patró
Dispose/finalitzador complet
Dispose/finalitzador completLa 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.
GC.Collect(): per què gairebé mai s'ha de cridar manualment
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.
- 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 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().
- Exemple:
IDisposable complet sobre una connexió SQLite de BiblioTech
IDisposable complet sobre una connexió SQLite de BiblioTechUnint 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 tornRepositoriSqlite é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
usingsobre unIDisposablepropi o aliè: senseusing, 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
IDisposableja gestionats, n'hi ha prou ambDispose(), 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
-
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
Llibrecreat perLlibre llibre1 = new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4");davant la variablellibre1en si. -
Escriu una classe
ConnexioSimulada : IDisposableamb un campbool _oberta = truei un mètodeDispose()que posi_obertaafalsei imprimeixi"Connexio tancada.". Fes-la servir ambusingdins d'un bloc i comprova, després del bloc, que la connexió ja no està oberta. -
Retomant
PanelDeNotificacionsde l'apartat 6: escriu una versió d'aquesta classe que implementiIDisposablei es desubscrigui correctament deBiblioteca.PrestecRegistratal seuDispose(). Subscriu-la, registra un préstec ambRegistrarPrestec(...)(Mòdul 4) per comprovar que rep l'avís, cridaDispose(), 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#
- Introducció al C#
- Configuració de l'Entorn de Desenvolupament
- Programa Hola Món
- Sintaxi i Estructura Bàsica
- Variables i Tipus de Dades
- Arrays i Cadenes de Text
Mòdul 2: Estructures de Control
Mòdul 3: Programació Orientada a Objectes
- Classes i Objectes
- Mètodes
- Constructors i Destructors
- Herència
- Polimorfisme
- Encapsulació
- Abstracció
- Structs i Records: Tipus per Valor i per Referència
Mòdul 4: Conceptes Avançats de C#
- Interfícies
- Delegats i Esdeveniments
- Pattern Matching i Característiques Modernes de C#
- Genèrics
- Col·leccions
- LINQ (Consulta Integrada en el Llenguatge)
- Programació Asíncrona
Mòdul 5: Treballant amb Dades
- Entrada/Sortida de Fitxers
- Serialització
- Connectivitat amb Bases de Dades
- Entity Framework
- Treball amb JSON i Consum d'APIs REST
Mòdul 6: Temes Avançats
- Reflexió
- Atributs
- Programació Dinàmica
- Gestió de Memòria i Recollida d'Escombraries
- Multifil i Programació Paral·lela
Mòdul 7: Construcció d'Aplicacions
Mòdul 8: Bones Pràctiques i Patrons de Disseny
- Estàndards de Codificació i Bones Pràctiques
- Patrons de Disseny
- Injecció de Dependències i Inversió de Control
- Proves Unitàries
- Revisió i Refactorització de Codi
