Garbage Collection ve Go (Golang)

Sayfayı kopyala
💡 Özet (TL;DR):
- Garbage Collection (GC) Nedir?: Programın artık ihtiyaç duymadığı heap belleğindeki nesneleri otomatik olarak tespit edip temizleyen arka plan mekanizmasıdır.
- Go GC Stratejisi: Go, bellek tahsisatı (allocation) ve GOGC eşik değerlerini izleyerek çöp toplamayı otomatik başlatır. Yerel değişkenlerin ömrünü belirlemek için Kaçış Analizi (Escape Analysis) kullanır.
- Kritik Fark: Değişkenlerin kapsamını (scope) dar tutmak, nesnelerin ömrünü netleştirir ve GC'nin belleği hızlıca geri kazanmasını sağlar. Global değişkenler ise her zaman GC kökü (root) olarak kaldığından bellek yönetimini zorlaştırır.
Türkçe karşılığı "Çöp Toplama" olan Garbage Collection, özetle artık kullanılmayan hafıza alanının boşaltılması anlamına gelir. Programlamada çöpleri kimin toplayacağı daha çok kullandığınız programlama diliyle ilişkilidir; fakat artık çoğu modern dilde hafıza yönetimiyle doğrudan uğraşmadan, bu süreci dilin çalışma zamanına (runtime) bırakabiliyoruz. Programlama dilinin bu desteği sağlaması daha az kod yazmamızı sağlarken olası hataların da önüne geçiyor.
Garbage Collection sürecinin nasıl çalıştığını anlarsak, bu sürecin daha verimli ve daha doğru işlemesini sağlayabiliriz. Bu yazıda Go Garbage Collection sürecinin nasıl işlediğini inceleyerek daha verimli bellek yönetimi için ipuçları elde etmiş olacağız.
Go Bellek Yönetimi: Stack vs. Heap
| Özellik | Stack (Yığın) | Heap (Yığın Kümesi) |
|---|---|---|
| Yönetim | CPU tarafından otomatik yönetilir (LIFO mantığı) | Garbage Collector (GC) tarafından yönetilir |
| Hız | Son derece hızlıdır | Stack'e göre daha yavaş çalışır (GC tarama yükü) |
| Değişken Ömrü | Fonksiyon sonlandığında otomatik temizlenir | Kökten (root) hiçbir referans kalmayana kadar yaşar |
| Kullanım | Kısa ömürlü, boyutu derleme anında bilinen değişkenler | Dinamik boyutlu, fonksiyon dışına kaçan (escape) nesneler |
💡 Bellek Alanları Üzerine Kısa Bir Not: Çalışan bir program nesneleri iki farklı hafıza bölümünde depolar; bunlardan biri heap, diğeri ise stack olarak adlandırılır. Garbage Collection'ın işi stack'le değil, heap iledir. Stack, son giren ilk çıkar (LIFO) mantığına dayanan ve fonksiyon değerlerini saklayan bir veri yapısıdır. Bir fonksiyonun içinden başka bir fonksiyonu çağırmak, stack'te o fonksiyonun değerlerini saklayan yeni bir çerçeve (frame) açar.
Hata veren bir programı veya betiği debug yaptıysanız stack yapısına aşina olmalısınız. Birçok programlama dilinin derleyicisi debug amaçlı bir call stack ağacı görüntüler. Öte yandan heap, fonksiyonların dışında referans verilen veya dinamik olarak oluşturulan nesneleri saklar. Programın başında tanımlanan statik sabitler veya Go struct'ları gibi daha karmaşık nesneler buna örnektir. Heap alanına kaydedilmesi gereken bir nesne tanımladığınızda gereken hafıza ayrılır ve bir pointer geri döndürülür. Heap temizlenmezse program çalıştıkça bellek tüketimi büyümeye devam eder.
Bellek Durumunu İzleyen Örnek Kod
Hafızayı biraz dolduracak ve bize bellek durumunu gösterecek kısa bir Go kodu yazalım:
package main
import (
"fmt"
"runtime"
"time"
)
func printStats(memory runtime.MemStats) {
runtime.ReadMemStats(&memory)
fmt.Println("Memory Allocation :", memory.Alloc)
fmt.Println("Memory Total Allocation :", memory.TotalAlloc)
fmt.Println("Memory Heap Allocation :", memory.HeapAlloc)
fmt.Println("Memory NumGC :", memory.NumGC)
fmt.Println("**********************************")
fmt.Print("\n")
}
func main() {
fmt.Print("\n")
var memory runtime.MemStats
fmt.Println("************* START **************")
printStats(memory)
var s []byte
for i := 0; i < 10; i++ {
s = make([]byte, 52428800)
if s == nil {
fmt.Println("Operation failed!")
}
}
fmt.Println("********* ALLOCATE 50M ***********")
printStats(memory)
for i := 0; i < 10; i++ {
s = make([]byte, 104587600)
if s == nil {
fmt.Println("Operation failed!")
}
}
time.Sleep(3 * time.Second)
fmt.Println("********* ALLOCATE 100 M *********")
printStats(memory)
fmt.Printf("%T\n", s)
}
Bu kodda hafızanın durumunu görüntülemek için Go standart kütüphanesinde yer alan runtime paketini kullandık.
runtime.ReadMemStats metodunu kullanarak bellek durumuyla ilgili istatistikleri elde edip bunları ekrana basan bir fonksiyonumuz var. Main fonksiyonumuz içerisinde sırasıyla şunları yapıyoruz:
- Önce mevcut hafıza durumunu ekrana basıyoruz.
- Ardından bir
fordöngüsü içerisindesisimli bir değişkene 10 defa 50 MB değerinde bir byte dilimi (slice) atıyoruz. Aslında her seferindesdeğişkeninin üzerine yazmış oluyoruz. - Ardından aynı döngüyü 100 MB ile tekrarlıyoruz.
Programı çalıştırdığımızda şöyle bir çıktı elde ederiz:

