1. Yaddaşın əsasları: Stack və Heap (Stack & Heap)
Proqramınız işə düşəndə yaddaşa çıxış əldə edir. .NET kontekstində (CLR — Common Language Runtime) bu yaddaş avtomatik idarə olunur və iki əsas sahəyə bölünür: Stack və Heap. Məlumatların harada və necə saxlandığını anlamaq effektiv və stabil kod yazmaq üçün kritikdir.
Stack
Stack-i boşqab yığını kimi təsəvvür et: yeni boşqabı həmişə üstə qoyursan və boşqabı də üstdən götürürsən. Bu LIFO (Last In, First Out) prinsipi — «son gələn, ilk gedən». Stack çox sürətli, amma ölçüsü məhduddur.
Stack-də nələr saxlanılır:
- Value types: birbaşa elan olunmuş dəyişənlərin öz dəyərləri.
- Referanslara baxışlar (references): reference type-lar üçün stackdə yalnız obyektin heap-dəki ünvanı saxlanılır.
- Method parameters: funksiyalara ötürülən dəyərlər.
- Local variables: metod daxilində elan olunan dəyişənlər.
- Return addresses: metod bitdikdən sonra hara qayıtmaq lazım olduğunu göstərən ünvanlar.
Stack-də yaddaş avtomatik və çox sürətlə ayrılır və azad edilir — uyğun metod bitəndə və ya dəyişən scope-dan çıxdıqda.
Nümunə: Value type-lar stack-də
void MyMethod()
{
int a = 10; // 'a' və dəyər 10 - stack-də
bool flag = true; // 'flag' və dəyər true - stack-də
char initial = 'Z'; // 'initial' və dəyər 'Z' - stack-də
// ...
} // MyMethod bitdikdə 'a', 'flag', 'initial' stack-dən silinir.
Nümunə: Stack-də referanslar
class MyObject { }
void AnotherMethod()
{
MyObject objRef; // 'objRef' (referans) - stack-də. Obyekt hələ yaradılmayıb.
// ...
} // AnotherMethod bitdikdə 'objRef' (referans) stack-dən silinir.
Heap
Heap daha böyük və daha çevik yaddaş sahəsidir. Burada LIFO kimi sərt qayda yoxdur; məlumatlar istənilən boş yerə yerləşdirilə bilər. Heap daha böyük və/vəya uzunömürlü obyektlər üçün istifadə olunur.
Heap-də nələr saxlanılır:
- Reference type obyektləri: class-ların, array-lərin, string-lərin məlumatları — new vasitəsilə yaradılır.
- Nested value types: əgər value type (struct) reference type (class)-ın field-disədirsə, o obyektin içində heap-də saxlanılır.
Heap-də yaddaşın idarə olunması avtomatik olaraq GC (Garbage Collector) tərəfindən aparılır.
Nümunə: Class obyektləri heap-də
class Person { public string Name; public int Age; }
void CreatePerson()
{
Person p = new Person(); // Person obyekti - heap-də. 'p' (referans) - stack-də.
p.Name = "Alice"; // "Alice" string-i (o da obyekt) - heap-də.
p.Age = 30; // 30 (int, value type) - Person obyektinin içində heap-də.
// ...
} // CreatePerson bitdikdə 'p' (referans) stack-dən silinir.
// Person obyekti heap-də unreachable olur və GC tərəfindən toplana bilər.
Nümunə: Array-lar heap-də
void ProcessArray()
{
int[] numbers = new int[5]; // 5 int-li massiv - heap-də. 'numbers' (referans) - stack-də.
numbers[0] = 10; // 10 elementin bir hissəsidir və heap-də saxlanılır.
// ...
} // Massiv üzərində referans qalmadıqda GC tərəfindən təmizlənəcək.
2. Garbage Collector (Garbage Collection)
Heap-də yaddaşın idarə olunması avtomatik olaraq GC sayəsində baş verir. Bu .NET-in əsas xüsusiyyətlərindən biridir və yaddaşın manual boşaldılmasından (manual memory management) xilas edir.
Məqsəd və iş prinsipi
Əsas məqsəd GC — proqram tərəfindən artıq istifadə edilməyən heap obyektlərini avtomatik boşaltmaqdır.
- Obyekt yaradılması: new zamanı CLR heap-də yer ayırır.
- Referansların izlənməsi: GC aktiv referansları izləyir; obyekt "reachable" sayılırsa stack-dən, static field-dən və ya başqa reachable obyektlərdən referans var deməkdir.
- "Garbage" olaraq müəyyən etmə: referans yoxdursa obyekt "unreachable" və garbage hesab olunur.
- Toplama (Collection): lazım olduqda GC reachable obyektləri mark edir, unreachable-ların yaddaşını azad edir.
- Compaction: fragmentasiyanı azaltmaq üçün qalan obyektlər köçürülərək ardıcıl sahələr yaradılır.
GC-də nəsillər (Generational GC)
GC nəsillərdən istifadə edir, çünki əksər obyektlər "gənc vaxtlarında" ölür:
- Generation 0 (Gen 0): yeni obyektlər; collection-lar tez-tez və sürətlidir.
- Generation 1 (Gen 1): Gen 0-dan sağ çıxmış obyektlər; collection-lar daha nadir və daha uzun.
- Generation 2 (Gen 2): uzunömürlü obyektlər; ən nadir və ən bahalı collection-lar.
GC nə zaman işə düşür?
- Yeni ayırma üçün kifayət qədər boş yer olmadıqda.
- Idle vaxtlarda müntəzəm yoxlamalar zamanı.
- Explicit (ümumiyyətlə tövsiyə olunmayan) çağırışla: GC.Collect().
GC və performans
GC-nin işə düşməsi qısa pauzalara səbəb ola bilər, çünki istifadəçi kodunun icrası dayandırılır. Heap-də çox sayda kısaömürlü obyekt yaratmaq collection-ları tez-tez etdirir və performansı azaldır.
Nümunə: Obyekt unreachable olur
class DataBlock { public byte[] Data; public DataBlock() => Data = new byte[1024 * 1024]; } // 1 MB data
void AllocateAndLose()
{
DataBlock block1 = new DataBlock(); // block1 obyekti heap-də, 'block1' referansı stack-də
// ... bəzi kod bloqu ...
block1 = null; // İndi DataBlock obyektinə aktiv referans yoxdur. O unreachable oldu.
// Bu mərhələdə GC HƏLƏ ONU SİLМİR, amma o silinmə namizədi oldu.
// Burada GC.Collect() çağırışı (yalnız demo üçün, production-da etməyin!)
Console.WriteLine("Calling GC.Collect()");
GC.Collect(); // GC indi yaddaşı təmizləyə bilər (amma zəmanət yoxdur)
}
AllocateAndLose();
// AllocateAndLose bitdikdə 'block1' referansı stack-dən silinir,
// və heap-dəki DataBlock obyekti garbage collection üçün namizəd olur.
3. Unmanaged resursların idarə olunması
GC .NET-in "managed" yaddaşını idarə edir. Amma unmanaged resurslar var ki, onlar GC-nin nəzarətindən kənardadır və onların əl ilə azad edilməsi tələb olunur:
- File descriptors (açıq fayllar).
- Network sockets.
- Window/graphics handles (məsələn, GDI+).
- OS-dən ayrılmış yaddaş (məsələn, P/Invoke vasitəsilə).
Finalizerlər (Destructors)
Finalizer — obyekt GC tərəfindən silinməzdən əvvəl unmanaged resursların son təmizlənməsini edən xüsusi metoddur. Sintaksis konstruktor kimi görünür, amma önündə tilde var:
class MyClassWithFinalizer
{
~MyClassWithFinalizer() { /* Unmanaged resursların azad edilməsi */ }
}
Finalizer GC tərəfindən qeyri-müəyyən vaxtda, finalizer thread-də çağırılır. Mənfi cəhətlər: qeyri-müəyyənlik, əlavə overhead (finalizer-li obyektlərə iki keçid lazım olur). Buna görə finalizerlər yalnız "sığorta" kimi istifadə olunmalıdır, əsas mexanizm deyillər.
Nümunə: Finalizer "sığorta" kimi
class UnmanagedResourceHolder
{
private bool _resourceReleased = false;
// Unmanaged resursun simuliasiyası
public UnmanagedResourceHolder() => Console.WriteLine("Resurs yaradıldı.");
public void ReleaseResource()
{
if (!_resourceReleased)
{
Console.WriteLine("Resurs AÇIQ-çəxildi (explicit metodla).");
_resourceReleased = true;
}
}
// Finalizer - GC tərəfindən son çarə kimi çağırılır
~UnmanagedResourceHolder()
{
Console.WriteLine("Finalizer ÇAĞIRILDI (resurs explicit azad edilmədi).");
ReleaseResource(); // Resursu azad etməyə cəhd
}
}
IDisposable interfeysi
IDisposable — resursların explicit azad edilməsi üçün üstünlük verilən mexanizmdir. Bir metoda sahibdir: void Dispose(). Tez-tez Dispose() çoxistifadəlidir və finalizer-i söndürmək üçün GC.SuppressFinalize(this) çağırışı edilir.
Nümunə: IDisposable-in implementasiyası
using System.IO;
class MyFileWriter : IDisposable
{
private StreamWriter _writer;
private bool _disposed = false;
public MyFileWriter(string path)
{
_writer = new StreamWriter(path, true);
Console.WriteLine($"Fayl '{path}' açıldı.");
}
public void WriteLog(string message) => _writer.WriteLine(message);
// IDisposable implementasiyası
public void Dispose()
{
Dispose(true); // Əsas Dispose məntiqini çağırırıq
GC.SuppressFinalize(this); // GC-ə finalizer-in artıq lazım olmadığını deyirik
_disposed = true;
}
// Protected virtual Dispose(bool) - ümumi pattern üçün
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Managed resursları azad et
_writer?.Dispose();
}
// Unmanaged resursları azad et (əgər varsa)
Console.WriteLine("Fayl Dispose vasitəsilə BAĞLANDI.");
}
}
// Optional finalizer "sığorta" kimi
~MyFileWriter()
{
Console.WriteLine("Fayl finalizer vasitəsilə BAĞLANDI (Dispose() çağırılmamışdı).");
Dispose(false); // Dispose-u explicit çağırılmış kimi yox, finalizer rejimində çağırırıq
}
}
using operatoru
using operatoru sintaktik şəkərdir və obyekt scope-dan çıxdıqda Dispose()-un çağırılmasını təmin edir, hətta exception baş versə də. IDisposable implement edən obyektlərlə işləməyin ən tövsiyə olunan yoludur.
Nümunə: using ilə avtomatik təmizləmə
// Yuxarıdakı MyFileWriter nümunəsindən istifadə
void ProcessFile(string fileName)
{
// MyFileWriter obyekti using bloku bitəndə avtomatik "dispose" olunacaq
using (var writer = new MyFileWriter(fileName))
{
writer.WriteLog("Birinci sətir.");
writer.WriteLog("İkinci sətir.");
// Burada exception olsa belə, Dispose() çağırılacaq
} // Burada avtomatik olaraq writer.Dispose() çağırılır
Console.WriteLine("Using bloku bitdi, fayl bağlandı.");
}
ProcessFile("testlog.txt");
// Bu halda MyFileWriter-in finalizer-i çağırılmayacaq, çünki Dispose() explicit çağırıldı.
Bu konsepsiyaları anlamaq C# ilə performanslı, etibarlı və miqyaslana bilən tətbiqlər yazmaq üçün açardır.
GO TO FULL VERSION