Lo heap Γ¨ una struttura dati in RAM non sequenziale ad accesso casuale. Le variabili vengono istanziate in questa area e vi Γ¨ possibile accedervi tramite un puntatore.
A differenza dello Stack, non esiste alcun modello forzato per l'allocazione e la deallocazione dei blocchi dall'heap: Γ¨ possibile allocare un blocco in qualsiasi momento e liberarlo in qualsiasi momento. CiΓ² rende molto piΓΉ complesso tenere traccia di quali parti dellβheap sono allocate o libere in un dato momento.
Caratteristiche
- Allocazione dinamica: Lo heap permette di allocare e deallocare memoria in modo dinamico durante lβesecuzione del programma. A differenza dello stack, che ha una dimensione fissa e gestisce automaticamente la memoria per le variabili locali e i parametri delle funzioni, lo heap consente di allocare memoria in base alle esigenze del programma.
- Durata degli oggetti: Gli oggetti allocati nello heap hanno una durata che va oltre la durata delle funzioni che li creano. La memoria allocata nello heap rimane disponibile fino a quando non viene esplicitamente deallocata o fino a quando il programma termina. La deallocazione delle variabili nellβheap deve essere quindi gestita esplicitamente: in alcuni linguaggi deve essere effettuata manualmente chiamando dei comandi appositi come
free, delete, or delete[[]
. In altri linguaggi esiste il Garbage Collector che automaticamente elimina gli oggetti inutilizzati nello heap senza che il programmatore debba fare nulla. Per il Dispose in C# vedi Eliminazione di oggetti in .NET - Accesso tramite puntatori o riferimenti: Gli oggetti nello heap sono generalmente accessibili tramite puntatori o riferimenti. Questo consente di condividere e modificare i dati tra diverse funzioni e moduli del programma senza dover passare copie degli oggetti stessi. Tuttavia, lβuso di puntatori e riferimenti puΓ² anche rendere il codice piΓΉ complesso e aumentare il rischio di errori, come dereferenziazione di puntatori nulli o accesso a memoria non inizializzata.
- Dimensioni variabili: Lo heap puΓ² espandersi e contrarsi dinamicamente in base alle esigenze di memoria del programma. Questo permette di gestire strutture dati di dimensioni variabili e di adattarsi alle esigenze di memoria che cambiano nel tempo. Tuttavia, lβespansione e la contrazione dello heap possono portare a frammentazione e ridurre lβefficienza nella gestione della memoria. Non avrΓ² quindi mai problemi di overflow, al massimo rallentamenti dovuti allo swapping.
- Condivisione tra thread: In ambienti multithreading, lo heap Γ¨ condiviso tra tutti i thread del processo. Questo permette ai thread di condividere e scambiare dati facilmente, ma richiede anche lβuso di meccanismi di sincronizzazione per prevenire problemi di concorrenza, come condizioni di gara e incoerenza dei dati.
Vantaggi
- Durata della memoria: Le variabili allocate nello heap persistono per tutta la durata del programma, a meno che non vengano liberate esplicitamente. CiΓ² permette di utilizzare variabili con una durata piΓΉ lunga rispetto a quelle dello stack, che vengono automaticamente eliminate al termine della funzione.
- Dimensioni: Lo heap ha spesso una dimensione molto maggiore rispetto allo stack, consentendo lβallocazione di strutture dati di grandi dimensioni che potrebbero non essere gestibili nello stack.
- FlessibilitΓ : Lβallocazione dinamica della memoria nello heap permette di creare e ridimensionare strutture dati come array e oggetti in modo piΓΉ flessibile, a differenza dello stack, che richiede che le dimensioni delle variabili siano conosciute al momento della compilazione.
- Condivisione dei dati: PoichΓ© le variabili nello heap sono accessibili da qualsiasi parte del programma, possono essere facilmente condivise tra diverse funzioni e thread.
- Allocazione esplicita: Lβallocazione e la liberazione della memoria nello heap avvengono in modo esplicito, il che consente al programmatore di avere un maggiore controllo sulla gestione della memoria.
Limiti
-
- Overhead di gestione della memoria: Lβallocazione e la deallocazione della memoria nello heap richiedono tempo e risorse aggiuntive, poichΓ© il sistema deve gestire la complessitΓ delle operazioni, come la ricerca di blocchi di memoria liberi, la loro combinazione e la loro divisione. Questo overhead puΓ² influire negativamente sulle prestazioni del programma, specialmente se si effettuano frequenti operazioni di allocazione e deallocazione.
- Frammentazione della memoria: Questo problema avviene quando la memoria disponibile nello heap Γ¨ gestita tramite blocchi discontinui, in particolari blocchi utilizzati sono inframezzati da blocchi inutilizzati. Quando vi Γ¨ eccessiva frammentazione puΓ² risultare impossibile allocare nuova memoria in quanto, anche se potenzialmente avrei memoria utilizzabile, questa non Γ¨ contigua.
- Problemi di sincronizzazione: Nei programmi multithread, lβaccesso e la manipolazione condivisa dello heap possono causare problemi di sincronizzazione e condizioni di gara. Per evitare questi problemi, Γ¨ necessario utilizzare meccanismi di sincronizzazione, come semafori o mutex, che possono aggiungere ulteriore complessitΓ e sovraccarico al programma.
- Gestione manuale della memoria: In alcuni linguaggi di programmazione, come C e C++, Γ¨ necessario gestire manualmente lβallocazione e la deallocazione della memoria nello heap. Questo puΓ² portare a errori umani, come dimenticare di liberare la memoria allocata, causando perdite di memoria (memory leak), oppure liberare la memoria piΓΉ volte, portando a comportamenti indefiniti e potenzialmente crash del programma. Anche in linguaggi come il C# dove Γ¨ presente il Garbage Collector comunque Γ¨ necessario porre attenzione alla memoria allocata, sopratutto di quella degli oggetti
IDisposable
. - Tempo di accesso: Lβaccesso alla memoria nello heap Γ¨ generalmente piΓΉ lento rispetto allβaccesso alla memoria nello stack, poichΓ© lβindirizzo di memoria degli oggetti allocati nello heap puΓ² essere meno prevedibile. Inoltre, le operazioni di allocazione e deallocazione nello heap sono piΓΉ complesse e richiedono piΓΉ tempo rispetto alle operazioni nello stack. Questo puΓ² influire sulle prestazioni del programma, in particolare se si effettuano molte operazioni su dati allocati nello heap.
Tipologie in .NET
In .NET ho due aree separate virtuali dello heap che vengono utilizzate per gestire in modo piΓΉ efficiente lβallocazione e la raccolta dei diversi tipi di oggetti sulla base delle loro dimensioni. Queste vengono utilizzare allβimplementazione del Garbage Collector di .NET e sono quindi presenti solo in tale ambiente.
Small Object Heap
Lo Small Object Heap Γ¨ progettato per contenere oggetti di dimensioni ridotte. Nellβambito del garbage collector di .NET, gli oggetti di dimensioni inferiori a 85.000 byte vengono allocati nello SOH. Il SOH Γ¨ organizzato in generazioni (0, 1 e 2) per ottimizzare la raccolta dei rifiuti e ridurre lβoverhead della scansione degli oggetti.
Questi vengono compattati durante la Compacting Phase del Collect
.
Large Object Heap
Il Large Object Heap Γ¨ destinato a contenere oggetti di dimensioni maggiori, tipicamente oggetti di dimensioni superiori a 85.000 byte. A differenza dello SOH, il LOH non Γ¨ organizzato in generazioni. Il motivo di questa separazione Γ¨ che gli oggetti di grandi dimensioni tendono ad avere un costo di allocazione e deallocazione piΓΉ elevato e possono causare una maggiore frammentazione dello heap. Raggruppando gli oggetti di grandi dimensioni nel LOH, il garbage collector puΓ² gestire in modo piΓΉ efficiente lβallocazione e la raccolta di questi oggetti. Il LOH viene raccolto meno frequentemente rispetto allo SOH, in quanto si presume che gli oggetti di grandi dimensioni abbiano una durata piΓΉ lunga. Tuttavia, quando il LOH viene raccolto, il processo Γ¨ piΓΉ dispendioso in termini di tempo e risorse, poichΓ© coinvolge la scansione e la raccolta di oggetti di grandi dimensioni.
Esempio
void barFunction( )
{
// Viene creato un puntatore nello stack ("f") che punta ad un nuovo oggetto che verrΓ creato nello heap
Foo\* f = new Foo( ) ;
// Sono al termine del metodo ma, dato che "m" Γ¨ nello heap, nessuno lo elminerΓ . Qualora stia utilizzando linguaggi senza GC come C++ devo eliminare l'oggetto manualmente con il domando "delete", altrimenti incorrerΓ² in un memory leak. Qualora invece utilizzi linguaggi con il GC come C# o Java il Dispose verrΓ effettuato automaticamente, a meno di oggetti IDisposable che rendono necessario il dispose manuale allo stesso modo.
// Se non c'Γ¨ il GC
delete m;
// Se c'Γ¨ il GC ma oggetto IDisposable
m.Dispose()
}
Esempio complessivo
Di seguito un esempio che racchiude tutti i concetti descritti sopra, in C.
int foo()
{
char *fooPointerArray; // Non viene allocato nulla a meno del puntatore che viene allocato nello stack
bool boolean = true; // La variabile b viene allocata nello stack.
if(boolean)
{
// Alloca 100 byte nello stack
char fooArray[100];
// Alloca 100 byte nello heap
fooPointerArray = new char[100];
}//<-- la variabile fooArray Γ¨ deallocata qui, al contrario di fooPointerArray
}//<--- Senza un delete[] fooPointerArray ho un memory leak