Birinci çıktıda toplam ayrılmış bellek 154.472 byte iken, ikinci çıktıda 50 MB'ın üzerinde olduğunu görüyoruz. Anlık ayrılan hafıza 50 MB civarında doluyken, atama işlemi 10 defa yapıldığı için toplam 500 MB bellek ayrılmıştır. Her yeni değer atama işleminde hafızada yeni bir alan ayrılıp s değişkeni bu alan (pointer) ile ilişkilendirilir. Bir önceki hafıza pointer'ının ilişkili olduğu hiçbir nesne kalmadığından Garbage Collector o alanı temizler. Sonuç olarak GC çalıştı, gereksiz hafıza alanlarını boşalttı ve sadece gereken hafıza alanı saklanmaya devam etti.
Değişken Kapsamını (Scope) Daraltmak
Şimdi kodun sadece main fonksiyonu içerisinde ufak bir değişiklik yapalım. Fonksiyonun başındaki var s []byte tanımını kaldıralım ve döngünün içini s := haline getirelim. Böylece değişkeni for döngüsünün içinde tanımlamış olacağız.
package main
import (
"fmt"
"runtime"
"time"
)
func printStats(memory runtime.MemStats) {
runtime.ReadMemStats(&memory)
fmt.Println("Memory Allocation :", memory.Alloc)
fmt.Println("Memory Total Allocation :", memory.TotalAlloc)
fmt.Println("Memory Heap Allocation :", memory.HeapAlloc)
fmt.Println("Memory NumGC :", memory.NumGC)
fmt.Println("**********************************")
fmt.Print("\n")
}
func main() {
fmt.Print("\n")
var memory runtime.MemStats
fmt.Println("************* START **************")
printStats(memory)
for i := 0; i < 10; i++ {
s := make([]byte, 52428800)
if s == nil {
fmt.Println("Operation failed!")
}
}
fmt.Println("********* ALLOCATE 50M ***********")
printStats(memory)
for i := 0; i < 10; i++ {
s := make([]byte, 104587600)
if s == nil {
fmt.Println("Operation failed!")
}
}
time.Sleep(3 * time.Second)
fmt.Println("********* ALLOCATE 100 M *********")
printStats(memory)
}
Yeni kodu çalıştırdığımızda şu sonucu alırız:

