Bellek Yönetimi
Şimdiye kadar yazdığımız programlarda hiç bellek yönetimiyle ilgilenmek zorunda kalmadık çünkü D bellek yönetimi gerektirmeyen bir dildir. O yüzden, burada anlatılanlara büyük olasılıkla hiç ihtiyaç duymayacaksınız. Buna rağmen, D gibi sistem dillerinde alt düzey bellek işlemleri ile ilgilenmek gerekebilir.
Bellek yönetimi çok kapsamlı bir konudur. Bu bölümde yalnızca çöp toplayıcıyı tanıyacağız, çöp toplayıcıdan nasıl bellek ayrıldığını ve belirli bellek bölgelerine değişkenlerin nasıl yerleştirildiklerini göreceğiz. Farklı bellek yönetimi yöntemlerini ve özellikle std.allocator
modülünü kendiniz araştırmanızı öneririm. (std.allocator
bu kitap yazıldığı sırada henüz deneysel aşamadaydı.)
Önceki bazı bölümlerde olduğu gibi, aşağıda kısaca yalnızca değişken yazdığım yerlerde yapı ve sınıf nesneleri de dahil olmak üzere her türden değişkeni kastediyorum.
Bellek
Bellek hem programın kendisini hem de kullandığı verileri barındırır. Bu yüzden diğer bilgisayar kaynaklarından daha önemlidir. Bu kaynak temelde işletim sistemine aittir. İşletim sistemi belleği ihtiyaçlar doğrultusunda programlara paylaştırır. Her programın kullanmakta olduğu bellek o programın belirli zamanlardaki ihtiyaçları doğrultusunda artabilir veya azalabilir. Belirli bir programın kullandığı bellek o program sonlandığında tekrar işletim sistemine geçer.
Bellek, değişken değerlerinin yazıldığı bir defter gibi düşünülebilir. Her değişken bellekte belirli bir yere yazılır. Her değişkenin değeri gerektikçe aynı yerden okunur ve kullanılır. Yaşamı sona eren değişkenlerin yerleri daha sonradan başka değişkenler için kullanılır.
Bellekle ilgili deneyler yaparken değişkenlerin adres değerlerini veren &
işlecinden yararlanabiliriz:
import std.stdio; void main() { int i; int j; writeln("i: ", &i); writeln("j: ", &j); }
Not: Adresler programın her çalıştırılışında büyük olasılıkla farklı olacaktır. Ek olarak, adres değerini edinmiş olmak, normalde bir mikro işlemci yazmacında yaşayacak olan bir değişkenin bile bellekte yaşamasına neden olur.
Çıktısı:
i: 7FFF2B633E28 j: 7FFF2B633E2C
Adreslerdeki tek fark olan son hanelere bakarak i
'nin bellekte j
'den hemen önce bulunduğunu görebiliyoruz: 8'e int
'in büyüklüğü olan 4'ü eklersek on altılı sayı düzeninde C elde edilir.
Çöp toplayıcı
D programlarındaki dinamik değişkenler çöp toplayıcıya ait olan bellek bölgelerinde yaşarlar. Yaşamları sona eren değişkenler çöp toplayıcının işlettiği bir algoritma ile sonlandırılırlar. Bu değişkenlerin yerleri tekrar kullanılmak üzere geri alınır. Bu işleme aşağıda bazen çöp toplama, bazen de temizlik diyeceğim.
Çöp toplayıcının işlettiği algoritma çok kabaca şöyle açıklanabilir: Çağrı yığıtı da dahil olmak üzere kök olarak adlandırılan bölgeler taranır. O bölgelerdeki değişkenler yoluyla doğrudan veya dolaylı olarak erişilebilen bütün bellek bölgeleri belirlenir ve program tarafından herhangi bir yolla erişilebilen bütün bölgelerin hâlâ kullanımda olduklarına karar verilir. Kullanımda olmadıkları görülen diğer bellek bölgelerindeki değişkenlerin sonlandırıcıları işletilir ve o bellek bölgeleri sonradan başka değişkenler için kullanılmak üzere geri alınır. Kökler; her iş parçacığının çağrı yığıtından, bütün evrensel değişkenlerden, ve GC.addRoot
veya GC.addRange
ile tanıtılmış olan bölgelerden oluşur.
Bazı çöp toplayıcılar kullanımda olan bütün değişkenleri bellekte yan yana dursunlar diye başka yerlere taşıyabilirler. Programın tutarlılığı bozulmasın diye de o değişkenleri gösteren bütün göstergelerin değerlerini otomatik olarak değiştirirler. (D'nin bu kitabın yazıldığı sırada kullandığı çöp toplayıcısı nesne taşıyan çeşitten değildi.)
Hangi bellek bölgelerinde gösterge bulunduğunun ve hangilerinde bulunmadığının hesabını tutan çöp toplayıcılarına hassas (precise) denir. Bunun aksine, her bellek bölgesindeki değerlerin gösterge olduklarını varsayan çöp toplayıcılarına ise korunumlu (conservative) denir. Bu kitabın yazıldığı sırada kullanılan D çöp toplayıcısının yarı korunumlu olduğunu söyleyebiliriz: yalnızca gösterge içeren bellek bölgelerini, ama o bölgelerin tamamını tarar. Bunun bir etkisi, bazı bellek bölgelerinin hiç toplanmayarak bellek sızıntısı oluşturabilmesidir. Yalancı göstergelerin neden olduğu bu durumdan kaçınmak için artık kullanılmadığı bilinen bellek bölgelerinin programcı tarafından açıkça geri verilmesi önerilir.
Temizlik işlemlerinin hangi sırada işletildikleri belirsizdir. Örneğin, nesnelerin referans türündeki (göstergeler dahil) üyeleri kendilerini barındıran nesneden daha önce sonlanmış olabilirler. Bu yüzden, yaşamları çöp toplayıcıya ait olan ve kendileri referans türünden olan üyelerin sonlandırıcı işlevler içinde kullanılmaları hatalıdır. Bu kavram sonlanma sıralarının tam olarak belirli olduğu C++ gibi bazı dillerden farklıdır.
Temizlik işlemleri boş yerin azalmaya başlaması gibi nedenlerle ve önceden kestirilemeyecek zamanlarda işletilebilir. Temizlik işlemleri devam ederken yeni yer ayrılması çöp toplama düzeneğinde karışıklık yaratabileceğinden programa ait olan bütün iş parçacıkları temizlik sırasında kısa süreliğine durdurulabilirler. Bu işlem sırasında programın tutukluk yaptığı hissedilebilir.
Programcının çöp toplayıcının işine karışması çoğu durumda gerekmese de temizlik işlemlerinin hemen işletilmeleri veya ertelenmeleri gibi bazı işlemler core.memory
modülünün olanakları ile sağlanabilir.
Temizlik başlatmak ve ertelemek
Programın tutukluk yapmadan çalışması gereken yerlerde temizlik işlemlerinin ertelenmesi mümkündür. GC.disable
temizlik işlemlerini erteler, GC.enable
da tekrar etkinleştirir:
GC.disable();
// ... tutukluk hissedilmeden işlemesi gereken işlemler ...
GC.enable();
Ancak, temizlik işlemlerinin kesinlikle işletilmeyecekleri garantili değildir: Çöp toplayıcı belleğin çok azaldığını farkettiği durumlarda boş yer bulmak için yine de işletebilir.
Temizlik işlemleri programın tutukluk yapmasının sorun oluşturmadığının bilindiği bir zamanda programcı tarafından GC.collect()
ile başlatılabilir:
import core.memory; // ... GC.collect(); // temizlik başlatır
Normalde, çöp toplayıcı boş kalan bellek bölgelerini işletim sistemine geri vermez ve ileride oluşturulacak olan değişkenler için elinde tutmaya devam eder. Bunun bir sorun oluşturduğunun bilindiği programlarda boş bellek bölgeleri GC.minimize()
ile işletim sistemine geri verilebilir:
GC.minimize();
Bellekten yer ayırmak
Bellekten herhangi bir amaç için bellek bölgesi ayrılabilir. Böyle bir bölge örneğin üzerinde değişkenler kurmak için kullanılabilir.
Belirli sayıda bayttan oluşan bir bellek bölgesi sabit uzunluklu bir dizi olarak ayrılabilir:
ubyte[100] yer; // 100 baytlık yer
Yukarıdaki dizi 100 baytlık bellek bölgesi olarak kullanılmaya hazırdır. Bazen bu bölgenin uybte
gibi bir türle ilgisi olması yerine hiçbir türden olması istenebilir. Bunun için eleman türü olarak void
seçilir ve void
türü herhangi bir değer alamadığından böyle dizilerin özel olarak =void
ile ilklenmeleri gerekir:
void[100] yer = void; // 100 baytlık yer
Bu bölümde bellek ayırmak için yalnızca core.memory
modülündeki GC.calloc
işlevini kullanacağız. Aynı modüldeki diğer bellek ayırma işlevlerini kendiniz araştırmak isteyebilirsiniz. Ek olarak, C standart kütüphanesinin olanaklarını içeren core.stdc.stdlib
modülündeki calloc()
ve diğer işlevler de kullanılabilir.
GC.calloc
bellekten kaç bayt istendiğini parametre olarak alır ve ayırdığı bellek bölgesinin başlangıç adresini döndürür:
import core.memory; // ... void * yer = GC.calloc(100); // 100 baytlık yer
void*
ile gösterilen bir bölgenin hangi tür için kullanılacağı o türün göstergesine dönüştürülerek belirlenebilir:
int * intYeri = cast(int*)yer;
Ancak, o ara adım çoğunlukla atlanır ve GC.calloc
'un döndürdüğü adres istenen türe doğrudan dönüştürülür:
int * intYeri = cast(int*)GC.calloc(100);
Öylesine seçmiş olduğum 100 gibi hazır değerler kullanmak yerine örneğin türün uzunluğu ile nesne adedi çarpılabilir:
// 25 int için yer int * yer = cast(int*)GC.calloc(int.sizeof * 25);
Sınıf nesnelerinin uzunluğu konusunda önemli bir fark vardır: .sizeof
sınıf nesnesinin değil, sınıf değişkeninin uzunluğudur. Sınıf nesnesinin uzunluğu __traits(classInstanceSize)
ile öğrenilir:
// 10 Sınıf nesnesi için yer Sınıf * yer = cast(Sınıf*)GC.calloc( __traits(classInstanceSize, Sınıf) * 10);
İstenen büyüklükte bellek ayrılamadığı zaman core.exception.OutOfMemoryError
türünde bir hata atılır:
void * yer = GC.calloc(10_000_000_000);
O kadar bellek ayrılamayan durumdaki çıktısı:
core.exception.OutOfMemoryError
Ayrılan bellek işi bittiğinde GC.free
ile geri verilebilir:
GC.free(yer);
Ancak, açıkça çağrılan free()
, sonlandırıcıları işletmez. Sonlanmaları gereken nesnelerin bellek geri verilmeden önce destroy()
ile teker teker sonlandırılmaları gerekir. Çöp toplayıcı struct
ve class
nesnelerini sonlandırma kararını verirken çeşitli etkenleri gözden geçirir. Bu yüzden, sonlandırıcının kesinlikle çağrılması gereken bir durumda en iyisi nesneyi new
işleci ile kurmaktır. O zaman GC.free()
sonlandırıcıyı işletir.
Daha önce çöp toplayıcıdan alınmış olan bir bellek bölgesinin uzatılması mümkündür. GC.realloc()
, daha önce edinilmiş olan adres değerini ve istenen yeni uzunluğu parametre olarak alır ve yeni uzunlukta bir yer döndürür. Aşağıdaki kod önceden 100 bayt olarak ayrılmış olan bellek bölgesini 200 bayta uzatıyor:
void * eskiYer = GC.calloc(100); // ... void * yeniYer = GC.realloc(eskiYer, 200);
realloc()
gerçekten gerekmedikçe yeni yer ayırmaz:
- Eski yerin hemen sonrası yeni uzunluğu karşılayacak kadar boşsa orayı da eski yere ekleyerek bir anlamda eski belleği uzatır.
- Eski yerin hemen sonrası boş değilse veya yeni büyüklük için yeterli değilse, istenen miktarı karşılayacak yeni bir bellek bölgesi ayırır ve eski belleğin içeriğini oraya kopyalar.
- Eski yer olarak
null
gönderilebilir; o durumda yalnızca yeni bir yer ayırır. - Yeni uzunluk olarak eski uzunluktan daha küçük bir değer gönderilebilir; o durumda yalnızca bellek bölgesinin geri kalanı çöp toplayıcıya geri verilmiş olur.
- Yeni uzunluk 0 ise eski bellek
free()
çağrılmış gibi geri verilir.
GC.realloc
C kütüphanesindeki aynı isimli işlevden gelmiştir. Görevi hem fazla çeşitli hem de fazla karmaşık olduğundan hatalı tasarlanmış bir işlev olarak kabul edilir. GC.realloc
'un şaşırtıcı özelliklerinden birisi, asıl bellek GC.calloc
ile ayrılmış bile olsa uzatılan bölümün sıfırlanmamasıdır. Bu yüzden, belleğin sıfırlanmasının önemli olduğu durumlarda aşağıdaki gibi bir işlevden yararlanılabilir (bellekNitelikleri
parametresinin anlamını biraz aşağıda göreceğiz):
import core.memory; /* GC.realloc gibi işler. Ondan farklı olarak, belleğin * uzatıldığı durumda eklenen baytları sıfırlar. */ void * boşOlarakUzat( void * yer, size_t eskiUzunluk, size_t yeniUzunluk, GC.BlkAttr bellekNitelikleri = GC.BlkAttr.NONE, const TypeInfo türBilgisi = null) { /* Asıl işi GC.realloc'a yaptırıyoruz. */ yer = GC.realloc(yer, yeniUzunluk, bellekNitelikleri, türBilgisi); /* Eğer varsa, yeni eklenen bölümü sıfırlıyoruz. */ if (yeniUzunluk > eskiUzunluk) { import core.stdc.string; auto eklenenYer = yer + eskiUzunluk; const eklenenUzunluk = yeniUzunluk - eskiUzunluk; memset(eklenenYer, 0, eklenenUzunluk); } return yer; }
core.stdc.string
modülünde tanımlı olan memset()
belirtilen adresteki belirtilen sayıdaki bayta belirtilen değeri atar. Örneğin, yukarıdaki çağrı eklenenYer
'deki eklenenUzunluk
adet baytı 0
yapar.
boşOlarakUzat()
işlevini aşağıdaki bir örnekte kullanacağız.
GC.realloc
ile benzer amaçla kullanılan GC.extend
'in davranışı çok daha basittir çünkü yalnızca yukarıdaki ilk maddeyi uygular: Eski yerin hemen sonrası yeni uzunluğu karşılayamıyorsa hiçbir işlem yapmaz ve bu durumu 0 döndürerek bildirir.
Ayrılan belleğin temizlik işlemlerinin belirlenmesi
Çöp toplayıcı algoritmasında geçen kavramlar ve adımlar bir enum
türü olan BlkAttr
'ın değerleri ile her bellek bölgesi için ayrı ayrı ayarlanabilir. BlkAttr
, GC.calloc
ve diğer bellek ayırma işlevlerine parametre olarak gönderilebilir ve bellek bölgelerinin niteliklerini belirlemek için kullanılır. BlkAttr
türünün değerleri şunlardır:
NONE
: Sıfır değeri; hiçbir niteliğin belirtilmediğini belirler.FINALIZE
: Bölgedeki nesnelerin temizlik sırasında çöp toplayıcı tarafından sonlandırılmaları gerektiğini belirler.Normalde, çöp toplayıcı kendisinden ayrılmış olan bellekteki nesnelerin yaşam süreçlerinin artık programcının sorumluluğuna girdiğini düşünür ve bu bölgelerdeki nesnelerin sonlandırıcılarını işletmez.
GC.BlkAttr.FINALIZE
değeri, çöp toplayıcının sonlandırıcıları yine de işletmesinin istendiğini belirtir:Sınıf * yer = cast(Sınıf*)GC.calloc( __traits(classInstanceSize, Sınıf) * 10, GC.BlkAttr.FINALIZE);
FINALIZE
, çöp toplayıcının bellek bloğuna yazdığı kendi özel ayarlarıyla ilgili bir belirteçtir. O yüzden, bu belirtecin normalde programcı tarafından değil, çöp toplayıcı tarafından kullanılması önerilir.NO_SCAN
: Bölgenin çöp toplayıcı tarafından taranmaması gerektiğini belirler.Ayrılan bölgedeki bayt değerleri tesadüfen ilgisiz başka değişkenlerin adreslerine karşılık gelebilirler. Öyle bir durumda çöp toplayıcı hâlâ kullanımda olduklarını sanacağından, aslında yaşamları sona ermiş bile olsa o başka değişkenleri sonlandırmaz.
Başka değişken referansları taşımadığı bilinen bellek bölgelerinin taranması
GC.BlkAttr.NO_SCAN
niteliği ile engellenir:int * intYeri = cast(int*)GC.calloc(100, GC.BlkAttr.NO_SCAN);
Yukarıdaki bellek bölgesine yerleştirilecek olan
int
değerlerinin tesadüfen başka değişkenlerin adreslerine eşit olmaları böylece artık sorun oluşturmaz.NO_MOVE
: Bölgedeki nesnelerin başka bölgelere taşınmamaları gerektiğini belirler.APPENDABLE
: Bu, D çalışma ortamına ait olan ve dizilere daha hızlı eleman eklenmesini sağlayan bir belirteçtir. Programcı tarafından kullanılmaz.NO_INTERIOR
: Bu bölgenin iç tarafındaki değişkenleri gösteren gösterge bulunmadığını belirtir (olası göstergeler bölgenin yalnızca ilk adresini gösterirler). Bu, yalancı gösterge olasılığını düşürmeye yarayan bir belirteçtir.
Bu değerler Bit İşlemleri bölümünde gördüğümüz işleçlerle birlikte kullanılabilecek biçimde seçilmişlerdir. Örneğin, iki değer |
işleci ile aşağıdaki gibi birleştirilebilir:
const bellekAyarları = GC.BlkAttr.NO_SCAN | GC.BlkAttr.NO_INTERIOR;
Doğal olarak, çöp toplayıcı yalnızca kendi ayırdığı bellek bölgelerini tanır ve temizlik işlemleri sırasında yalnızca o bölgeleri tarar. Örneğin, core.stdc.stdlib.calloc
ile ayrılmış olan bellek bölgelerinden çöp toplayıcının normalde haberi olmaz.
Kendisinden alınmamış olan bir bölgenin çöp toplayıcının yönetimine geçirilmesi için GC.addRange()
işlevi kullanılır. Bunun karşıtı olarak, bellek geri verilmeden önce de GC.removeRange()
'in çağrılması gerekir.
Bazı durumlarda çöp toplayıcı kendisinden ayrılmış olan bir bölgeyi gösteren hiçbir referans bulamayabilir. Örneğin, ayrılan belleğin tek referansı bir C kütüphanesi içinde tutuluyor olabilir. Böyle bir durumda çöp toplayıcı o bölgenin kullanımda olmadığını düşünecektir.
GC.addRoot()
, belirli bir bölgeyi çöp toplayıcıya tanıtır ve oradan dolaylı olarak erişilebilen bütün nesneleri de yönetmesini sağlar. Bunun karşıtı olarak, bellek geri verilmeden önce de GC.removeRoot()
işlevinin çağrılması gerekir.
Bellek uzatma örneği
realloc()
'un kullanımını göstermek için dizi gibi işleyen çok basit bir yapı tasarlayalım. Çok kısıtlı olan bu yapıda yalnızca eleman ekleme ve elemana erişme olanakları bulunsun. D dizilerinde olduğu gibi bu yapının da sığası olsun. Aşağıdaki yapı sığayı gerektikçe yukarıda tanımladığımız ve kendisi GC.realloc
'tan yararlanan boşOlarakUzat()
ile arttırıyor:
struct Dizi(T) { T * yer; // Elemanların bulunduğu yer size_t sığa; // Toplam kaç elemanlık yer olduğu size_t uzunluk; // Eklenmiş olan eleman adedi /* Belirtilen numaralı elemanı döndürür */ T eleman(size_t numara) { import std.string; enforce(numara < uzunluk, format("%s numara yasal değil", numara)); return *(yer + numara); } /* Elemanı dizinin sonuna ekler */ void ekle(T eleman) { writefln("%s numaralı eleman ekleniyor", uzunluk); if (uzunluk == sığa) { /* Yeni eleman için yer yok; sığayı arttırmak * gerekiyor. */ size_t yeniSığa = sığa + (sığa / 2) + 1; sığaArttır(yeniSığa); } /* Elemanı en sona yerleştiriyoruz */ *(yer + uzunluk) = eleman; ++uzunluk; } void sığaArttır(size_t yeniSığa) { writefln("Sığa artıyor: %s -> %s", sığa, yeniSığa); const eskiUzunluk = sığa * T.sizeof; const yeniUzunluk = yeniSığa * T.sizeof; /* Bu bölgeye yerleştirilen bayt değerlerinin * tesadüfen başka değişkenlerin göstergeleri * sanılmalarını önlemek için NO_SCAN belirtecini * kullanıyoruz. */ yer = cast(T*)boşOlarakUzat( yer, eskiUzunluk, yeniUzunluk, GC.BlkAttr.NO_SCAN); sığa = yeniSığa; } }
Bu dizinin sığasi her seferinde yaklaşık olarak %50 oranında arttırılıyor. Örneğin, 100 elemanlık yer tükendiğinde yeni sığa 151 oluyor. (Yeni sığa hesaplanırken eklenen 1 değeri, başlangıç durumunda sıfır olan sığa için özel bir işlem gerekmesini önlemek içindir. Öyle olmasaydı, sıfırın %50 fazlası da sıfır olacağından sığa hiç artamazdı.)
Bu yapıyı double
türünde elemanlarla şöyle deneyebiliriz:
import std.stdio; import core.memory; import std.exception; // ... void main() { auto dizi = Dizi!double(); size_t adet = 10; foreach (i; 0 .. adet) { double elemanDeğeri = i * 1.1; dizi.ekle(elemanDeğeri); } writeln("Bütün elemanlar:"); foreach (i; 0 .. adet) { write(dizi.eleman(i), ' '); } writeln(); }
Çıktısı:
0 numaralı eleman ekleniyor Sığa artıyor: 0 -> 1 1 numaralı eleman ekleniyor Sığa artıyor: 1 -> 2 2 numaralı eleman ekleniyor Sığa artıyor: 2 -> 4 3 numaralı eleman ekleniyor 4 numaralı eleman ekleniyor Sığa artıyor: 4 -> 7 5 numaralı eleman ekleniyor 6 numaralı eleman ekleniyor 7 numaralı eleman ekleniyor Sığa artıyor: 7 -> 11 8 numaralı eleman ekleniyor 9 numaralı eleman ekleniyor Bütün elemanlar: 0 1.1 2.2 3.3 4.4 5.5 6.6 7.7 8.8 9.9
Hizalama birimi
Değişkenler normalde kendi türlerine özgü bir değerin katı olan adreslerde bulunurlar. Bu değere o türün hizalama birimi denir. Örneğin, int
türünün hizalama birimi 4'tür çünkü int
değişkenler ancak dördün katı olan adreslerde (4, 8, 12, vs.) bulunabilirler.
Hizalama, hem mikro işlemci işlemlerinin hızlı olması için istenen hem de mikro işlemcinin nesne adresleyebilmesi için gereken bir kavramdır. Ek olarak, bazı değişkenler yalnızca kendi türlerinin hizalama birimine uyan adreslerde iseler kullanılabilirler.
Türlerin .alignof
niteliği
Bir türün .alignof
niteliği o türün varsayılan hizalama birimini döndürür. Ancak, sınıflarda .alignof
sınıf nesnesinin değil, sınıf değişkeninin hizalama birimidir. Sınıf nesnesinin hizalama birimi için std.traits.classInstanceAlignment
kullanılmalıdır.
Aşağıdaki program çeşitli türün hizalama birimini yazdırıyor.
import std.stdio; import std.meta; import std.traits; struct BoşYapı { } struct Yapı { char c; double d; } class BoşSınıf { } class Sınıf { char karakter; } void main() { alias Türler = AliasSeq!(char, short, int, long, double, real, string, int[int], int*, BoşYapı, Yapı, BoşSınıf, Sınıf); writeln(" Uzunluk Hizalama Tür\n", "========================"); foreach (Tür; Türler) { static if (is (Tür == class)) { size_t uzunluk = __traits(classInstanceSize, Tür); size_t hizalama = classInstanceAlignment!Tür; } else { size_t uzunluk = Tür.sizeof; size_t hizalama = Tür.alignof; } writefln("%6s%9s %s", uzunluk, hizalama, Tür.stringof); } }
Bu programın çıktısı farklı ortamlarda farklı olabilir:
Uzunluk Hizalama Tür ======================== 1 1 char 2 2 short 4 4 int 8 8 long 8 8 double 16 16 real 16 8 string 8 8 int[int] 8 8 int* 1 1 BoşYapı 16 8 Yapı 16 8 BoşSınıf 17 8 Sınıf
Biraz aşağıda nesnelerin belirli adreslerde de kurulabildiklerini göreceğiz. Bunun güvenle yapılabilmesi için hizalama birimlerinin gözetilmeleri gerekir.
Bunun örneğini görmek için yukarıdaki 17 bayt uzunluğundaki Sınıf
türünün iki nesnesinin bellekte yan yana nasıl durabileceklerine bakalım. Her ne kadar yasal bir adres olmasa da, örneği kolaylaştırmak için birinci nesnenin 0 adresinde bulunduğunu varsayalım. Bu nesneyi oluşturan baytlar 0'dan 16'ya kadar olan adreslerdedir:
0 1 16 ┌────┬────┬─ ... ─┬────┬─ ... │<───birinci nesne────>│ └────┴────┴─ ... ─┴────┴─ ...
Bir sonraki boş yerin adresi 17 olduğu halde o adres değeri Sınıf
'ın hizalama birimi olan 8'in katı olmadığından ikinci nesne orada kurulamaz. İkinci nesnenin 8'in katı olan bir sonraki adrese, yani 24 adresine yerleştirilmesi gerekir. Aradaki kullanılmayan baytlara doldurma baytları denir:
0 1 16 17 23 24 25 30 ┌────┬────┬─ ... ─┬────┬────┬─ ... ─┬────┬────┬────┬─ ... ─┬────┬─ ... │<───birinci nesne────>│ <──doldurma───> │<────ikinci nesne────>│ └────┴────┴─ ... ─┴────┴────┴─ ... ─┴────┴────┴────┴─ ... ─┴────┴─ ...
Bir nesnenin belirli bir aday adresten sonra yasal olarak kurulabileceği ilk adresi elde etmek için şu hesap kullanılabilir:
(adayAdres + hizalamaBirimi - 1) / hizalamaBirimi * hizalamaBirimi
Yukarıdaki hesabın doğru olarak işlemesi için bölme işleminden kalanın gözardı edilmesi şarttır. O yüzden o hesapta tamsayı türleri kullanılır.
Aşağıda emplace()
'in örneklerini gösterirken yukarıdaki hesabı uygulayan şu işlevden yararlanacağız:
T * hizalıAdres(T)(T * adayAdres) { import std.traits; static if (is (T == class)) { const hizalama = classInstanceAlignment!T; } else { const hizalama = T.alignof; } const sonuç = (cast(size_t)adayAdres + hizalama - 1) / hizalama * hizalama; return cast(T*)sonuç; }
Yukarıdaki işlev nesnenin türünü şablon parametresinden otomatik olarak çıkarsamaktadır. Onun void*
adresleri ile işleyen yüklemesini de şöyle yazabiliriz:
void * hizalıAdres(T)(void * adayAdres) { return hizalıAdres(cast(T*)adayAdres); }
Bu işlev de aşağıda emplace()
ile sınıf nesneleri oluştururken yararlı olacak.
Son olarak, yukarıdaki işlevden yararlanan yardımcı bir işlev daha tanımlayalım. Bu işlev, nesnenin boşluklarla birlikte kaç bayt yer tuttuğunu döndürür:
size_t boşlukluUzunluk(T)() { static if (is (T == class)) { size_t uzunluk = __traits(classInstanceSize, T); } else { size_t uzunluk = T.sizeof; } return cast(size_t)hizalıAdres(cast(T*)uzunluk); }
.offsetof
niteliği
Hizalama üye değişkenlerle de ilgili olan bir kavramdır. Üyeleri kendi türlerinin hizalama birimlerine uydurmak için üyeler arasına da doldurma baytları yerleştirilir. Örneğin, aşağıdaki yapının büyüklüğü bekleneceği gibi 6 değil, 12'dir:
struct A { byte b; // 1 bayt int i; // 4 bayt ubyte u; // 1 bayt } static assert(A.sizeof == 12); // 1 + 4 + 1'den daha fazla
Bunun nedeni, hem int
üye dördün katı olan bir adrese denk gelsin diye ondan önceye yerleştirilen, hem de bütün yapı nesnesi yapı türünün hizalama birimine uysun diye en sona yerleştirilen doldurma baytlarıdır.
.offsetof
niteliği bir üyenin nesnenin başlangıç adresinden kaç bayt sonra olduğunu bildirir. Aşağıdaki işlev belirli bir türün bellekteki yerleşimini doldurma baytlarını .offsetof
ile belirleyerek yazdırır:
void nesneYerleşiminiYazdır(T)() if (is (T == struct) || is (T == union)) { import std.stdio; import std.string; writefln("=== '%s' nesnelerinin yerleşimi" ~ " (.sizeof: %s, .alignof: %s) ===", T.stringof, T.sizeof, T.alignof); /* Tek satır bilgi yazar. */ void satırYazdır(size_t uzaklık, string bilgi) { writefln("%4s: %s", uzaklık, bilgi); } /* Doldurma varsa miktarını yazdırır. */ void doldurmaBilgisiYazdır(size_t beklenenUzaklık, size_t gözlemlenenUzaklık) { if (beklenenUzaklık < gözlemlenenUzaklık) { /* Gözlemlenen uzaklık beklenenden fazlaysa * doldurma baytı var demektir. */ const doldurmaMiktarı = gözlemlenenUzaklık - beklenenUzaklık; satırYazdır(beklenenUzaklık, format("... %s bayt DOLDURMA", doldurmaMiktarı)); } } /* Bir sonraki üyenin doldurma olmayan durumda nerede * olacağı bilgisini tutar. */ size_t doldurmasızUzaklık = 0; /* Not: __traits(allMembers) bir türün üyelerinin * isimlerinden oluşan bir 'string' topluluğudur. */ foreach (üyeİsmi; __traits(allMembers, T)) { mixin (format("alias üye = %s.%s;", T.stringof, üyeİsmi)); const uzaklık = üye.offsetof; doldurmaBilgisiYazdır(doldurmasızUzaklık, uzaklık); const türİsmi = typeof(üye).stringof; satırYazdır(uzaklık, format("%s %s", türİsmi, üyeİsmi)); doldurmasızUzaklık = uzaklık + üye.sizeof; } doldurmaBilgisiYazdır(doldurmasızUzaklık, T.sizeof); }
Aşağıdaki program, büyüklüğü yukarıda 12 bayt olarak bildirilen A
yapısının yerleşimini yazdırır:
struct A { byte b; int i; ubyte u; } void main() { nesneYerleşiminiYazdır!A(); }
Programın çıktısı 6 doldurma baytının nesnenin nerelerinde olduğunu gösteriyor. Çıktıda soldaki sütun nesnenin başından olan uzaklığı göstermektedir:
=== 'A' nesnelerinin yerleşimi (.sizeof: 12, .alignof: 4) ===
0: byte b
1: ... 3 bayt DOLDURMA
4: int i
8: ubyte u
9: ... 3 bayt DOLDURMA
Doldurma baytlarını olabildiğince azaltmanın bir yolu, üyeleri yapı içinde büyükten küçüğe doğru sıralamaktır. Örneğin, int
üyeyi diğerlerinden önceye alınca yapının büyüklüğü azalır:
struct B { int i; // Üye listesinin başına getirildi byte b; ubyte u; } void main() { nesneYerleşiminiYazdır!B(); }
Bu sefer yalnızca en sonda 2 doldurma baytı bulunduğundan yapının büyüklüğü 8'e inmiştir:
=== 'B' nesnelerinin yerleşimi (.sizeof: 8, .alignof: 4) ===
0: int i
4: byte b
5: ubyte u
6: ... 2 bayt DOLDURMA
align
niteliği
align
niteliği değişkenlerin, kullanıcı türlerinin, ve üyelerin hizalama birimlerini belirler. Parantez içinde belirtilen değer hizalama birimidir. Her tanımın hizalama birimi ayrı ayrı belirlenebilir. Örneğin, aşağıdaki tanımda S
nesnelerinin hizalama birimi 2, ve özellikle i
üyesinin hizalama birimi 1 olur (hizalama birimi 1, hiç doldurma baytı olmayacak demektir):
align (2) // 'S' nesnelerinin hizalama birimi struct S { byte b; align (1) int i; // 'i' üyesinin hizalama birimi ubyte u; } void main() { nesneYerleşiminiYazdır!S(); }
int
üyenin hizalama birimi 1 olduğunda onun öncesinde hiç doldurma baytına gerek kalmaz ve yapının büyüklüğü üyelerinin büyüklüğü olan 6'ya eşit olur:
=== 'S' nesnelerinin yerleşimi (.sizeof: 6, .alignof: 4) ===
0: byte b
1: int i
5: ubyte u
Ancak, varsayılan hizalama birimleri gözardı edildiğinde programın hızında önemli derecede yavaşlama görülebilir. Ek olarak, yanlış hizalanmış olan değişkenler bazı mikro işlemcilerde programın çökmesine neden olabilirler.
align
ile değişkenlerin hizalamaları da belirlenebilir:
align (32) double d; // Bu değişkenin hizalama birimi
Ancak, çöp toplayıcı new
ile ayrılmış olan nesnelerin hizalama birimlerinin size_t
türünün uzunluğunun bir tam katı olduğunu varsayar. Çöp toplayıcıya ait olan değişkenlerin hizalama birimlerinin buna uymaması tanımsız davranışa neden olur. Örneğin, size_t
8 bayt ise new
ile ayrılmış olan nesnelerin hizalama birimleri 8'in katı olmalıdır.
Değişkenleri belirli bir yerde kurmak
new
ifadesi üç işlem gerçekleştirir:
- Bellekten nesnenin sığacağı kadar yer ayırır. Bu bellek bölgesi henüz hiçbir nesneyle ilgili değildir.
- Nesnenin türünün
.init
değerini o yere kopyalar ve kurucu işlevini o bellek bölgesi üzerinde işletir. Nesne ancak bu işlemden sonra o bölgeye yerleştirilmiş olur. - Nesne daha sonradan sonlandırılırken kullanılmak üzere bellek bölgesi belirteçlerini ayarlar.
Bu işlemlerden birincisinin GC.calloc
ve başka işlevlerle gerçekleştirilebildiğini yukarıda gördük. Bir sistem dili olan D, normalde otomatik olarak işletilen ikinci adımın da programcı tarafından belirlenmesine olanak verir.
Nesnelerin belirli bir adreste kurulması için "yerleştir" anlamına gelen std.conv.emplace
kullanılır.
Yapı nesnelerini belirli bir yerde kurmak
emplace()
, nesnenin kurulacağı adresi parametre olarak alır ve o adreste bir nesne kurar. Eğer varsa, nesnenin kurucu işlevinin parametreleri bu adresten sonra bildirilir:
import std.conv; // ... emplace(adres, /* ... kurucu parametreleri ... */);
Yapı nesneleri kurarken türün ayrıca belirtilmesi gerekmez; emplace()
hangi türden nesne kuracağını kendisine verilen göstergenin türünden anlar. Örneğin, aşağıdaki emplace()
çağrısında öğrenciAdresi
'nin türü bir Öğrenci*
olduğundan emplace()
o adreste bir Öğrenci
nesnesi kurar:
Öğrenci * öğrenciAdresi = hizalıAdres(adayAdres);
// ...
emplace(öğrenciAdresi, isim, numara);
Yukarıdaki işlevlerden yararlanan aşağıdaki program bütün nesneleri alabilecek büyüklükte bir bölge ayırıyor ve nesneleri o bölge içindeki hizalı adreslerde kuruyor:
import std.stdio; import std.string; import core.memory; import std.conv; // ... struct Öğrenci { string isim; int numara; string toString() { return format("%s(%s)", isim, numara); } } void main() { /* Önce bu türle ilgili bilgi yazdırıyoruz. */ writefln("Öğrenci.sizeof: %#x (%s) bayt", Öğrenci.sizeof, Öğrenci.sizeof); writefln("Öğrenci.alignof: %#x (%s) bayt", Öğrenci.alignof, Öğrenci.alignof); string[] isimler = [ "Deniz", "Pınar", "Irmak" ]; const toplamBayt = boşlukluUzunluk!Öğrenci() * isimler.length; /* Bütün Öğrenci nesnelerine yetecek kadar yer ayırıyoruz. * * UYARI! Bu dilimin eriştirdiği nesneler henüz * kurulmamışlardır. */ Öğrenci[] öğrenciler = (cast(Öğrenci*)GC.calloc(toplamBayt)) [0 .. isimler.length]; foreach (i, isim; isimler) { Öğrenci * adayAdres = öğrenciler.ptr + i; Öğrenci * öğrenciAdresi = hizalıAdres(adayAdres); writefln("adres %s: %s", i, öğrenciAdresi); const numara = 100 + i.to!int; emplace(öğrenciAdresi, isim, numara); } /* Bütün elemanları kurulmuş olduğundan bir Öğrenci dilimi * olarak kullanmakta artık bir sakınca yoktur. */ writeln(öğrenciler); }
Yukarıdaki program Öğrenci
türünün uzunluğunu, hizalama birimini, ve her öğrencinin kurulduğu adresi de yazdırıyor:
Öğrenci.sizeof: 0x18 (24) bayt Öğrenci.alignof: 0x8 (8) bayt adres 0: 7FCF0B0F2F00 adres 1: 7FCF0B0F2F18 adres 2: 7FCF0B0F2F30 [Deniz(100), Pınar(101), Irmak(102)]
Sınıf nesnelerini belirli bir yerde kurmak
Sınıf değişkenlerinin nesnenin tam türünden olması gerekmez. Örneğin, Hayvan
değişkenleri Kedi
nesnelerine de erişim sağlayabilirler. Bu yüzden emplace()
, kuracağı nesnenin türünü kendisine verilen göstergenin türünden anlayamaz ve asıl türün emplace()
'e şablon parametresi olarak bildirilmesini gerektirir. (Not: Ek olarak, sınıf göstergesi nesnenin değil, değişkenin adresi olduğundan türün açıkça belirtilmesi nesne mi yoksa değişken mi yerleştirileceği seçimini de programcıya bırakmış olur.)
Sınıf nesnelerinin kurulacağı yer void[]
türünde bir dilim olarak belirtilir. Bunlara göre sınıf nesneleri kurarken şu söz dizimi kullanılır:
Tür değişken =
emplace!Tür(voidDilimi,
/* ... kurucu parametreleri ... */);
emplace()
, belirtilen yerde bir nesne kurar ve o nesneye erişim sağlayan bir sınıf değişkeni döndürür.
Bunları denemek için bir Hayvan
sıradüzeninden yararlanalım. Bu sıradüzene ait olan nesneleri GC.calloc
ile ayrılmış olan bir belleğe yan yana yerleştireceğiz. Alt sınıfları özellikle farklı uzunlukta seçerek her nesnenin yerinin bir öncekinin uzunluğuna bağlı olarak nasıl hesaplanabileceğini göreceğiz.
interface Hayvan { string şarkıSöyle(); } class Kedi : Hayvan { string şarkıSöyle() { return "miyav"; } } class Papağan : Hayvan { string[] sözler; this(string[] sözler) { this.sözler = sözler; } string şarkıSöyle() { /* std.algorithm.joiner, belirtilen aralıktaki * elemanları belirtilen ayraçla birleştirir. */ return sözler.joiner(", ").to!string; } }
Nesnelerin yerleştirilecekleri bölgeyi GC.calloc
ile ayıracağız:
const sığa = 10_000; void * boşYer = GC.calloc(sığa);
Normalde, nesneler kuruldukça o bölgenin tükenmediğinden de emin olunması gerekir. Örneği kısa tutmak için bu konuyu gözardı edelim ve kurulacak olan iki nesnenin on bin bayta sığacaklarını varsayalım.
O bölgede önce bir Kedi
nesnesi sonra da bir Papağan
nesnesi kuracağız:
Kedi kedi = emplace!Kedi(kediYeri); // ... Papağan papağan = emplace!Papağan(papağanYeri, [ "merrba", "aloo" ]);
Dikkat ederseniz Papağan
'ın kurucusunun gerektirdiği parametreler nesnenin yerinden sonra belirtiliyorlar.
emplace()
çağrılarının döndürdükleri değişkenler bir Hayvan
dizisine eklenecekler ve daha sonra bir foreach
döngüsünde kullanılacaklar:
Hayvan[] hayvanlar; // ... hayvanlar ~= kedi; // ... hayvanlar ~= papağan; foreach (hayvan; hayvanlar) { writeln(hayvan.şarkıSöyle()); }
Diğer açıklamaları programın içine yazıyorum:
import std.stdio; import std.algorithm; import std.conv; import core.memory; // ... void main() { /* Bu bir Hayvan değişkeni dizisidir; Hayvan nesnesi * dizisi değildir. */ Hayvan[] hayvanlar; /* On bin baytın bu örnekte yeterli olduğunu varsayalım. * Normalde nesnelerin buraya gerçekten sığacaklarının da * denetlenmesi gerekir. */ const sığa = 10_000; void * boşYer = GC.calloc(sığa); /* İlk önce bir Kedi nesnesi yerleştireceğiz. */ void * kediAdayAdresi = boşYer; void * kediAdresi = hizalıAdres!Kedi(kediAdayAdresi); writeln("Kedi adresi : ", kediAdresi); /* Sınıflarda emplace()'e void[] verildiğinden adresten * dilim elde etmek gerekiyor. */ size_t kediUzunluğu = __traits(classInstanceSize, Kedi); void[] kediYeri = kediAdresi[0..kediUzunluğu]; /* Kedi'yi o yerde kuruyoruz ve döndürülen değişkeni * diziye ekliyoruz. */ Kedi kedi = emplace!Kedi(kediYeri); hayvanlar ~= kedi; /* Papağan'ı Kedi nesnesinden sonraki ilk uygun adreste * kuracağız. */ void * papağanAdayAdresi = kediAdresi + kediUzunluğu; void * papağanAdresi = hizalıAdres!Papağan(papağanAdayAdresi); writeln("Papağan adresi: ", papağanAdresi); size_t papağanUzunluğu = __traits(classInstanceSize, Papağan); void[] papağanYeri = papağanAdresi[0..papağanUzunluğu]; Papağan papağan = emplace!Papağan(papağanYeri, [ "merrba", "aloo" ]); hayvanlar ~= papağan; /* Nesneleri kullanıyoruz. */ foreach (hayvan; hayvanlar) { writeln(hayvan.şarkıSöyle()); } }
Çıktısı:
Kedi adresi : 7F869469E000 Papağan adresi: 7F869469E018 miyav merrba, aloo
Programın adımlarını açıkça gösterebilmek için bütün işlemleri main
içinde ve belirli türlere bağlı olarak yazdım. O işlemlerin iyi yazılmış bir programda yeniNesne(T)
gibi bir şablon içinde bulunmalarını bekleriz.
Nesneyi belirli bir zamanda sonlandırmak
new
işlecinin tersi, sonlandırıcı işlevin işletilmesi ve nesne için ayrılmış olan belleğin çöp toplayıcı tarafından geri alınmasıdır. Bu işlemler normalde belirsiz bir zamanda otomatik olarak işletilir.
Bazı durumlarda sonlandırıcı işlevin programcının istediği bir zamanda işletilmesi gerekebilir. Örneğin, açmış olduğu bir dosyayı sonlandırıcı işlevinde kapatan bir nesnenin sonlandırıcısının hemen işletilmesi gerekebilir.
Buradaki kullanımında "ortadan kaldır" anlamına gelen destroy()
, nesnenin sonlandırıcı işlevinin hemen işletilmesini sağlar:
destroy(değişken);
Sonlandırıcı işlevi işlettikten sonra destroy()
değişkene türünün .init
değerini atar. Sınıf değişkenlerinin ilk değeri null
olduğundan nesne o noktadan sonra kullanılamaz. destroy()
yalnızca sonlandırıcı işlevi işletir; belleğin gerçekten ne zaman geri verileceği yine de çöp toplayıcının kararına kalmıştır.
Uyarı: Yapı göstergesiyle kullanıldığında destroy()
'a göstergenin kendisi değil, gösterdiği nesne verilmelidir. Yoksa nesnenin sonlandırıcısı çağrılmaz, göstergenin kendisi null
değerini alır:
import std.stdio; struct S { int i; this(int i) { this.i = i; writefln("%s değerli nesne kuruluyor", i); } ~this() { writefln("%s değerli nesne sonlanıyor", i); } } void main() { auto g = new S(42); writeln("destroy()'dan önce"); destroy(g); // ← YANLIŞ KULLANIM writeln("destroy()'dan sonra"); writefln("g: %s", g); writeln("main'den çıkılıyor"); }
destroy()
'a gösterge verildiğinde sonlandırılan (yani, türünün .init
değeri verilen) göstergenin kendisidir:
42 değerli nesne kuruluyor destroy()'dan önce destroy()'dan sonra ← Bu satırdan önce nesne sonlanmamıştır g: null ← Onun yerine gösterge null olmuştur main'den çıkılıyor 42 değerli nesne sonlanıyor
Bu yüzden, yapı göstergesiyle kullanıldığında destroy()
'a gösterilen nesne verilmelidir:
destroy(*g); // ← Doğru kullanım
Sonlandırıcı işlevin bu sefer doğru noktada işletildiğini ve göstergenin değerinin null
olmadığını görüyoruz:
42 değerli nesne kuruluyor destroy()'dan önce 42 değerli nesne sonlanıyor ← Nesne doğru noktada sonlanmıştır destroy()'dan sonra g: 7FC5EB4EE200 ← Gösterge null olmamıştır main'den çıkılıyor 0 değerli nesne sonlanıyor ← Bir kere de S.init değeriyle
Son satır, artık S.init
değerine sahip olan nesnenin sonlandırıcısı bir kez de kapsamdan çıkılırken işletilirken yazdırılmıştır.
Nesneyi çalışma zamanında ismiyle kurmak
Object
sınıfının factory()
isimli üye işlevi türün ismini parametre olarak alır, o türden bir nesne kurar, ve adresini döndürür. factory()
, türün kurucusu için parametre almaz; bu yüzden türün parametresiz olarak kurulabilmesi şarttır:
module deneme; import std.stdio; interface Hayvan { string ses(); } class Kedi : Hayvan { string ses() { return "miyav"; } } class Köpek : Hayvan { string ses() { return "hav"; } } void main() { string[] kurulacaklar = [ "Kedi", "Köpek", "Kedi" ]; Hayvan[] hayvanlar; foreach (türİsmi; kurulacaklar) { /* "Sözde değişken" __MODULE__, her zaman için içinde * bulunulan modülün ismidir ve bir string olarak * derleme zamanında kullanılabilir. */ const tamİsim = __MODULE__ ~ '.' ~ türİsmi; writefln("%s kuruluyor", tamİsim); hayvanlar ~= cast(Hayvan)Object.factory(tamİsim); } foreach (hayvan; hayvanlar) { writeln(hayvan.ses()); } }
O programda hiç new
kullanılmadığı halde üç adet Hayvan
nesnesi oluşturulmuş ve hayvanlar
dizisine eklenmiştir:
deneme.Kedi kuruluyor deneme.Köpek kuruluyor deneme.Kedi kuruluyor miyav hav miyav
Object.factory()
'ye türün tam isminin verilmesi gerekir. O yüzden yukarıdaki tür isimleri "Kedi"
ve "Köpek"
gibi kısa olarak değil, modülün ismi ile birlikte "deneme.Kedi"
ve "deneme.Köpek"
olarak belirtiliyorlar.
factory
'nin dönüş türü Object
'tir; bu türün yukarıdaki cast(Hayvan)
kullanımında gördüğümüz gibi doğru türe açıkça dönüştürülmesi gerekir.
Özet
- Çöp toplayıcı belleği belirsiz zamanlarda tarar, artık kullanılmayan nesneleri belirler, onları sonlandırır, ve yerlerini geri alır.
- Çöp toplayıcının temizlik işlemleri
GC.collect
,GC.disable
,GC.enable
,GC.minimize
, vs. ile bir ölçüye kadar yönetilebilir. - Çöp toplayıcıdan yer ayırmak için
GC.calloc
(ve başka işlevler), ayrılmış olan belleği uzatmak içinGC.realloc
, geri vermek için deGC.free
kullanılır. - Çöp toplayıcıdan ayrılan belleğin
GC.BlkAttr.NO_SCAN
,GC.BlkAttr.NO_INTERIOR
, vs. olarak işaretlenmesi gerekebilir. .alignof
türün varsayılan hizalama birimini verir. Sınıf nesneleri içinclassInstanceAlignment
kullanılır..offsetof
bir üyenin nesnenin başlangıç adresinden kaç bayt sonra olduğunu bildirir.align
niteliği değişkenlerin, kullanıcı türlerinin, ve üyelerin hizalama birimlerini belirler.emplace
yapı nesnesi kurarken gösterge, sınıf nesnesi kurarkenvoid[]
alır.destroy
nesnenin sonlandırıcısını işletir. (destroy
'a yapı göstergesi değil, gösterilen yapı nesnesi verilmelidir.)Object.factory
uzun ismiyle belirtilen türde nesne kurar.