Stack e Heap

stack heap

Per ogni applicazione eseguita sul sistema operativo, quest’ultimo assegna una quantità di RAM nella quale si trovano due zone distinte: gli stack e lo heap.

In particolare il SO assegna uno stack ad ogni thread dell’applicazione (viene quindi assegnato alla creazione del thread e eliminato al suo termine), mentre lo heap è unico (viene creato al lancio dell’applicazione e eliminato alla sua chiusura).

La dimensione dello heap viene decisa al lancio dell’applicazione ma può aumentare in base allo spazio necessario.

Dato che vi è un unico heap per tutti i thread dell’applicazione, questo deve essere thread-safe: ogni allocazione e deallocazione nello heap deve essere sincronizzata.

Dato un metodo da eseguire, nello stack verranno inseriti le variabili “passate per valore” che verranno eliminate al termine del metodo stesso, mentre nello heap le variabili “passate per riferimento” che invece rimarranno nella memoria anche al termine del metodo.

Queste ultime dovranno essere eliminate da un agente interno (Dispose manuale) o esterno (Garbage collector).

Per approfondire le variabili per valore o riferimento vedi Cosa vuol dire passaggio per valore o per riferimento?.

Lo stack è notevolmente più veloce di un heap in quanto il modello di accesso rende banale l’allocazione e la deallocazione della memoria da esso (un puntatore viene semplicemente incrementato o decrementato, vi è una operazione di push e pop che occupa un’istruzione macchina), mentre l’heap deve avere una gestione molto più complessa per l’allocazione o deallocazione che coinvolge il sistema operativo.

Stack

Lo stack è una area di archiviazione in RAM sequenziale, in particolare è una “pila” LIFO (Last in first out). Quando viene chiamata una funzione, un blocco è riservato nella parte superiore dello stack per le variabili locali e alcuni dati.

Quando quella funzione termina, il blocco diventa inutilizzato e può essere usato alla successiva chiamata di una funzione.

Questa allocazione lineare e sequenziale della memoria e viene utilizzata nell’allocazione della memoria statica (variabili passate per valore).

Dimensione fissa

La dimensione dello stack è fissa e decisa dal sistema operativo quando viene creato assegnandolo ad un thread. Se la dimensione dello stack supera il valore iniziale vi è un errore di stack overflow. Questa eccezione può avvenire, per esempio, nei casi di ricorsione infinita.

Deallocazione

La deallocazione avviene automaticamente quando le variabili vanno out of scope. Il programmatore non deve fare nulla.

Esempio

void barFunction( )
{
  // Crea una variabile "f" di tipo "Foo" nello stack. Viene settata a null.
  Foo f;

} // l'oggetto "f" viene distrutto automaticamente al termine della funzione

Heap

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.

Dimensione variabile

La dimensione dello Hep non è fissa come quella dello stack ma viene aggiunta dal SO in base alle esigenze dell’applicazione. Non avrò quindi mai problemi di overflow, al massimo rallentamenti dovuti allo swapping.

Deallocazione

La deallocazione delle variabili nell’heap deve essere gestita esplicitamente. In particolare 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 Perchè l’interfaccia IDisposable è così importante?.

Frammentazione

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.

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;

Articoli interessanti

Per approfondire stack, heap e altri concetti fondamentali consiglio questo bellissimo articolo: https://www.codeproject.com/Articles/76153/Six-important-NET-concepts-Stack-heap-value-types

Indice

Share
Ultimi articoli
Join

Newsletter

Nessuno spam, solo articoli interessanti ;)

Focus

Post correlati

semaphoreslim

SmaphoreSlim 101

SemaphoreSlim è una classe che permette la sincronizzazione di n thread che hanno una risorsa (scarsa) condivisa limitandone l’uso ad un numero massimo.

interlocked

Interlocked 101

La sincronizzazione dei thread è un elemento fondamentale nella programmazione asincrona, ne ho infatti parlato in vari post. La soluzione più versatile è sicuramente utilizzare

event

Come testare gli eventi

Testare che degli eventi siano stato effettivamente lanciati in C# non è immediato. Tipicamente è possibile testare che un evento venga lanciato aspettando un ManualResetEvent

Codice Pragmatico

Contatti

Per informazioni, dubbi o consulenze non esitate a contattarmi.

Lascia un messaggio

Ricevi le ultime news

Iscrivi alla newsletter

Solo articoli interessanti, promesso ;)