Bellek Yönetimi ve Çöp Toplama (Garbage Collection) – ÖZET

Program yazmada dikkat edilmesi gereken konulardan biri belleği en uygun şekilde kullanmaktır. Özellikle kurumsal uygulamalarda kullanılacak programların, kaynakları ekonomik ve ergonomik kullanması her zaman tercih nedeni olmuştur. Bir programcı başta bellek olmak üzere sistem kaynaklarını daima idareli kullanmalı ve meşgul ettiği alanı işi bittikten sonra serbest bırakmalıdır!
Yüksek düzeyli dillerinde sınıflardan ürettiğimiz nesneleri, onlarla işimiz bittiğinde serbest bırakmamız gerekiyor. Fakat bu süreçte her zaman bu kurala uyulamadığı için programlardan istenmeyen durumlar oluşabilmektedir;
. Bazı programcılar kullandıkları nesneleri silmedikleri için bellek sarfiyeti veya memory leak olarak tanımlanan bellek sızıntısı problemi yaşanıyordu.
. Bazı programcılar da, nesneleri iki defa silmeye çalıştıkları için programda hatalar oluşuyordu.
. Başka bir programcı grubu da daha önce serbest bırakılmış nesneye erişmeye kalkışıyordu.
Bellek yönetimi için geliştirilen çöp toplama aracı (Garbage Collection – GC), yeni nesnelere yer açmak (allocate) içi bellekteki uygun alanları serbest bırakan (deallocate) düzenektir. GC sistemi, uzun süre erişilmeyen, kullanılmayan nesneleri bellekten kaldırır, oluşturulan yeni nesneler için heap üzerinde yer açar. Daha önceki Microsoft uygulamalarında, bir nesneyi bellekten temizlemek için Nothing yöntemini kullanıyorduk. Bu yöntemde serbest bırakılacak nesne Nothing değerine eşitlenir. Fakat çoğu zaman nesneyi Nothing olarak sabitlemeyi unutmamız, yanlış nesneyi sonlandırmamız veya birbirine bağlı kaynakları hangi sırayla serbest bırakacağımızı kolayca yönetemeyişimiz, program içerisinde istenmeyen durumlarda neden olabilmekteydi. İş bu noktada, .NET Framework’ün sunduğu çöp toplama aracı, programlarımızda kontrol etmekte zorluk çektiğimiz bu bellek sızıntılarını bizim yerimize otomatik olarak yok etmektedir (automatic object lifetime management). GC, her zaman hem de maliyet açısından çöplerimizi bir başkasının toplaması gerektiğinin bir sonucu olarak ortaya çıkmıştır.
Garbage Collection mekanizmasının algoritmasını iyi anlamanın yolu bellek birimlerini tanımaktan geçer. Bir programın çalışmaya başlama ve devam etme sürecinde CPU ve RAM cephesinde bazı operasyonlar gerçekleşir. Program belleğe yüklendiğinde belleğin segment adı verilen üç farklı alanına yayılır:
. Code Segment (Programın kod alanı)
. Stack (Yığın)
. Heap
Text Segment olarak ta bilinen Code Segment, programa ait makine kodunu yani programın CPU’ya göndereceği komutları tutar. Geri kalan stack ve heap alanında (Data Segment) ise programın kullandığı veri ve kaynaklar tutulur. Bellekteki bu verilere ulaşmak için CPU tarafında register denen lokasyonlar kullanılır. İşlemci çekirdeğine gömülü özel bellek birimi olarak düşünebileceğimiz register alanları, genel olarak matematiksel işlemler ve bellekteki verilere ulaşma işlemlerini gerçekleştirir. Konumuzla ilgili olanlar, Code Segment (CS) ve Data Segment (DS) registerleridir. Bir program yüklendiği zaman işletim sistemi, programdaki komut ve değişkenleri belleğe yükler ve ilgili segmentlerin adreslerini bahsi geçen registerlere aktararak programın ilk komutu ile işlemi programa bırakır.
Bu bölümde daha çok işletim sisteminin programımız için bellek (Random Access Memory – RAM) üzerinde ayırdığı yığın (stack) ve heap alanlarını inceleyeceğiz;
. Yığın (Stack) alanı, işlemci tarafından verilerin geçici olarak saklandığı veya uygulamanın kullandığı değişkenlerin tutulduğu ve büyüklüğü işletim sistemine göre değişen bellek bölgesidir. Yığının büyüklüğü, yönetimi, programcının değil uygulamanın sorumluluğundadır. Derleyici veya çalıştırma ortamı, program içindeki değişkenleri ve uzunluklarını tarar ve yığın üzerinde ona göre ayırım yaparak yerleştirir. Bu yüzden derleyici, programı oluşturmadan önce yığın üzerinde oluşturulacak verilerin boyut ve ömürlerini bilmek zorundadır. Bir değişkenin hangi bellek bölgesine kaydedileceğini CPU üzerindeki Stack Pointer (SP) denen register belirler. SP, yığın alanının en üst kısmının yani belleğin o anki uygun yerinin adresini gösterir. Depolanacak verilerin eklenmesi veya silinmesiyle SP değeri bir azaltılır veya artırılır.
Bu bellek biriminin yığın olarak anılması, LIFO (Son giren ilk çıkar, Last-In First-Out) ilkesine göre çalışmasından kaynaklanmaktadır. CPU tarafından verilerin yığın alanına konulması, Push, alandan alınması, Pop olarak tanımlanır. Bu ekleme ve çıkarma işlemlerinde bellek adresleri değişmez bunun yerine yığın işaretçisi (stack pointer), aşağı yukarı hareket ettirilerek ilgili veriye erişilir veya yeni veri eklenir.
Yığın, dinamik değişkenleri saklamanın yanında, yordam çağrıları yaparken, geri dönüş adresini saklamak, yerel değişkenleri depolamak, yordamlara parametre yollamak için de kullanılır.
Yığın bellek alanı, program çalışmaya başladığı anda belirlenir ve daha sonra bu alanın boyutu değiştirilemez. Bu alan, işletim sistemi tarafından genellikle kısıtlı şekilde belirlendiği için yığın üzerindeki yoğu ekleme işlemlerinde hafıza birimi taşması sorunu yaşanabilmektedir (stack overflow).
. Heap alanı ise işletim sistemi tarafından programcının yönetimine bırakılmış daha geniş alanlı ve kalıcı bir alandır. Genellikle ne kadar yer kaplayacağı belli olmayan değişkenlerin (nesnelerin) veya büyük verilerin geçici olarak saklanması için kullanılır. Yığın bölgesinin tersine çalıştırma ortamı (CLR), heap alanında ne kadar yer kullanılacağını bilmek zorunda değildir. New anahtar sözcüğüyle bir nesne oluşturduğumuzda CLR, bu nesne için heap üzerinde yer tahsis eder. İşimiz bitince heap üzerindeki nesnelerimizi sildiğimizde heap biriminin o bölgesini boşaltmış oluruz. Böylece yeni değişkenleri o bölgede saklayabiliriz. Heap alanı, program, bellek üzerinde açık olduğu sürece yaşamaya devam eder. Bu yüzden genel (global) ve statik (static) değişkenler bu alan içinde tutulur. Heap alanının okunması veya yazması yığın alanı kadar hızlı değildir.
Kısacası, stack, programda tanımlanan sabit uzunluklu değişkenler için tahsis edilmiş bellek birimidir. Heap ise programın kullanımı için tahsis edilmiş genel amaçlı bellek birimidir.

