Kapak Görseli Tima Miroshnichenko

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 geliyor. Programlamada çöpleri kimin toplayacağı daha çok kullandığınız programlama diliyle ilişki fakat artık çoğu programlama dilinde hafıza yönetimi ile uğraşmadan, bunu programla dilinin runtime’ına bırakabiliyoruz. Programlama dilinin bu desteği sağlaması daha az kod yazmamızı sağlarken olası bug’larında önüne geçiyor.

Garbage Collection sürecinin nasıl çalıştığını anlarsak sürecin daha verimli ve hatta daha doğru işlemesini sağlayabiliriz. Bu yazıda GoLang Garbage Collection süreci nasıl işliyor inceleyerek daha verimli hafıza yönetimi için ipuçları elde etmiş olacağız.

Ç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’la değil, HEAP iledir. Stack son giren ilk çıkar 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’da o fonksiyonun değerlerini saklayan yeni bir çerçeve açar ve bu böyle gider. Hata veren bir program veya betiği debug yaptıysanız stacke’e aşina olmalısınız. Bir çok programlama dilinin derleyicisi debug amaçlı bir stack ağacı görüntüler. Öte yandan heap fonksiyonların dışında referans verilen değerleri saklar. Programın başında tanımlanan statik sabitler (constants) veya Go struct’ları gibi daha kompleks nesneler buna örnektir. HEAP’e 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 büyümeye devam eder.

An overview of memory management in Go by Scott Gangemi

Şimdi sizinle hafızayı biraz dolduracak ve bize durumunu gösterecek kısa bir kod 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", s)
}

Bu kodda hafızanın durumunu görüntülemek için go standart kütüphaneleri içinde gelen runtime’ı kullandık.

Temel olarak runtime kütüphanesinin ReadMemStats metodunu kullanarak hafıza durumu ile ilgili istatistikleri elde edip bunları ekrana basan bir fonksiyonumuz var. Bu fonksiyonun işlevine bir kaç defa ihtiyacımız olacağı için bir fonksiyon olarak tanımladık. Main fonksiyonumuz içerisinde sırasıyla şunları yapıyoruz

  • Önce mevcut hafıza durumunu ekrana basıyorurz
  • Ardından bir for döngüsü içerisinde s isimli bir değişkene 10 defa 52.428.800 byte (50MB) değerinde bir değişken atıyoruz. (aslında her seferinde s’in üzerine yazmış oluyoruz – aşağıda tekrar değineceğim)
  • Ardından aynı döngüyü 104.587.600 byte (100MB) ile tekrarlıyoruz.

Ben programı çalıştırdığımda şöyle bir çıktı aldım

Birinci çıktıda toplam ayrılmış bellek 154.472 byte iken, ikinci çıktıda 50MB’ın üzerinde olduğunu görüyoruz. Anlık aurılan hafıza 50M civarında doluyken, atama işlemi 10 defa yapıldığı için toplam 500M bellek ayrılmış. Her yeni değer atama işleminde hafızada yeni bir alan ayrılıp S değişkeni bu alan (pointer) ile ilişkilendiriliyor ve haliyle bir önceki hafıza pointer’ının ilişkili olduğu hiç bir şey kalmadığından Garbage Collector’ümüz o alanı temizliyor. Sonuç olarak garbage collector 10 defa çalıştı, gereksiz hafıza alanlarını boşalttı ve sadece gereken hafıza alanı saklanmaya devam etti.İkinci döngü çalışıp 100M’lık bir değer atadığımızda da benzer sürecin çalıştığını görüyoruz.

Şimdi kodun sadece main fonksiyonu içerisinde çok ufak bir değişiklik yapacağım.

Fonksiyonun başındaki var s = []byte tanımını kaldırdım ve for döngüsünün içindeki s = yerine s:= haline getirdim. (Değişkeni for döngüsünün içinde tanımlamış oldum). Artık for döngülerinin dışında bir s değişkeni tanımlı olmadığı için en sondaki fmt.Printf(“%T”,s) satırını da kaldırdım. ( Zaten bu satırı kullanılmayan değişken hatası vermemesi için mecburen koymuştum )

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ığımda şu sonucu aldım

Gördüğünüz gibi yine 10 defa çalışan Garbage Collector ve 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ğerinde. “s” değişkenini for’un içinde tanımladığımız ve for döngüsünden çıkıldığında artık kullanılmadığı için Go onu Garbage Collection sürecine soktu ve artık işe yaramadığı için hafızayı boşalttı. Yani Go Garbage Collection sürecinde fonksiyon ve döngü scope’larını da dikkate alarak ilgili değerin yaşam sürecini (lifetime) hesaplıyor ve artık işe yaramayacaksa ilgili hafıza alanını boşaltıyor.

Yani çağırdığınız bir fonksiyon içerisinde yerel olarak tanımlanmış değişkenlerde aynı süreçten geçiyor. Ön yinelemeli (Recursive) fonksiyonlar olmadıkça fonksiyonlardaki yerel değişkenlerin kapladıkları yer GO için çok sorun teşkil etmeyecek.

Şimdi kodumuzu biraz daha değiştirelim;

Bu sefer for döngüsü içerisindeki değişken tanımlamasını tekrar kaldırdım, fakat değişken tanım işini en yukarıda, global olarak yaptım. Bakalım hafıza kullanımımız nasıl değişecek.

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)
}

Nasıl yani? Bu yaptığımızın ilk durumdan ne farkı var? İkisinde de s değişkeni önceden tanımlı ve for döngüsü içinde sadece yeni değer ataması yapıyoruz? Neden Garbage Collector daha az çalıştı, neden anlık ayrılan hafıza ilk durumun 3 katı?

Bir değişkeni global olarak tanımladığımızda Garbage Collector yaşam süresi hakkında bir ön görüde bulunamıyor ve bu tip değerleri daha nadir temizlemeyi tercih ediyor. Basit olarak yaşam süresi en kısa olanlar en çok, en uzun olanlar en az temizleniyor şeklinde düşünebiliriz.

Kıssadan hisse;

Garbage Collector’ün işini düzgün yapabilmesi için değişkenlerimizin ömrünü gereksiz uzun yapmamalı ve işimiz bittiğinde öldürmeliyiz. Garbage Collector’ün düzgün çalışması için saatlerce ekstra kod yazmamıza gerek yok, bazı ufak detaylara dikkat edersek gerisini o bizim yerimize halledecektir.

Bu Yazıda Yapılan Değişiklikler
  • 11.05.2022: Yazı özeti düzenlendi.