C#/.NET Bellek Yönetimi ve Value Type, Reference Type Kavramları

Çağlar GÜL
Çağlar GÜL | Blog
7 min readJun 20, 2020

Programlama dilleri uygulama yürütme süreci esnasında verilerin adresleneceği RAM bellek bölgesini birkaç farklı bölgeye ayırarak kullanırlar. Uygulama çalışma zamanında (runtime) açılacak olan değişken ve nesneleri bu bölgelerde oluşturarak, sahip oldukları verileri adreslerindeki yerlerine yerleştirirler. Öncelikli olarak bu bellek bölgelerini kısaca tanıyalım;

RAM Bellek bölgeleri Nelerdir?

1. Stack Bölgesi

Konuya girmeden Stack Pointerlarla ilgili bilgi verelim.

Stack (Yığın) kavramı Bilgisayar bilimleri alanından gelen soyut bir veri yapısıdır. Öyle ki, temel amacı içine bi takım objeler saklamak olan bu yapının temel özelliği içine son koyduğunuz objeyi ilk geri almanızdır. Yani bir stack’iniz varsa iki işlem yapabilirsiniz: PUSH yani stack’e obje saklamak, ve POP yani stack’ten obje almak. Başka türlü bir erişim yoktur. Dolayısıyla demin söylediğimiz gibi stack’e arka arkaya a, b ve c objelerini push ederseniz, ardından pop ettiğinizde elinize c objesi gelir. Bir daha pop ederseniz b objesini alırsınız. Yani son giren ilk çıkar (last in first out : LIFO)

Stack yapısı daha sonraları CPU mimarilerinde kullanılmaya başlamıştır. Pek çok CPU bellekte bazı bölgeleri stack olarak kullanır. Bu amacla genelde CPU’ larda “stack pointer” adı verilen registerler olur.

Örneğin; 6510 mikroişlemcisinde $0100 — $01ff arası bölge stack olarak kullanılır. 6510'un içinde Stack Pointer (kısaca SP) adı verilen 8 bitlik bir register bulunur. Bu register ilk başta $ff değerindedir. yani stackin son adresini gösterir. Bu esnada stack boştur yani içinde saklanan hiç bir bayt yoktur. Eğer bir bayt push edilecek olursa stack bölgesinde (başka bir deyişle stack page’de) SP’nin gösterdiği adrese ($01ff) kopyalanır. Hemen ardından SP’nin değeri otomatik olarak bir azalır ve $fe olur. Böylece bir sonraki push komutunda push edilen bayt $01fe adresine kopyalanır.

Pop komutu geldiğinde ise SP bir artırılır ve böylece son push edilen baytı geri döndürür. Dikkat ederseniz, stack içine obje push edildikçe objeler page’in sonundan geriye doğru stack page’e kopyalanır ve bu esnada SP hep geriye doğru ilerler. Pop edildikçe de SP page sonuna doğru ilerler ve boylece stack boşalmış olur.

Özetleyecek olursak, Stack Pointer o anda çalışılan bellek bölgesinin adresini tutar. Tahsis işlemi için önceden ayrılacak bölgenin büyüklüğünü bilmesi gerekir. Buna göre arttırım veya azaltım yaparak veriyi uygun adreste bulur. Bilmesi gereken bu büyüklükler, .NET platformunun altyapısını oluşturan JIT derleyiciler tarafından sabit değerler ile tayin edilir.

Örneğin int tipi 32 bitlik olup 4 byte büyüklüğündedir. Bir diğer örnek long tipi 64 bit yani 8 byte, boolen tipi ise 1 bit için 1 byte yer tutar. Program yüklendiğinde okunulan değişken tanımına dair kodlarınızı bu sabit değerler eşliğinde hesaplayarak SP’nin pozisyonlarını doğru konumlandırması için bellek tahsisatını yapar.

Stack Bölgesinde Değer tipi (Value Type) dediğimiz veri tipleri (15 tane değer tipi vardır: int, byte, bool ve özellikle söylemek isterim ki struct vs.) barındılır.

Bu bölgedeki değişkenler, bulunduğu kapsamdan(Scope) çıktığı anda kendiliğinden yok edilirler. Yani Bellekte onlar için ayrılan yerler boşaltılırak yeniden kullanılabilir hale getirilir.

Heap bölgesi ile kıyaslandığında daha hızlı bir bölgedir. Yani değişkenlere erişim daha hızlıdır. Örnek vermek gerekirse; Masada, içinde belgeler olan bir sürü dosya olsun. Bu dosyalar masada çok yer kapladığı için masanın kullanılabilirliğini azaltır. Oysaki bu dosyaları masaya ait 4 çekmeceye bölerek yerleştirirsek ve hangi çekmecede olduklarını işaretleyen bir not kağıdı kullanırsak (Bu sayede daha sonra ihtiyaç duyduğumuz dosyayı not kağıdından öğrenip çekmeceden alabiliriz) masa boşalacak ve kullanılabilirlik artacaktır. Burada istemeden de olsa Heap bölgesinden bahsetmiş olduk ama bence anlamak için gayet güzel oldu. Bir de çok yer kaplamayan ve sürekli kullandığımız tek bir belge olsun. Bu belgeyi sürekli eğilip çekmeceden çıkarırsak ve tekrar yerine koyarsak bu bizi hem yorar hem de zaman kaybetmemize sebep olur. Onun yerine en mantıklı olanı nedir? Tabii ki bu belgeyi masanın üzerinde tutmak. Yani özetlersek; sürekli kullanılan değişkenler, küçük ama pratik kazandıran elemanlar Stack bölgesinde bulunur. Umarım anlaşılmıştır.