Bu alanlar üzerinde ne tür değişkenler saklayabiliriz onu inceleyelim. Uygulama geliştirirken iki farklı değişken türü kullanırız; Değer Türleri (Value Types) ve Başvuru Türleri (Reference Types).
Değer Türleri, doğrudan, belirlediğimiz veriyi tutan değişkenlerdir. Veri ile birlikte anlam kazanan değişkenler olduğu için değer türleri, hiçbir zaman boş (null) olamazlar. Bu türden değişkenler tanımlandığında, CLR tarafından, belleğin yığın alanı üzerinde o türün kapladığı byte kadar alan ayrılır ve çalışma esnasında yerleri değiştirilmez. Ayrıca başka bir değişkeni de etkileyemezler.

int X = 2005;

şeklinde tanımladığımız “X” değişkeni için yığın üzerinde 4 byte (32 bit) alan ayrılır.
Başvuru Türleri ise doğrudan veriyi değil verinin atanacağı nesneyi veya nesnenin işaretçisini belirtir. Referans türleri, değer türlerinden farklı olarak boş olabilir ve belleğin heap alanı üzerinde tutulurlar. Bu değişkenlerin kapladıkları alan sabit olmayıp programcı tarafından değiştirilebilir. Bu yüzden dinamik değişken olarak ta tanımlanırlar. Başvuru türündeki değişkenler, o türdeki adresleriyle bilinirler. Bu adresleri tutan göstergelere işaretçi (pointer) denilir. Programcı, işaretçi aracılığıyla bu nesnelere erişir ve onları yönetir. İşaretçiler, yığın üzerinde saklanır, temsil ettikleri nesneler ise heap üzerinde saklanırlar. .NET Framework’te bu heap adreslerine erişilemez. Kitap içerisidne detaylı işleyeceğimiz Sınıf (Class), Arabirim (Interface), Temsilcisi (Delegate) değişkenleri başvuru türüne örnek gösterilebilir.
.NET Framework ortamında yeni bir nesne, yeni bir başvuru türü new (MSIL dilindeki newobj komutuna karşılık) operatörüyle oluşturulur.

