Cos'è la garbage collection?
Il meccanismo con cui un runtime libera la memoria non più raggiungibile, e le sue pause che pesano sul p99.
La garbage collection è il meccanismo con cui un runtime gestito, come quello della JVM, di Go, di .NET o di Python, libera automaticamente la memoria occupata da oggetti che il programma non può più raggiungere, senza che lo sviluppatore chiami esplicitamente una funzione di deallocazione. Il punto che confonde più spesso è la differenza fra raggiungibilità e utilità: il raccoglitore non decide cosa "serve ancora" al programma, decide soltanto cosa è ancora referenziato da qualche parte raggiungibile dalle radici. Un oggetto enorme può restare vivo per mesi solo perché una lista dimenticata lo referenzia: questo è un memory leak anche in un linguaggio con garbage collection. Le famiglie in una riga ciascuna: reference counting libera un oggetto appena il contatore tocca zero, ma non risolve i cicli da solo; mark-and-sweep percorre il grafo dei raggiungibili e libera il resto; gli approcci generazionali separano gli oggetti giovani da quelli longevi; i raccoglitori concorrenti spostano gran parte del lavoro su thread paralleli all'applicazione.
Le pause contano più del throughput medio
Il punto che davvero sposta una decisione architetturale non è quanta memoria il raccoglitore libera in media, è per quanto tempo ferma il programma per farlo. Una pausa da 200 millisecondi non si vede in una media calcolata su migliaia di richieste veloci, ma si vede per intero nella coda della latenza: finisce dritta nel p99, cioè proprio nella metrica che un SLO difende. Un sistema di observability ben strumentato mostra la firma tipica: latenza piatta interrotta da picchi periodici che coincidono esattamente con i cicli di raccolta, un pattern che un dashboard di soli tempi medi nasconde del tutto. Da qui il trade-off vero, e le tre grandezze non si massimizzano insieme: throughput del programma (quanto lavoro utile fa per secondo, al netto del tempo speso a raccogliere), latenza delle pause (quanto a lungo, e quanto spesso, il programma si ferma) e occupazione di memoria (quanto heap serve per raccogliere di rado invece che di continuo). Un servizio interattivo che rispetta il proprio SLO solo in media non lo rispetta affatto.
Un esempio enterprise
Un servizio di pricing in tempo reale gira in container con un limite di memoria imposto dal cgroup di Kubernetes, per esempio due gigabyte. Il runtime non sempre lo sa: la JVM e .NET leggono il limite del cgroup per default e dimensionano l'heap di conseguenza, mentre il runtime di Go non lo fa: senza un GOMEMLIMIT esplicito la sua soglia di raccolta è solo un multiplo dell'heap vivo, e non esiste alcun tetto di memoria di cui il raccoglitore tenga conto. Il risultato non è una raccolta più aggressiva ma una raccolta che non accelera affatto man mano che il limite si avvicina: il processo continua ad allocare oltre la soglia reale, e il kernel invoca l'OOM killer, che termina il container in un istante, senza log applicativi, senza uno stack trace, senza che il raccoglitore abbia mai avuto la possibilità di intervenire. Dai dashboard sembra un crash casuale; è invece un raccoglitore che non ha mai visto il vincolo reale sotto cui girava. La correzione non è aggiungere memoria al container, è dichiarare al runtime il limite sotto cui gira. Ridurre le pause è un obiettivo distinto, e si affronta con la scelta del raccoglitore, non con il limite di memoria.
Perché conta per chi decide
La garbage collection è tornata un tema architetturale, non un dettaglio da manuale universitario, proprio a causa dei container: un runtime pensato per una macchina fisica dedicata gira oggi dentro limiti di memoria stretti e condivisi. Chi decide l'architettura di un servizio a bassa latenza deve trattare la configurazione del garbage collector come parte del contratto di prestazione, non come un parametro di tuning da sistemare dopo il primo incidente: un p99 gonfiato dalle pause di raccolta non si vede finché qualcuno non lo va a cercare con lo strumento giusto.
Termini correlati
- Observability · La capacità di capire cosa succede dentro un sistema in produzione da log, metriche e tracce, senza doverci indovinare sopra.
- SLI, SLO e SLA · La catena che misura un servizio (SLI), gli dà una soglia interna (SLO) e ne fa un impegno contrattuale (SLA).
- Backpressure · Il meccanismo con cui un sistema segnala a monte di non riuscire a tenere il ritmo, invece di collassare in silenzio.
- Performance profiling · La misura di dove un programma spende tempo e memoria durante l'esecuzione, per decidere cosa ottimizzare davvero.
- Throughput vs latenza · Throughput misura quanto lavoro passa nel sistema, latenza quanto ci mette ogni singola richiesta.
Un termine che ti riguarda da vicino? Parliamone.
CONTATTAMI