Bütün bunlar demek oluyor ki Değer tipi değişkenlerinden istediğiniz kadar tanımlayabilirsiniz. Çünkü işleri bittikten sonra mutlaka siliniyorlar. İstediğiniz kadar derken istisnai bir durumdan da bahsetmek gerekir. 256 mb’lık bir RAM’e 256 megabyte’a karşılık gelecek 33554432 tane long(8 byte kabul edilen sistemlerde) tipi değişken tanımlarsanız Stack bölgesi bir anda 256 mb’ı bu değişkenler için tahsis eder ve RAM tamamen dolmuş olur ki bu da Overflow Exception hatası verir. Bunu yapmak neredeyse imkansız tabii çünkü günümüzde RAM kapasiteleri bayağı artmış durumda.

2. Heap Bölgesi

Referans tipi (Reference Type) dediğimiz veri tiplerinin(Class, Interface, Delegate ve biraz daha farklı olan built-in(yerleşik) veri tipleri olan dynamic, object ve string) barındığı bölgeye de Heap bölgesi denir.

Bu bölgedeki nesneler, ileride anlatacağımız GC(Garbage Collector) sınıfı veya kodlayan kişi tarafından GC’yi beklemeden manuel bir şekilde bu bölgeden yok edilmeyene kadar asla silinmez. Bu bağlamda eğer nesneler için burada yer tahsis edilmeye devam ederse RAM yetersiz kaldığı anda Overflow Exception hatası alınır.

Heap bölgesinin kullanılması büyük esneklik kazandırır. Heap bölgesinden yer tahsisatı için “new” anahtarı kullanılır. Çalışma zamanında dinamik
olarak yaratılır. Derleme zamanında yapılmazlar.

Heap bölgesinin Stack’den farkı; heap bölgesinde tahsisatı yapılacak nesnenin derleyici tarafından bilinmesine gerek duymaz.

Yukarıda anlattığımız masa örneğindeki çekmeceler grubunu barındıran alan işte bu alandır. O dosyalar burada depolanır. O dosyalar birer nesnedir. Buradan çıkaracağımız sonuç, Heap bölgesi yalnız nesneleri depolamak için kullanılır. Bahsi geçen o örnekteki not kağıdı Stack bölgesinde, nesneler ise burada tutulur. Hemen şunu kavrayalım: nesneler için yer ayrıldığında Stack bölgesinde nesneye ait referans (Not kağıdı çekmeceleri gösteren bir referanstır) depolanır; Heap’te ise nesnenin kendisi depolanır.

Bunu örneklendirelim.

Personel adında bir class’ımızın olduğunu düşünelim.

class Personel
{
public string Ad;
public string Soyad;
}

Personel P1 = new Personel(); dediğimizde Stack bölgesinde P1 adında bir değişken ismi olacak ve içerdiği değer ise nesnenin adresi(referans) olacaktır. Mesela 0x00001 olsun bu adres.

+-------------+------------------+
| Stack | Heap |
+-------------+------------------+
| P1 =0x00001 | Personel Nesnesi |
| | (Ad soyad vs.) |
+-------------+------------------+

Değer tipi olsaydı sadece bu sefer şöyle olacaktı;

int a=5;+-------+------+
| Stack | Heap |
+-------+------+
| a = 5 | |
+-------+------+

Stack ile Heap bölgelerinin daha iyi anlaşılması için aşağıya bir kaç görsel bırakıyorum.

Heap bölgesini doğru kulanabilmek için ve Garbage Collector kullanımını iyi öğrenmemiz gerekiyor.

.NET de Garbage Collector sınıfına ait GC.Collect() methodu çalışma zamanı belirsiz bir şekilde çalışır ve RAM’de Heap bölgesinde başıboş (Stack bölgesinde ilgili nesnenin referansını gösteren herhangi bir değişken olmaması) bulunan nesneleri siler. Yani RAM’de nesne için ayrılmış alanı serbest bırakır. Eğer RAM’in şişmesi durumunda Garbage Collector hala gelmediyse “Memory Exception” hatasını alırız. Bu tip durumlarda Garbage Collector’ ü beklemek yerine manuel olarak biz çağırarak Heap’teki alanı serbest bırakabiliriz.

Bu işlemi yapabilmemiz için IDisposable interface’inden kalıtım alarak, classımıza Dispose methodu otomatik olarak eklenecektir. Bu methodun içini aşağıdaki gibi doldurarak nesneyi başarıyla yok edebiliriz.