SqlConnection myConnection;
myConnection = new SqlConnection ();

SqlConnection türünde myConnection isimli bir değişken tanımladık. Aynı zamanda alt satırda bunu bir nesneyle ilişkilendireceğimiz için yığın üzerinde bir işaretçi tanımlamış olduk. SqlConnection türündeki bu işaretçiyi SqlConnection türündeki bir nesneye bağladık. Bundan böyle bu nesne, myConnection isimli işaretçiyle temsil edilecektir. Dolayısıyla myConnection, normal bir değişkenden farklı olarak bir nesneyle bağlantılı bir değişkendir. “new” operatörü, nesne için heap üzerinde alan ayırıp bu alanın adresini döndürerek ilgili işaretçiye aktarır. Bu nesnenin heap üzerinde nereye kaydedileceğini belirleyemeyiz. Buna çalışma ortamı karar verir.
Şimdi, bir başvuru türü olan myConnection değişkeni ile yukarıda tanımlanmış değer türü olan X değişkeni arasındaki farkı daha net görebiliriz. X, Integer türünde bir değişken olup 2005 değerini barındırır, myConnection ise nesnenin kendisini değil nesneyi işaret eden göstergeci barındırır.
Ayrıca görüldüğü gibi bir referans değeri tek başına bir şey ifade etmiyor, onu, new anahtarını kullanarak bir nesneyle ilişkilendirmek gerekir. Framework ortamında, referans değişkenini nesneyle ilişkilendirmeden kullanmaya çalıştığımızda “Object reference not set to an instance of an object.” hatasını alırız.
Bir sınıfı kullanmak istediğimizde yani new operatörüyle kendisinden bir örnek (instance) oluşturduğumuzda otomatik olarak constructor (yapıcı) olarak tanımlanan sınıfa ait başlangıç yordamı çağrılmış olur. Aynı şekilde bu nesne, yok edilirken de (dispose işlemi) arka tarafta destructor (yıkıcı) olarak bilinen sonlandırıcı yordamı tetiklenir (destruction işlemi). Destructor işleminin, .NET Framework’teki karşılığı Finalize() yordamıdır. Finalize(), çöp toplayacısının, bir nesneyi yok etmeden önce yapmasını istediğimiz işlemlerin tanımlandığı yordamdır. Bu yüzden sadece GC tarafından çöp temizleme esnasında otomatik olarak çağrılır, programcı tarafından doğrudan çağrılamaz. Programcı, bu yordamı çağırmak için Dispose() yordamını kullanır.
Değişkenleri, genel olarak bu şekilde belleğe yüklemiş oluruz, peki geri yüklenmeyi nasıl yapacağız yani yüklenmiş kaynakları nasıl serbest bırkacağız. Bu işlemi programcı kendisi yapabildiği gibi çöp toplayıcısına da bırakabilir. Konumuz GC olduğu için ikinci seçenek üzerinde duracağız.
GC’nin çalışma mantığı, program içindeki değişkenlerin veya nesnelerin durumlarını takip edip ona göre davranış sergilemekten ibarettir. Günümüzde birkaç GC algoritması kullanılıyor. Bu algoritmalarda önemli olan değişkenlerin veya nesnelerin ne zaman silinmeye hazır olduklarının bilinmesidir.
Değer türleri, yani yığın tabanlı değişkenler geçerli oldukları alandan (scope) çıktıkları zaman silinmeye hazır birer parça haline gelmiş olur.

public void BirseylerYap ()
	{		//Geçerlilik alanı başlangıcı
	int X = 125;
	}		//Geçerlilik alanı bitişi. Sonraki satırlarda X değişkenine ulaşılamaz.

Nesneler ise kendilerine başvuru olmadığı zaman silinmeye hazır değişkenler haline gelir.
Son paragraflarda anlattıklarımızı şekil üzerinde anlatmamız, çalışma ortamında neden bellek yönetiminin gerekli olduğunu daha açık gösterecektir. Yazdığımız programda Main() yordamı içerisinden aşağıdaki fonksiyonu çağırdığımızı varsayalım;

	public int KareAl (int Sayi)
	{
    int Sonuc;
    Sonuc = Sayi*Sayi;
    return Sonuc;
	}// KareAl metodunun sonu

Program yüklenmeye başladığında Main() yordamı içerisindeki tüm değişken ve yordamlar sırayla yığın üzerine taşınır. Bu aşamada KareAl() yordamı ve yordamın yerel parametresi olan Sayi değişkeni ardışıl şekilde yığın birimi üzerine taşınır.

CLR, KareAl() fonksiyonunun bulunduğu yığın alanına konumlanır ve Sayi parametresini set eder. KareAl() fonksiyonu çalışırken içeride Sonuc isimli değişkenin yaratıldığı görülür. Bu değişken için de yığın üzerinde yer ayrılır.