Benzer miktarda toplam ayrılmış hafıza değerleri görüyoruz, fakat anlık ayrılan hafıza başlangıçtaki sıfır değerine yakındır. s değişkenini for döngüsünün içinde tanımladığımız ve döngüden çıkıldığında artık bu değişkene erişim kalmadığı için Go runtime'ı onu çöp toplama sürecine soktu ve hafızayı boşalttı. Yani Go, değişkenlerin kapsamlarını (scope) dikkate alarak nesnelerin ömrünü hesaplar ve artık kullanılmayacaksa hafıza alanını boşaltır.
Özyinelemeli (recursive) fonksiyonlar olmadıkça, fonksiyonlardaki yerel değişkenlerin kapladıkları yer Go için büyük bir sorun teşkil etmeyecektir.
Global Değişken Tanımlamanın GC Yükü
Şimdi kodumuzu biraz daha değiştirelim. Bu sefer for döngüsü içerisindeki yerel değişken tanımlamasını kaldıralım ve bu değişkeni en yukarıda paket seviyesinde (global) tanımlayalım:
package main
import (
"fmt"
"runtime"
"time"
)
var s []byte
func printStats(memory runtime.MemStats) {
runtime.ReadMemStats(&memory)
fmt.Println("Memory Allocation :", memory.Alloc)
fmt.Println("Memory Total Allocation :", memory.TotalAlloc)
fmt.Println("Memory Heap Allocation :", memory.HeapAlloc)
fmt.Println("Memory NumGC :", memory.NumGC)
fmt.Println("**********************************")
fmt.Print("\n")
}
func main() {
fmt.Print("\n")
var memory runtime.MemStats
fmt.Println("************* START **************")
printStats(memory)
for i := 0; i < 10; i++ {
s = make([]byte, 52428800)
if s == nil {
fmt.Println("Operation failed!")
}
}
fmt.Println("********* ALLOCATE 50M ***********")
printStats(memory)
for i := 0; i < 10; i++ {
s = make([]byte, 104587600)
if s == nil {
fmt.Println("Operation failed!")
}
}
time.Sleep(3 * time.Second)
fmt.Println("********* ALLOCATE 100 M *********")
printStats(memory)
}

Global değişken tanımladığımızda çöp toplayıcının daha az çalıştığını ve anlık bellek kullanımının (Memory Allocation) ilk durumun yaklaşık 3 katı olduğunu görürüz.
Neden Böyle Oldu? (Kaçış Analizi)
Go derleyicisi, derleme aşamasında hangi değişkenlerin stack alanında kalacağını, hangilerinin heap alanına taşınacağını belirlemek için Kaçış Analizi (Escape Analysis) yapar.
Bir değişkeni global (paket düzeyinde) tanımladığımızda, o değişken uygulamanın ömrü boyunca hayatta kalır. Bu durum onu çöp toplayıcı için kalıcı bir kök referans (GC Root) haline getirir. Global değişkene atanan değer ezilse dahi, Go GC'si (GOGC ayarları gereği) bu tür global referansları tarayıp temizleme konusunda yerel değişkenlere göre çok daha temkinli davranır ve belleği hemen iade etmeyebilir.
Kıssadan Hisse
Garbage Collector'ün işini düzgün yapabilmesi için değişkenlerimizin ömrünü gereksiz yere uzatmamalı ve işleri bittiğinde kapsam (scope) dışına çıkmalarını sağlamalıyız. Çöp toplayıcının verimli çalışması için saatlerce ekstra kod yazmamıza gerek yoktur; değişken sınırlarını olabildiğince dar tutarsak gerisini Go bizim yerimize halledecektir.
Bu Yazıda Yapılan Değişiklikler
- 21.06.2026: Global değişken kullanımında GC'nin davranış farkı "Kaçış Analizi (Escape Analysis)" ve "GC Kökleri (GC Roots)" kavramlarıyla teknik olarak açıklandı. İmla hataları (STACK'da -> stack'te, stacke'e -> stack'e vb.) ve klavye hataları (aurılan -> ayrılan, basıyorurz -> basıyoruz vb.) giderildi. Kapak fotoğrafı telif satırları kaldırıldı. Karşılaştırma tablosu ve özet bloğu eklendi.
- 11.05.2022: Yazı özeti düzenlendi.
