Merhaba, bu yazımda sizlere bellek yapısından bahsetmek istiyorum. Ayrıca, belleği elle yönetmenizi sağlayan C++ dilinden birkaç örnek vereceğim.
Öncelikle belleğin ne olduğundan bahsetmek istiyorum. Bellek, kelime anlamı olarak verilerin saklandığı yer demektir. Biz, rastgele erişimli bellekten (RAM) bahsedeceğiz. RAM, kullanılan tek bellek türü değildir. RAM dışında salt okunabilir bellek (ROM) ve silinebilir programlanabilir salt okunabilir bellek (EPROM) gibi devrelerde kullanılan bellek türleri de vardır. RAM'lerin bilgisayarlardaki kullanımı daha çok bilindiğinden RAM'ler üzerinden gideceğiz.
Yığın bellek yönetimi ise dilden dile değişkenlik gösterebilmektedir. Bazı diller, derleyicileri ve kapasiteleri sayesinde yığın belleklere erişimi kısıtlarken bazı diller serbest bırakabilir. Örneğin C# ve Java dillerinde yığın bellek yönetimi yazılımcıya bırakılmaz. Yazılımcı, referans tipleri kullanarak işini görür. C++ dilinde ise yığın belleği istediğiniz gibi kullanabilirsiniz.
Referans tipte olan değişkenler ise basitçe sınıflardır. Sınıflar, kendi içerisinde birden fazla referans tipi veya değer tipini barındırabilir. Örneğin string denilen yazı tipi harflerden (char) oluşur. Stringin kendisi bir referans tip iken oluştuğu harf, değer tipidir. Referans tipte bir değişken oluşturduğunuz zaman tip, önce yığın bellekte blok şeklinde oluşturulur. Daha sonra bu bloğun adresi sıralı bellekteki ayrılmış alana kaydedilir. Yani siz bir sınıf oluşturup değişkene atadığınız zaman aslında değişkende tutulan şey yığın bellekteki adresidir. Başka bir değişken oluşturup bu önceden oluşturduğunuz değişkene eşitlediğiniz zaman değerler eşitlendiği için aslında yığın bellekteki bloğun adreslerini eşitlemiş olursunuz. Birinci değişkenden sınıfa erişip bir şeyi değiştirdiğiniz zaman yığın bellekteki değer değiştiği için ikinci değişkenden veriyi çektiğiniz zaman değiştirdiğiniz değere ulaşırsınız.
İlk değişkeni oluşturduğunuz başka bir değişkene eşitleyip değiştirdiğiniz zaman ikinci değişkenden bu değişikliği göremezsiniz çünkü artık ilk değişkenin tuttuğu şey bloğun adresi değil, atadığınız değişkenin değeridir. Yani iki değişkenin ortak bir tarafı kalmamıştır.
Yığın bellek yönetiminin serbest olduğu dillerde ise referans tip diye bir şey yoktur. Tüm oluşturulan değişkenler sıralı bellekte oluşturulur. İster sınıf olsun ister int ister struct olsun hepsi sıralı bellekten ayrılan yerde depolanır. Yığın belleği kullanmak istediğiniz zaman dilin kendi metodlarını kullanarak yığın bellekte alan ayırıp bu alanı kullanırsınız. C++ için konuşacak olursak malloc (memory allocation, bellek paylaştırma) metodu ile bu alanı ayırabilirsiniz. İstediğiniz büyüklüğü girip yığın bellekte bir blok oluşturup kullanabilirsiniz.
Daha sonra oluşturduğumuz Test sınıfını oluşturalım ve bellekte nasıl bulunduğuna bakalım. Ek olarak x değişkeni oluşturmak istedim daha iyi anlaşılması için.
Görüldüğü üzere Test sınıfı oluşturulduğunda aslında art arda 2 adet int değişken oluşturulmuş oluyor. y değişkeninde tutulan ise aslında Test sınıfının içerdiği ilk değişkenin (y.a yani a değişkeni) adresi, başka bir deyişle y değişkeninin başlangıcı. Siz y.a dediğiniz zaman ilk önce y değişkenine yani başlangıç noktası olan a değişkeninin adresine ulaşırsınız, oradan da a değişkenine ulaşmak için 0 kere sayılır ve neticesinde adrese ulaşırsınız. y.b dediğiniz zaman ise ilk önce yine y değişkenine erişirsiniz, daha sonra 1 kere sayıp b değişkenine ulaşırsınız. Sonuç olarak sınıf içindeki değişkenler art arda oluşturulmuştur ve sayarak diğer elemanlara erişilebilir.
Yığın belleğe geçmeden önce size işaretçileri anlatmam gerekiyor. Önceden de bahsettiğim gibi aslında işaretçiler bellekte tutulan verilerin adresleridir aslında. Önceki verdiğim 2 bellek resminden farklı olarak bir de adreslerin de işin içine dahil edildiği bir tane göstermek istiyorum.
Görselde gördüğünüz üzere bir yukarıda verilen görselden farklı olarak adreslerini de belirttim. a değişkeni oluştuğu zaman ayrılan alanın adresine yani pointerına yani işaretçisine ptra diyelim. Daha sonra C++ dilinde işaretçileri kullanmak için kullanılan * ve & belirteçlerini kullanarak a'nın işaretçisini değişkene atayalım. * belirteci türün yanına gelerek değişken tipinin o türün işaretçisi olduğunu belirtir. int* demek int türünde bir değişkenin işaretçisidir, yani tipi int işaretçisidir. Bu oluşturduğumuz b değişkenine de &a yani a değişkeninin işaretçisini atayalım. Böylece bellekte yeni bir alan ayrılır ve değerine a'nın adresi atanır. Tabii bu da bir değişken olduğu için onun da bir adresi olacak, bu adrese de ptrb diyelim. Böylece işaretçilerin oluşturuluşunu görmüş olduk. Peki bu ne işimize yarayacak?
b değişkenini kullanarak a değişkenini değiştirebiliriz. Bunun için sayma yöntemini kullanalım. C++'ta saymak için [] operatörünü kullanıyoruz. Köşeli parantezler içine kaç saymak istediğimizi yazıyoruz. Örneğin b[2] dersek b işaretçisinden başla 2 kere say anlamına gelir. Artık b değişkeninin 2 ilerisinde hangi değer tanımlanmışsa bize onu döndürür. Bunu eğer bir tanımlamadıysak ki yukarıdaki örneğe göre sadece 2 değişkenimiz var, bize çöp bir veri dönderecektir. Hani biz değişken oluştururken bellekte alan ayırmıştık ya, işte o ayırdığımız alanı geçip bellekte var olan diğer verilere erişmiş oluruz. Yani ne geleceği belli değildir. Eğer b[0] dersek b'den başlayıp 0 kere sayarız, yani a'nın adresine ulaşırız. Sonuç olarak b[0]'ı kullanarak a değişkenini etkileyebiliriz.
Eğer işaretçi kullanmadan yapsaydık ise durum farklı olurdu.
Gördüğünüz gibi b değişkenini değiştirdik ama a değişkeni etkilenmedi çünkü aralarında bir bağ yok. Sadece değerleri aynıydı, onu da zaten değiştirdik.
Sıralı bellek kısaca bu şekilde gösterilebilir. Yığın bellek yönetimi için ise yine Test sınıfını kullanacağız ama bu sefer sınıfı yığın bellekte oluşturacağız. malloc metodu bizden boyut ister, boşluk türünde işaretçi döndürür.
İlk önce sıralı bellekte Test sınıfını oluşturduk, a ve b değişkenlerine değer verdik. Sonra malloc metodundan Test sınıfının boyutu kadar yığın bellekte bir alan açmasını istedik ve malloc metodu da yığın bellekte istediğimiz alanı ayırıp bu alanın adresini bize dönderdi, biz de Test işaretçisi türünde bir değişken oluşturup bu alanın adresini oraya atadık. Böylece artık hpx değişkeni yığın bellekteki Test sınıfının adresini tutuyor. hpx[0] ile yığın bellekteki boş ayırdığımız alana önceden sıralı bellekte oluşturduğumuz Test sınıfını atadık ve böylece yığın bellekte bir sınıf tutmuş olduk. Tabii ki bu kullanım çoğunlukla tercih edilmiyor, sonuç olarak hem yığın hem de sıralı bellekte obje oluşturmuş oluyoruz. Bunun yerine new kullanılıyor fakat ben mantığını göstermek istedim.
Daha sonra oluşturduğumuz bu sınıfı oluşturup değişkene atayalım.
Burada ek bir işleme gerek kalmadan Test sınıfını oluşturup değerlerini atadık, oluşturduğumuz sınıf direkt olarak yığın bellekte oluştu ve adresi x değişkeninde tutuluyor. y değişkenini oluşturup x'e eşitlediğimiz zaman da bu adres aynı zamanda da y değişkeninin de değeri oldu. Artık y değişkenini değiştirdiğimiz zaman yığın bellekteki değer değişeceğinden x değişkeni de otomatik olarak değişmiş olacak.
Bellek açığı, yığın bellekte gerçekleşen sinir bozucu bir olaydır. Sebebi ise artık kullanılmayan objelerin bu bellekten temizlenmediği için birikmesidir. Birikme sonucu bellek ağırlaşır ve açık oluşur. Bu, hatalara ve ciddi performans kayıplarına yol açabilir. Çöp toplama, bir obje artık kullanılmadığı zaman yığın bellekten temizlenmesi ve bu ayrılan alanın serbest bırakılmasına denir. C# bunu otomatik olarak yaparken C++'ta bunu kendiniz yaparsınız ama korkmayın, basit bir işlem. Yığın bellekte oluşturduğunuz bir objeye artık ihtiyacınız kalmadığında onu silmek için delete sözcüğünü kullanabilirsiniz. Bu, ayırdığınız alanı serbest bırakarak belleği temizlemenizi sağlayacaktır. Örnek olarak yukarıdaki örnekte bulunan hpx'i temizlemek istersek delete hpx yazabiliriz. Böylece bellekte açık oluşmasını önlemiş oluruz.
İyi forumlar
Öncelikle belleğin ne olduğundan bahsetmek istiyorum. Bellek, kelime anlamı olarak verilerin saklandığı yer demektir. Biz, rastgele erişimli bellekten (RAM) bahsedeceğiz. RAM, kullanılan tek bellek türü değildir. RAM dışında salt okunabilir bellek (ROM) ve silinebilir programlanabilir salt okunabilir bellek (EPROM) gibi devrelerde kullanılan bellek türleri de vardır. RAM'lerin bilgisayarlardaki kullanımı daha çok bilindiğinden RAM'ler üzerinden gideceğiz.
Rastgele Erişimli Bellek (RAM)
Normal depolama modüllerine göre çok daha hızlı olduğundan dolayı programların geçici verilerinin depolanması için kullanılır. Mobil cihazlardan bilgisayarlara kadar pek çok kullanım alanına sahiptir. Yazılan programların hepsi çalışabilmek için RAM'i kullanır. Oluşturulan tüm değişkenler ve metodlar burada tutulur. Değişkene erişmek veya değiştirmek istendiğinde RAM sayesinde işlem çok hızlı bir şekilde gerçekleştirilir. Veriler, RAM üzerinde bulunan birden fazla fiziksel depolama modülünde tutulur. Bu modüllerin hepsi eş zamanlı olarak kullanılabilir.Belleğin Sanal Yapısı
Veriler bellekte dosya depolanır gibi değil, stack (sıralı yığın) veya heap (yığın) olarak ikiye ayrılmış bölümlerde depolanır. Sıralı bellekte adı üstünde veriler sıralı şekilde depolanırken yığın bellekte bloklar şeklinde depolanır. Veriler, sıralı bellekte art arda depolandığından aradan bir veriyi silmek mümkün değildir. Yeni gelen veriler üste eklenir, üstten silinir. Aradan bir veri silinemeyeceği için sıfırlanır ve alan boşaltılır. Yığın bellekte ise böyle değildir; bloklar eklenebilir, silinebilir, genişletilip daraltılabilir. Sıralı bellekten verilere daha hızlı erişilir fakat kapasitesi yığın belleğe göre daha azdır. Yığın bellekte ise verilere ulaşmak daha yavaşken daha büyük veriler depolanabilir. Depolanan bu verilerin aynı bizim kimlik numaramız gibi bir numarası vardır, bu numaraya ise işaretçi (pointer) denir. Bu numaralar sayesinde bu verilere erişilebilir.Derleyicilerde ve Dillerde Bellek Yönetimi
Sıralı bellekte genel olarak değişken gibi çok byte kaplamayan veriler tutulurken sınıflar gibi çok sayıda değişken ve metod içeren türler yığın bellekte tutulur. Sıralı bellek, yapısı gereği derleyici tarafından denetlenir. Değişken oluşturmak için sizin ek bir çaba sarf etmenize gerek kalmaz.Yığın bellek yönetimi ise dilden dile değişkenlik gösterebilmektedir. Bazı diller, derleyicileri ve kapasiteleri sayesinde yığın belleklere erişimi kısıtlarken bazı diller serbest bırakabilir. Örneğin C# ve Java dillerinde yığın bellek yönetimi yazılımcıya bırakılmaz. Yazılımcı, referans tipleri kullanarak işini görür. C++ dilinde ise yığın belleği istediğiniz gibi kullanabilirsiniz.
Referans ve Değer Tipler
Yığın bellek yönetiminin kısıtlı olduğu dillerde referans ve değer tip olmak üzere iki çeşit değişken vardır. Değer tipte oluşturulan değişken struct, int, double ve char gibi türlerden oluşur. Bu türlerin her birinin kapladığı belirli bir boyut vardır. Örneğin int değişken çoğu işlemci için 32 bitlik alan kaplar, yani bellekte bu kadar yer ayrılır. Bu yeri derleyici, boş alana göre sıralı belleğin herhangi bir bölümünden ayırabilir. Değer tipte bir değişken oluştuğu zaman ayrılan yere bu değişkenin değeri kaydedilir. Değişkene erişip bir değeri değiştirdiğiniz zaman sıralı bellekteki bu yerin değeri değiştirillir. İkinci bir değişken oluşturup ilk değişkene eşitleme yaptığınız zaman değerler eşitlenir. Yani iki ayrı değişken için bellekte iki ayrı yer ayrılmış olur, tek aynı tarafları ise değerlerinin aynı olmasıdır. İkinci değişkenin değeri değiştirilince ilk değişken değişmez, aralarında bir bağ yoktur.Referans tipte olan değişkenler ise basitçe sınıflardır. Sınıflar, kendi içerisinde birden fazla referans tipi veya değer tipini barındırabilir. Örneğin string denilen yazı tipi harflerden (char) oluşur. Stringin kendisi bir referans tip iken oluştuğu harf, değer tipidir. Referans tipte bir değişken oluşturduğunuz zaman tip, önce yığın bellekte blok şeklinde oluşturulur. Daha sonra bu bloğun adresi sıralı bellekteki ayrılmış alana kaydedilir. Yani siz bir sınıf oluşturup değişkene atadığınız zaman aslında değişkende tutulan şey yığın bellekteki adresidir. Başka bir değişken oluşturup bu önceden oluşturduğunuz değişkene eşitlediğiniz zaman değerler eşitlendiği için aslında yığın bellekteki bloğun adreslerini eşitlemiş olursunuz. Birinci değişkenden sınıfa erişip bir şeyi değiştirdiğiniz zaman yığın bellekteki değer değiştiği için ikinci değişkenden veriyi çektiğiniz zaman değiştirdiğiniz değere ulaşırsınız.
İlk değişkeni oluşturduğunuz başka bir değişkene eşitleyip değiştirdiğiniz zaman ikinci değişkenden bu değişikliği göremezsiniz çünkü artık ilk değişkenin tuttuğu şey bloğun adresi değil, atadığınız değişkenin değeridir. Yani iki değişkenin ortak bir tarafı kalmamıştır.
Yığın bellek yönetiminin serbest olduğu dillerde ise referans tip diye bir şey yoktur. Tüm oluşturulan değişkenler sıralı bellekte oluşturulur. İster sınıf olsun ister int ister struct olsun hepsi sıralı bellekten ayrılan yerde depolanır. Yığın belleği kullanmak istediğiniz zaman dilin kendi metodlarını kullanarak yığın bellekte alan ayırıp bu alanı kullanırsınız. C++ için konuşacak olursak malloc (memory allocation, bellek paylaştırma) metodu ile bu alanı ayırabilirsiniz. İstediğiniz büyüklüğü girip yığın bellekte bir blok oluşturup kullanabilirsiniz.
C++ Dilinde Bellek Yönetimi
İlk önce 2 adet int tipinde değişken içeren bir sınıf oluşturalım.
C++:
class Test{
public:
int a;
int b;
};
Daha sonra oluşturduğumuz Test sınıfını oluşturalım ve bellekte nasıl bulunduğuna bakalım. Ek olarak x değişkeni oluşturmak istedim daha iyi anlaşılması için.
C++:
int x = 2;
Test t = Test();
t.a = 1;
t.b = 5;
Görüldüğü üzere Test sınıfı oluşturulduğunda aslında art arda 2 adet int değişken oluşturulmuş oluyor. y değişkeninde tutulan ise aslında Test sınıfının içerdiği ilk değişkenin (y.a yani a değişkeni) adresi, başka bir deyişle y değişkeninin başlangıcı. Siz y.a dediğiniz zaman ilk önce y değişkenine yani başlangıç noktası olan a değişkeninin adresine ulaşırsınız, oradan da a değişkenine ulaşmak için 0 kere sayılır ve neticesinde adrese ulaşırsınız. y.b dediğiniz zaman ise ilk önce yine y değişkenine erişirsiniz, daha sonra 1 kere sayıp b değişkenine ulaşırsınız. Sonuç olarak sınıf içindeki değişkenler art arda oluşturulmuştur ve sayarak diğer elemanlara erişilebilir.
Yığın belleğe geçmeden önce size işaretçileri anlatmam gerekiyor. Önceden de bahsettiğim gibi aslında işaretçiler bellekte tutulan verilerin adresleridir aslında. Önceki verdiğim 2 bellek resminden farklı olarak bir de adreslerin de işin içine dahil edildiği bir tane göstermek istiyorum.
Görselde gördüğünüz üzere bir yukarıda verilen görselden farklı olarak adreslerini de belirttim. a değişkeni oluştuğu zaman ayrılan alanın adresine yani pointerına yani işaretçisine ptra diyelim. Daha sonra C++ dilinde işaretçileri kullanmak için kullanılan * ve & belirteçlerini kullanarak a'nın işaretçisini değişkene atayalım. * belirteci türün yanına gelerek değişken tipinin o türün işaretçisi olduğunu belirtir. int* demek int türünde bir değişkenin işaretçisidir, yani tipi int işaretçisidir. Bu oluşturduğumuz b değişkenine de &a yani a değişkeninin işaretçisini atayalım. Böylece bellekte yeni bir alan ayrılır ve değerine a'nın adresi atanır. Tabii bu da bir değişken olduğu için onun da bir adresi olacak, bu adrese de ptrb diyelim. Böylece işaretçilerin oluşturuluşunu görmüş olduk. Peki bu ne işimize yarayacak?
b değişkenini kullanarak a değişkenini değiştirebiliriz. Bunun için sayma yöntemini kullanalım. C++'ta saymak için [] operatörünü kullanıyoruz. Köşeli parantezler içine kaç saymak istediğimizi yazıyoruz. Örneğin b[2] dersek b işaretçisinden başla 2 kere say anlamına gelir. Artık b değişkeninin 2 ilerisinde hangi değer tanımlanmışsa bize onu döndürür. Bunu eğer bir tanımlamadıysak ki yukarıdaki örneğe göre sadece 2 değişkenimiz var, bize çöp bir veri dönderecektir. Hani biz değişken oluştururken bellekte alan ayırmıştık ya, işte o ayırdığımız alanı geçip bellekte var olan diğer verilere erişmiş oluruz. Yani ne geleceği belli değildir. Eğer b[0] dersek b'den başlayıp 0 kere sayarız, yani a'nın adresine ulaşırız. Sonuç olarak b[0]'ı kullanarak a değişkenini etkileyebiliriz.
Eğer işaretçi kullanmadan yapsaydık ise durum farklı olurdu.
Gördüğünüz gibi b değişkenini değiştirdik ama a değişkeni etkilenmedi çünkü aralarında bir bağ yok. Sadece değerleri aynıydı, onu da zaten değiştirdik.
Sıralı bellek kısaca bu şekilde gösterilebilir. Yığın bellek yönetimi için ise yine Test sınıfını kullanacağız ama bu sefer sınıfı yığın bellekte oluşturacağız. malloc metodu bizden boyut ister, boşluk türünde işaretçi döndürür.
İlk önce sıralı bellekte Test sınıfını oluşturduk, a ve b değişkenlerine değer verdik. Sonra malloc metodundan Test sınıfının boyutu kadar yığın bellekte bir alan açmasını istedik ve malloc metodu da yığın bellekte istediğimiz alanı ayırıp bu alanın adresini bize dönderdi, biz de Test işaretçisi türünde bir değişken oluşturup bu alanın adresini oraya atadık. Böylece artık hpx değişkeni yığın bellekteki Test sınıfının adresini tutuyor. hpx[0] ile yığın bellekteki boş ayırdığımız alana önceden sıralı bellekte oluşturduğumuz Test sınıfını atadık ve böylece yığın bellekte bir sınıf tutmuş olduk. Tabii ki bu kullanım çoğunlukla tercih edilmiyor, sonuç olarak hem yığın hem de sıralı bellekte obje oluşturmuş oluyoruz. Bunun yerine new kullanılıyor fakat ben mantığını göstermek istedim.
C# Dilinde Bellek Yönetimi
İlk önce yine bir sınıf oluşturalım.
C#:
class Test{
public int a;
public int b;
}
Daha sonra oluşturduğumuz bu sınıfı oluşturup değişkene atayalım.
Burada ek bir işleme gerek kalmadan Test sınıfını oluşturup değerlerini atadık, oluşturduğumuz sınıf direkt olarak yığın bellekte oluştu ve adresi x değişkeninde tutuluyor. y değişkenini oluşturup x'e eşitlediğimiz zaman da bu adres aynı zamanda da y değişkeninin de değeri oldu. Artık y değişkenini değiştirdiğimiz zaman yığın bellekteki değer değişeceğinden x değişkeni de otomatik olarak değişmiş olacak.
Çöp Toplama ve Bellek Açığı (GC, Memory Leak)
Çöp toplama (garbage collection ya da gc) önemli bir husus. Peki nedir bu?Bellek açığı, yığın bellekte gerçekleşen sinir bozucu bir olaydır. Sebebi ise artık kullanılmayan objelerin bu bellekten temizlenmediği için birikmesidir. Birikme sonucu bellek ağırlaşır ve açık oluşur. Bu, hatalara ve ciddi performans kayıplarına yol açabilir. Çöp toplama, bir obje artık kullanılmadığı zaman yığın bellekten temizlenmesi ve bu ayrılan alanın serbest bırakılmasına denir. C# bunu otomatik olarak yaparken C++'ta bunu kendiniz yaparsınız ama korkmayın, basit bir işlem. Yığın bellekte oluşturduğunuz bir objeye artık ihtiyacınız kalmadığında onu silmek için delete sözcüğünü kullanabilirsiniz. Bu, ayırdığınız alanı serbest bırakarak belleği temizlemenizi sağlayacaktır. Örnek olarak yukarıdaki örnekte bulunan hpx'i temizlemek istersek delete hpx yazabiliriz. Böylece bellekte açık oluşmasını önlemiş oluruz.
Diziler
Diziler, C# gibi dillerde yığın bellekte; C++ gibi dillerde ise sıralı bellekte tutulur. Dizinin tüm elemanları bellekte sırayla dizilir ve dizi değişkeninin değeri ilk elemanın yani başlangıç noktasının adresi olur. Anlaşıldığı üzere [] operatörü ile sayarak elemanlara erişirsiniz. Bu hem sıralı bellek hem de yığın bellek için geçerlidir. Yani [] operatörünün aslında ne olduğunu merak ediyorsanız sayma operatörü olarak özetleyebiliriz.Sonuç
Artık belleğin yapısı ve dillerdeki kullanımlar hakkında bilgi sahibisiniz. Tüm detayları vermedim kafa karıştırmamak adına fakat mantığını anlattığımı düşünüyorum. Bu konuda Türkçe olarak çok açıklayıcı kaynaklar bulamadığım ve bilgi kirliliği olduğu için bu yazıyı yazma ihtiyacı duydum. Umarım tatmin edici bilgiler verebilmişimdir, aklınıza takılan şeyleri sorabilirsiniz.İyi forumlar