IDisposable interface’nin örnek implemantasyonu aşağıdaki gibidir.

public class Personel : IDisposable
{
bool Disposed = false;

public string Ad;
public int Yas;
public Image Resim;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this); // Bu metot kullanıldıktan sonra örnek sınıf nesnesinin yok edilmesine yakın girilen fonksiyona girilmemesi için komut verir. Yani adından da anlaşılacağı gibi ona baskın olunur.
}
private void Dispose(bool Disposing)
{
if (Disposed)
return;
if (Disposing)
{
// Yönetimli kaynakların(Managed Resource) yok edileceği yer
Resim.Dispose(); // Buradaki Dispose metodu Resim adlı değişkende tutulan dosyanın RAM'de, kendisi için ayrıldığı yeri serbest(Release) bırakmak için kullanıldı.
}
// Yönetimsiz kaynakların(Unmanaged Resource) yok edileceği yer
/*
Yönetimsiz kodlara örnek;
Open files
Open network connections
Unmanaged memory
In XNA: vertex buffers, index buffers, textures, etc.
*/
Disposed = true;
}

~Personel() // Desctructor'lar bir sınıf nesnesinin yok edilmeden önce girildiği yerlerdir.
{
Dispose(false);
}
}

Garbage Collector’ün ne zaman geleceğini bilemediğimiz için bizden sonra veya bizden önce çalışırsa diye Disposed değişkeni kullanıldı. Yani bir kereden fazla yok etmek diye bir şey söz konusu olmadığı için bunu bu değişkenle sağlıyoruz. Eğer Disposed=true olursa bir daha “dispose” işlemlerini işletmeyeceğiz demektir kısaca.

Disposing parametresinin anlamı bu methodu kullanıcı mı yoksa sistem mi çağrımda bulunuyor, onu belirlemek. Eğer true ise yönetimli kaynaklar dispose edilecek. Yok eğer false ise zaten yönetimli kaynaklar dispose edilmiş biz sadece yönetimsiz kaynakları dispose ettireceğiz. Yönetimli kaynakların dispose edildiğini nereden anlarız derseniz, kodları incelediğinizde Disposing değişkenini sadece Destructor’dan false olarak geldiğini görürsünüz ki sınıf nesnesinin GC tarafından yok edilirken Destructor’a girdiğini biliyoruz. Yani GC zaten dispose etmiş, sadece haber veriyor size, bir şey yapacaksanız Destructor’ın içinde yapın diye.

Disposing değişkeni ne olursa olsun yönetimsiz kaynakların mutlaka yok edilmesi gerekir. Aksi takdirde program boyunca bellekte yer kaplamaya devam ederler.

Garbage Collector’ün kontrolü altında olan, silmesi gerektiği zaman bellekten silebildiği kaynaklara Yönetimli Kaynaklar(Managed Resource) denir.

Herhangi bir referans tipli değişkeni “Null” a eşitlediğinizde bu bellekten yok olmasını sağlamaz. Sadece referansı serbest bırakır. Yani artık o değişkenin referansını tuttuğu nesnenin adresini unutmasını sağlarsınız. Mesela şöyle olur: 0x00000. Bunu yapmanız artık o nesneyi kullanan kimsenin olmadığını bildirir. GC dolaşmaya çıktığı anda sahipsiz bu nesneyi toplar ve götürür.

Eğer sürekli kullanılan ama boyutu çok fazla olmayan sınıf yapıları yerine hemen yok olabilecek, daha hızlı erişilebilecek ve dolayısıyla performans artışı sağlayabilecek bir yapı kullanabilirsiniz. Struct lar bunlara örnektir.

3. Register Bölgesi

Stack ve Heap tahsisat mekanizmalarına göre çok hızlıdır. Sebebi mikroişlemcinin ikincil bellek bölgesinde bulunmasıdır. Mikroişlemcinin kaydedici (register) adını verdiğimiz bu bellek bölgelesine doğrudan erişim hakkımız yoktur. Tamamen altyapıda çalışan JIT (Just in Time) derleyicilerinin kontrolündedir.

4. Static Bölge

Bellekteki herhangi bir bölgeyi temsil eder. Static alanlarda saklanan veriler programın bütün çalışma sürecinde ayakta kalırlar. Anahtar “static sözcüğüdür.

5. Sabit Bölge

Sabit(constant) değerler genellikle program kodlarının içine gömülü şekildedir. Değerleri asla değişmezler. Sadece okuma amaçlıdır.

6. Ram Olmayan Bölge

Bellek bölgesini temsil etmeyen disk alanlarıdır. Kalıcı olması istenen verilerin saklandığı bölgedir.

Lütfen belleği hor kullanmayın; çalışması gereken diğer programları düşünün.

--

--

Çağlar GÜL
Çağlar GÜL | Blog

elektrik-elektonik mühendisi | yazılıma ve tasarıma meraklı | araştırmayı ve paylaşmayı seven | blogger ve oyun sevdalısı