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

C#, VB.NET, ASP.NET Add comments

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.

6 Responses to “Bellek Yönetimi ve Çöp Toplama (Garbage Collection) – ÖZET”

  1. emin Says:

    daha geniÅŸ bilgiye sahip arkadaÅŸ varsa onu da ekleyebilir mi?

  2. atilla Says:

    Ü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 :/

  3. mahir talan Says:

    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…

  4. causality Says:

    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 ?

  5. Ahmet Kaymaz Says:

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

  6. erkan Says:

    çok güzel anlatmışsınız. teşekkürler.

Leave a Reply


9 × = 36

WP Theme & Icons by N.Design Studio
Entries RSS Comments RSS GiriÅŸ