Stack & Heap
Quando viene eseguito un programma, il computer carica il codice da eseguire in memoria, quindi vengono salvati i dati e le istruzioni che verranno eseguite dal computer. Lo stack e l'heap sono due aree di memoria che vengono utilizzate in modo differente e sono fondamentali per la corretta gestione della memoria durante l'esecuzione di un programma.
Quando un programma richiede di istanziare una variabile, viene occupato dello spazio in memoria e, a seconda del tipo e del momento in cui viene istanziato, viene salvato nello stack o nell'heap.
Attraverso il debugger, è possibile visualizzare i valori delle variabili salvate nella memoria. Consentono di fermare l'esecuzione di un programma in un determinato punto e di esaminare lo stato dell'heap e dello stack.
Stack
Lo stack è l'area di memoria statica che viene allocata temporaneamente quando viene eseguito il programma.
Funzionamento
Lo stack in memoria funziona come una pila: i dati vengono inseriti e rimossi dallo stack in modo LIFO e il programma in esecuzione può accedere ai frame presenti nello stack solo in cima alla pila, ovvero all'ultimo frame inserito. I frame vengono quindi inseriti e rimossi in modo sequenziale e viene gestito dal sistema operativo in automatico: non bisogna allocare o liberare la memoria manualmente.
Ogni suddivisione (frame) nello stack ha un indirizzo assoluto, ogni dato salvato all'interno di un frame ha un indirizzo locale, che rappresenta la posizione della variabile rispetto all'inzio del frame, con cui il programma accede al dato. Per determinare l'indirizzo assoluto di un dato, è necessario conoscere l'indirizzo base dello stack e l'offset (la posizione relativa all'indirizzo base dello stack).
Per la gestione dello stack sono riservati 2 registri: ESP indirizzo dell'ultimo frame dello stack, su cui il programma lavora, e EBP indirizzo base dello stack.
Dati salvati
Quando è necessario salvare nuovi dati nello stack, viene creato un record di attivazione.
Il record di attivazione (o stack frame) è uno spazio nello stack che viene creato e rimosso ogni volta che è necessario salvare nuovi i dati.
Di conseguenza, la struttura dello stack è la seguente:
Stack
Nei stack frame sono salvati:
Funzioni: variabili locali e indirizzo di ritorno
Quando una funzione viene chiamata, il programma salva lo stato corrente del contesto di esecuzione, come i registri della CPU, nello stack della funzione chiamante.
Viene creato un nuovo frame nello stack per la funzione chiamata (push). Questo frame contiene le variabili locali statiche, i parametri della funzione e l'indirizzo di ritorno1.
Il programma quindi lavora all'interno della funzione utilizzando l'ultimo frame inserito nello stack, come in una pila.
Se la funzione chiama altre funzioni, il processo si ripete, aggiungendo allo stack nuovi frame per ogni funzione chiamata.
Quando una funzione termina, il suo frame viene rimosso dallo stack (pop) e il frame precedente nella pila diventa l'ultimo, ovvero il frame della funzione chiamante, su cui il programma lavorerà e continuerà l'esecuzione.
1: l'indirizzo di ritorno permette di ritornare al punto precedente alla chiamata della funzione e continuare l'esecuzione del codice.
Blocchi di codice
Le variabili locali nei blocchi di codice come if, while e for vengono salvati nello stack, ma non hanno un record di attivazione proprio: condividono il record di attivazione funzione chiamante, con la differenza che vengono allocate e rimosse quando il blocco stesso finisce.
Vantaggi
- Velocità: grazie alla struttura LIFO come una pila, l'accesso ai dati è molto veloce.
- Gestione automatica delle variabili locali e chiamate di funzione: l'allocazione e la deallocazione di variabili locali e funzioni avviene in modo automatico.
Svantaggi
- Dimensione limitata: la dimensione dello stack in memoria è limitata e predefinita dal sistema operativo. Se il programma richiede più memoria di quella disponibile nello stack, può verificarsi un errore "overflow dello stack".
- Utilizzo inefficiente della memoria: costringe ad allocare spazio per tutte le variabili del codice, anche se solo una piccola parte di queste sono utilizzate.
Inoltre, può verificarsi un sottoutilizzo della memoria, dato che i blocchi di memoria allocati sono più grandi di quelli necessari per i dati contenuti. - Non permette l'utilizzo di strutture dati dinamiche: come la lista, pila o coda.
- Inefficienza nella ricorsione: per ogni chiamata ricorsiva viene creato un frame per la funzione nello stack con una copia delle variabili locali.
Simulazione stack
public class Example{
public static void main(String[] args){
int n;
n = 6;
if(true){
int half;
half = foo(n);
System.out.println(half);
}
}
public static int foo(int x){
int h;
h = x/2;
return h;
}
}
Stack
[
]
bottom
Heap
L'heap è l'area di memoria dinamica che viene riservata per allocare dati di dimensioni variabili durante l'esecuzione.
Funzionamento
La memoria heap è organizzata in blocchi di dimensioni variabili. Quando un programma deve allocare dei dati nell'heap, il sistema operativo assegna un blocco di memoria libero di dimensioni adeguate. Ogni dato viene identificato da un indirizzo assoluto e il programma può accedere a questi dati solo tramite un puntatore, che indica l'indirizzo di memoria del blocco.
La memoria heap viene allocata e gestita dal sistema operativo, che fornisce un'interfaccia per la gestione della memoria allocazione e deallocazione di blocchi di memoria: per esempio, in C e C++ sono malloc() per l'allocazione e free() per la deallocazione, mentre in Java la gestione piò essere effettuata automaticamente dal garbage collector.
Dati salvati
Oggetti
Gli oggetti richiedono dimensioni di memoria variabili e vengono quindi allocati dinamicamente nell'heap: per istanziare una classe in Java o C++, quest'ultima viene allocata dinamicamente nell'heap.
Nota: vengono allocati nell'heap i dati degli oggetti, come gli attributi, e non i dati dei metodi chiamati, che vengono allocati nello stack.
Array di dimensioni variabili
Gli array di dimensioni variabili non possono essere dichiarati staticamente durante la compilazione, quindi vengono allocati dinamicamente durante il run-time nell'heap.
Strutture dati dinamiche
Le strutture dati dinamiche, come le liste, le code, gli alberi, ecc..., sono allocate nella memoria heap.
Vantaggi
- Flessibilità: permette di allocare e deallocare blocchi di memoria dinamicamente, di dimensioni variabili in base alle esigenze del programma.
- Persistenza: La memoria allocata persiste fino a quando non viene deallocata manualmente, anche dopo la fine dello scope di variabili locali. Dunque, può essere accessibile globalmente, cioè da diverse parti del programma, a differenza della memoria dello stack che può essere accessibile solo all'interno della funzione corrente.
- Dimensione illimitata: la dimensione dell'heap è illimitata e dipende dalla quantità di memoria fisica disponibile.
- Utilizzo efficiente della memoria: permette di allocare spazio solo per le variabili utilizzate dal programma.
- Permette l'utilizzo di strutture dati dinamiche.
Svantaggi
- Gestione manuale della memoria (non applicabile a Java1): l'allocazione e deallocazione della memoria nell'heap devono essere gestite manualmente dal programmatore. Questo può essere fonte di errori come la memoria non deallocata (memory leak) o l'accesso a dati già deallocati (dangling pointer).
- Prestazioni: l'allocazione e deallocazione della memoria nell'heap richiedono un tempo di esecuzione maggiore rispetto allo stack.
- Fragmentation: l'allocazione dinamica della memoria può causare la frammentazione dell'heap, cioè la creazione di spazi liberi troppo piccoli per ospitare nuovi dati, anche se la memoria totale libera sarebbe sufficiente.
1: in Java è possibile liberare la memoria heap manualmente o attraverso un meccanismo automatico di garbage collection: l'heap viene liberata automaticamente dai dati che non sono più in uso, eliminando gli oggetti che non sono più utilizzati. Quindi in base al comportamento del garbage collector l'uso dell'heap può cambiare nel tempo.
Simulazione heap
import java.util.*;
public class Example{
public static void main(String[] args){
Random random = new Random();
int[] vet = new int[random.nextInt(10)];
String str = "esempio";
}
}
Heap