Fonksiyonun çalışması bittikten sonra yani bu fonksiyonu çağıran koda geri dönüldüğünde yığın alanında bu fonksiyon için açılmış tüm alanlar temizlenir.

Burada Sonuc değişkeni, yığın alanı üzerinde oluşturuldu. Bir yordam içerisinde tanımlanmış değer türleri, otomatik olarak yığın alanı üzerinde depolanır ancak bazen heap alanı üzerinde de oluşturulabilir. Aşağıdaki kodları inceleyelim;

public class Islem
{
    public int Sonuc;
}	//Islem

public Islem KareAl (int Sayi)
{
    Islem oIslem = new Islem ();
    oIslem.Sonuc = Sayi * Sayi;
    return oIslem;
}	// KareAl

Yine aynı şekilde KareAl() fonksionu çağrıldığı zaman yığın alanına taşınmış olur.

Fonksiyon içindeki oIslem değişkeni bir referans türüne işaret ettiği için kendisi yığın üzerinde, nesnenin kopyası da heap üzerinde depolanır.

Görüldüğü gibi Sonuc değişkeni, bir değer türü olduğu halde heap üzerinde tanımlandı.
Önceki durumda olduğu gibi fonksiyonun işlenmesi bittikten sonra, geçerlilik alanından çıkılma esasından dolayı yığın’teki değişkenler temizlenecek ve heap alanındaki Islem nesnesinin yığın ile bağlantısı kesilmiş olacaktır.

Heap üzerindeki bu bellek alanına programcı tarafından da ulaşılamayacaktır. Bu durumda, GC’nin devreye girmesi beklenir. GC, çalıştığı zaman heap üzerinde yığın ile bağlantısı kesilmiş nesneleri belirleyip onları bellekten kaldırır.
Program içerisinde, bir yordamdan başka bir yordamı geçildiği zaman ikinci metodun çalışması bittikten sonra programın kaldığı yerden devam edebilmesi için yığın üzerinde bir geri dönüş işaretçisi oluşturulur. Bu geri dönüş işaretçisi, fonksiyon çağrıları yapılırken geri dönüş adresini saklamak için kullanılır
Daha önce C, Pascal, C++ dillerinde tanımladığımız pointerlere karşılık .NET’te “Başvuru Türleri” kullanılır.

Bellek Yönetimi ve Çöp Toplama (Garbage Collection) – ÖZET” hakkında 8 yorum

  1. atilla

    Ülkemiz sınırları içerisinde şu konuyu ayrıntılı bir şekilde anlatan arkadaş görürsem ellerinden öpeceğim! .Net Clr Belleği performans grafikleriyle test edip koyacak işin özünü anlatacak birisi olsada okusak :/

    Cevapla
  2. mahir talan

    sabrın sonu selametmiş. sabredin bakalım biri çıkar belki. bakın adam yazmış elinden geleni. kim bilir belki siz ögrenir daha güzel bişeyler yazarsınız.
    hadi hepimize kolay gelsin.

    Cevapla
  3. causality

    Garbage Collector’ün toparladığı nesnelerin listesini alabileceğimiz bir yol var mı ?
    Örneğin uygulamada 17 adet obje hafızada diyebiliyor muyuz ? Bu objeleri daha sonra tiplerini alarak 3 adet integer 5 adet string 2 adet form objesi var diyebilir miyiz ?

    Cevapla
  4. Ahmet Kaymaz Yazar

    Doğrudan bu listeyi sunan bir yordam olduğunu sanmıyorum ancak Memory Profiler gibi bir araç kullanılırsa bu bilgiye erişilebilir.

    Cevapla
  5. Erkan

    Gercekten guzel bir anlatim olmus. Ama bir taraftan da atilla isimli arkadasin soyledigine katilmamak elde degil. Bence programlamada verimliligin saglanmasi dendiginde akla ilk gelmesi gereken konu hafiza yonetimi. Ama su konunun tam manasiyla aciklandigi turkce bir makale yok. Sizin yazinizda da verdiginiz ‘GC yapiyor zaten bosverin elle yonetimi’ hissiyati yerine, bir bu kadar guzel dispose ve finally uygulamalarini ve inceliklerini de gorseydik. Ne gerek var diyenlere;
    Bu gun yaptigim bir denemede uygulamamin bir bolumunde islemin 100 tekrari sonrasi, normalde 20MB lik RAM kullaniminin 140 MB a cikisini gordum. Uygulamayi kapatana kadar da degisiklik gostermedi. Test yazilimina gore GC bu sure zarsinda ondan fazla devreye girdi ama nafile. Mekanizmayi tum yonleriyle ogrenmek onemli.
    Her halukarda benim icin faydali bir yaziydi. Tesekkurler

    Cevapla

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir